Source code for segnomms.svg.accessibility

"""Accessibility builder for SVG documents.

This module handles accessibility enhancements including ARIA attributes,
semantic structure, and screen reader support.
"""

import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional

from ..a11y.accessibility import (
    AccessibilityConfig,
    AccessibilityEnhancer,
    create_basic_accessibility,
)
from .models import TitleDescriptionConfig


[docs] class AccessibilityBuilder: """Builder for SVG accessibility features. Manages ARIA attributes, semantic structure, titles, descriptions, and other accessibility enhancements. """
[docs] def __init__(self, accessibility_config: Optional[AccessibilityConfig] = None): """Initialize the accessibility builder. Args: accessibility_config: Configuration for accessibility features """ if accessibility_config and accessibility_config.enabled: self.enhancer = AccessibilityEnhancer(accessibility_config) else: # Create basic accessibility config and enhancer basic_config = create_basic_accessibility() self.enhancer = AccessibilityEnhancer(basic_config)
[docs] def add_title_and_description( self, svg: ET.Element, title: Optional[str] = None, description: Optional[str] = None, ) -> None: """Add title and description elements for accessibility. Args: svg: SVG element to add title and description to title: Title text for the SVG description: Description text for the SVG """ if title is None or title == "": return # Nothing to add config = TitleDescriptionConfig(title=title, description=description) # Add title element if config.title: # Remove any existing title existing_title = svg.find(".//title") if existing_title is not None: svg.remove(existing_title) title_elem = ET.Element("title") title_elem.text = config.title svg.insert(0, title_elem) # Add description element if config.description: # Remove any existing description existing_desc = svg.find(".//desc") if existing_desc is not None: svg.remove(existing_desc) desc_elem = ET.Element("desc") desc_elem.text = config.description # Insert after title if it exists, otherwise at beginning insert_pos = 1 if config.title else 0 svg.insert(insert_pos, desc_elem) # Set ARIA attributes if config.title or config.description: # Ensure proper ARIA labeling label_id = svg.get("aria-labelledby", "") desc_id = svg.get("aria-describedby", "") if config.title and not label_id: title_elem_found = svg.find(".//title") if title_elem_found is not None: title_id = "qr-title" # Use deterministic ID title_elem_found.set("id", title_id) svg.set("aria-labelledby", title_id) if config.description and not desc_id: desc_elem_found = svg.find(".//desc") if desc_elem_found is not None: desc_id = "qr-desc" # Use deterministic ID desc_elem_found.set("id", desc_id) svg.set("aria-describedby", desc_id)
[docs] def create_layered_structure(self, svg: ET.Element) -> Dict[str, ET.Element]: """Create a semantic layered structure for the SVG. Creates groups for different layers (background, modules, overlays) with proper semantic markup. Args: svg: SVG element to structure Returns: Dictionary mapping layer names to group elements """ # NOTE: Future feature - configurable layer structure via LayerStructureConfig # config = LayerStructureConfig() layers = {} # Background layer (direct child of SVG) bg_group = ET.SubElement( svg, "g", attrib={ "id": "segnomms-background", "class": "qr-layer-background", "aria-hidden": "true", }, ) layers["background"] = bg_group # Quiet zone layer (direct child of SVG) qz_group = ET.SubElement( svg, "g", attrib={ "id": "segnomms-quiet-zone", "class": "qr-layer-quiet-zone", "aria-hidden": "true", }, ) layers["quiet_zone"] = qz_group # Modules layer (direct child of SVG - main content) modules_group = ET.SubElement( svg, "g", attrib={ "id": "segnomms-modules", "class": "qr-modules", "role": "presentation", }, ) layers["modules"] = modules_group # Pattern groups within modules (also returned as accessible layers) for pattern_type in [ "finder", "timing", "alignment", "format", "version", "data", ]: pattern_group = ET.SubElement( modules_group, "g", attrib={ "class": f"qr-pattern-{pattern_type}", "data-pattern-type": pattern_type, }, ) # Add to layers for accessibility enhancement layers[f"pattern_{pattern_type}"] = pattern_group # Effects layer (direct child of SVG - for frame effects, overlays, etc.) effects_group = ET.SubElement( svg, "g", attrib={ "id": "segnomms-frame-effects", "class": "frame-effects", "pointer-events": "none", }, ) layers["effects"] = effects_group # Frame layer (for backward compatibility) layers["frame"] = effects_group # Interactive overlay layer (for backward compatibility) layers["overlay"] = effects_group return layers
[docs] def enhance_module_accessibility( self, element: ET.Element, row: int, col: int, module_type: str = "data" ) -> None: """Enhance accessibility for individual QR modules. Args: element: Module element to enhance row: Row position of the module col: Column position of the module module_type: Type of QR module """ # Use the accessibility enhancer self.enhancer.enhance_module_element(element, row, col, module_type)
[docs] def enhance_pattern_group_accessibility( self, group_element: ET.Element, pattern_type: str, module_count: int ) -> None: """Enhance accessibility for pattern groups. Args: group_element: Group element containing pattern modules pattern_type: Type of pattern (finder, timing, etc.) module_count: Number of modules in the pattern """ # Use the accessibility enhancer # Note: AccessibilityEnhancer expects 'index' parameter, but we receive module_count # In the test context, module_count is actually used as the pattern index self.enhancer.enhance_pattern_group(group_element, pattern_type, module_count)
[docs] def get_accessibility_report(self) -> Dict[str, Any]: """Get a report of accessibility features applied. Returns: Dictionary containing accessibility metrics and features """ return self.enhancer.generate_accessibility_report()
[docs] def validate_accessibility(self) -> List[str]: """Validate accessibility compliance. Returns: List of validation warnings or errors """ return self.enhancer.validate_accessibility()
[docs] def apply_final_enhancements(self, svg: ET.Element) -> None: """Apply final accessibility enhancements to the complete SVG. Args: svg: Complete SVG element """ # Ensure SVG has proper role if not svg.get("role"): svg.set("role", "img") # Check for title and add generic one if missing if not svg.find(".//title"): self.add_title_and_description(svg, title="QR Code") # Ensure proper focus management focusable_elements = svg.findall(".//*[@tabindex]") if focusable_elements: # Set up focus order for i, elem in enumerate(focusable_elements): if i == 0: elem.set("tabindex", "0") else: elem.set("tabindex", "-1") # Add skip link for keyboard navigation if self.enhancer.config.enable_keyboard_navigation: skip_link = ET.Element( "a", attrib={ "href": "#qr-content", "class": "qr-skip-link", "style": "position: absolute; left: -9999px;", }, ) skip_link.text = "Skip to QR content" svg.insert(0, skip_link)