Source code for segnomms.svg.core

"""Core SVG building functionality.

This module provides the base SVG building operations including creating
root elements, adding styles, and managing backgrounds.
"""

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

from ..core.interfaces import SVGBuilder
from .models import BackgroundConfig, SVGElementConfig


[docs] class CoreSVGBuilder(SVGBuilder): """Core SVG document builder for basic operations. Handles fundamental SVG operations like creating root elements, adding CSS styles, and managing background elements. """
[docs] def create_svg_root(self, width: int, height: int, **kwargs: Any) -> ET.Element: """Create the root SVG element with proper attributes and namespaces. Args: width: Width of the SVG in pixels height: Height of the SVG in pixels **kwargs: Additional attributes for the SVG element Returns: Root SVG Element with configured attributes """ config = SVGElementConfig(width=width, height=height, **kwargs) # Create SVG element with namespace ET.register_namespace("", "http://www.w3.org/2000/svg") ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") svg = ET.Element( "svg", attrib={ "width": str(config.width), "height": str(config.height), "viewBox": f"0 0 {config.width} {config.height}", "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", }, ) # Add ID if provided if config.id: svg.set("id", config.id) # Add CSS classes if provided if config.css_class: svg.set("class", config.css_class) return svg
[docs] def add_styles( self, svg: ET.Element, interactive: bool = False, animation_config: Optional[Dict[str, Any]] = None, ) -> None: """Add CSS styles to the SVG. Args: svg: SVG element to add styles to interactive: Whether to include interactive hover styles animation_config: Optional animation configuration dict """ # Check if styles already exist existing_style = svg.find(".//style") if existing_style is not None: return style_content = self._generate_css_styles(interactive, animation_config) if style_content: style = ET.SubElement(svg, "style", attrib={"type": "text/css"}) style.text = f"\n<![CDATA[\n{style_content}\n]]>\n"
[docs] def add_background(self, svg: ET.Element, width: int, height: int, color: str, **kwargs: Any) -> None: """Add a background rectangle to the SVG. Args: svg: SVG element to add background to width: Width of the background height: Height of the background color: Background color **kwargs: Additional background configuration options """ config = BackgroundConfig(width=width, height=height, color=color, **kwargs) # Create background rectangle ET.SubElement( svg, "rect", attrib={ "x": "0", "y": "0", "width": str(config.width), "height": str(config.height), "fill": config.color, "class": "qr-background", }, )
def _generate_css_styles( self, interactive: bool, animation_config: Optional[Dict[str, Any]] = None ) -> str: """Generate CSS styles for the SVG. Args: interactive: Whether to include interactive styles animation_config: Optional animation configuration dict Returns: CSS style string """ styles = [] # Base styles (matching original) styles.append( """ .qr-background { pointer-events: none; } /* Transform geometry fixes to prevent wiggle during animations */ .qr-root, .qr-module, .qr-finder, .qr-timing, .qr-data, .qr-alignment, .qr-format, .qr-cluster, .qr-contour { transform-box: fill-box; transform-origin: center; animation-fill-mode: both; } .qr-module { transition: all 0.2s ease; } .qr-finder { fill: currentColor; } .qr-finder-inner { fill: var(--qr-bg-color, white); } .qr-timing { fill: currentColor; } .qr-data { fill: currentColor; } .qr-alignment { fill: currentColor; } .qr-format { fill: currentColor; } .qr-cluster { fill: currentColor; stroke: none; } .qr-contour { fill: currentColor; stroke: none; } """ ) # Interactive styles (matching original) if interactive: styles.append( """ .qr-module:hover { filter: brightness(1.2); } .qr-module.clickable { cursor: pointer; } .qr-module.clickable:hover { filter: brightness(1.3) drop-shadow(0 0 3px rgba(0,0,0,0.3)); } .qr-module.selected { filter: brightness(1.5) drop-shadow(0 0 5px rgba(255,255,0,0.8)); } .qr-finder:hover { fill: #333; } .qr-data:hover { fill: #666; } .qr-timing:hover { fill: #999; } .qr-cluster:hover { fill: #444; stroke: #666; stroke-width: 1; } .qr-contour:hover { fill: #555; stroke: #777; stroke-width: 1; } @media (prefers-reduced-motion: reduce) { .qr-module { transition: none; } .qr-module:hover { transform: none; } } """ ) # Animation styles if animation_config: animation_styles = self._generate_animation_styles(animation_config) if animation_styles: styles.append(animation_styles) return "\n".join(styles) def _generate_animation_styles(self, config: Dict[str, Any]) -> str: """Generate CSS animation styles based on configuration. Args: config: Animation configuration dictionary Returns: CSS animation styles string """ animation_styles = [] # Extract animation settings fade_in = config.get("animation_fade_in", False) fade_duration = config.get("animation_fade_duration", 0.5) stagger = config.get("animation_stagger", False) stagger_delay = config.get("animation_stagger_delay", 0.02) pulse = config.get("animation_pulse", False) timing = config.get("animation_timing", "ease") # Fade-in animation if fade_in: animation_styles.append( f""" @keyframes qrFadeIn {{ from {{ opacity: 0; transform: scale(0.8); }} to {{ opacity: 1; transform: scale(1); }} }} .qr-module {{ animation: qrFadeIn {fade_duration}s {timing} both; }} """ ) # Stagger support using CSS variables if stagger: animation_styles.append( f""" /* Staggered animation delays using CSS variables */ .qr-root {{ --stagger-step: {stagger_delay}s; }} .qr-module {{ animation-delay: calc(var(--i, 0) * var(--stagger-step)); }} """ ) # Pulse effect for finder patterns (using halos) if pulse: animation_styles.append( f""" @keyframes qrPulse {{ 0%, 100% {{ transform: scale(1); opacity: 0.3; }} 50% {{ transform: scale(1.1); opacity: 0.1; }} }} /* Pulse effect on finder halos only, not the actual patterns */ .finder-halo {{ animation: qrPulse 2s {timing} infinite; transform-origin: center; pointer-events: none; }} .finder-halo:nth-child(1) {{ animation-delay: 0s; }} .finder-halo:nth-child(2) {{ animation-delay: 0.66s; }} .finder-halo:nth-child(3) {{ animation-delay: 1.33s; }} /* Ensure actual finder patterns are not animated */ .qr-finder {{ animation: none !important; }} """ ) # Respect reduced motion preferences animation_styles.append( """ @media (prefers-reduced-motion: reduce) { .qr-root * { animation: none !important; transition: none !important; animation-delay: 0s !important; } } @media print { .qr-root * { animation: none !important; transition: none !important; } } """ ) return "\n".join(animation_styles)