Source code for segnomms.svg.definitions

"""SVG definitions builder for gradients, patterns, and filters.

This module handles creating and managing SVG definition elements like
gradients, patterns, and filters that can be reused throughout the document.
"""

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

from .models import GradientConfig


[docs] class DefinitionsBuilder: """Builder for SVG definition elements. Manages gradients, patterns, filters, and other reusable SVG elements that belong in the <defs> section. """
[docs] def add_definitions( self, svg: ET.Element, gradients: Optional[Sequence[Union[GradientConfig, Dict[str, Any]]]] = None, patterns: Optional[Sequence[Dict[str, Any]]] = None, filters: Optional[Sequence[Dict[str, Any]]] = None, ) -> ET.Element: """Add definitions section to SVG with gradients, patterns, and filters. Args: svg: SVG element to add definitions to gradients: List of gradient configurations patterns: List of pattern configurations filters: List of filter configurations Returns: The defs element """ # Find or create defs element defs = svg.find(".//defs") if defs is None: defs = ET.SubElement(svg, "defs") # Add gradients if gradients: for gradient in gradients: self._add_gradient(defs, gradient) # Add patterns if patterns: for pattern in patterns: self._add_pattern(defs, pattern) # Add filters if filters: for filter_config in filters: self._add_filter(defs, filter_config) return defs
def _add_gradient(self, defs: ET.Element, gradient: Union[GradientConfig, Dict[str, Any]]) -> None: """Add a gradient definition to the defs element. Args: defs: Defs element to add gradient to gradient: Gradient configuration """ # Handle dictionary format (test format or raw dict) if isinstance(gradient, dict): self._add_gradient_from_dict(defs, gradient) return # Handle GradientConfig object if gradient.gradient_type == "linear": grad_elem = ET.SubElement( defs, "linearGradient", attrib={ "id": gradient.gradient_id, "x1": str(gradient.x1), "y1": str(gradient.y1), "x2": str(gradient.x2), "y2": str(gradient.y2), }, ) else: # radial grad_elem = ET.SubElement( defs, "radialGradient", attrib={ "id": gradient.gradient_id, "cx": str(gradient.cx), "cy": str(gradient.cy), "r": str(gradient.r), }, ) # Note: fx and fy not supported in current GradientConfig model # Add gradient stops if gradient.stops is not None: for i, stop_position in enumerate(gradient.stops): # Use color from colors list color = gradient.colors[i] if i < len(gradient.colors) else gradient.colors[-1] ET.SubElement( grad_elem, "stop", attrib={ "offset": str(stop_position), "stop-color": color, "stop-opacity": "1", }, ) else: # Auto-generate stops based on colors for i, color in enumerate(gradient.colors): offset = i / (len(gradient.colors) - 1) if len(gradient.colors) > 1 else 0 ET.SubElement( grad_elem, "stop", attrib={ "offset": str(offset), "stop-color": color, "stop-opacity": "1", }, ) def _add_gradient_from_dict(self, defs: ET.Element, gradient: Dict[str, Any]) -> None: """Add a gradient definition from dictionary format. This method handles the test format which may not match GradientConfig model. Args: defs: Defs element to add gradient to gradient: Gradient configuration dictionary """ gradient_type = gradient.get("gradient_type", gradient.get("type", "linear")) gradient_id = gradient.get("gradient_id", gradient.get("id")) if gradient_type == "linear": # Build attributes, preserving original format attrib = {"id": gradient_id} # Add coordinates, preserving original format (percentages, etc.) for coord in ["x1", "y1", "x2", "y2"]: if coord in gradient: attrib[coord] = str(gradient[coord]) # Filter out None values for ET.SubElement compatibility filtered_attrib = {k: v for k, v in attrib.items() if v is not None} grad_elem = ET.SubElement(defs, "linearGradient", attrib=filtered_attrib) else: # radial # Build attributes for radial gradient attrib = {"id": gradient_id} # Add coordinates for radial gradients for coord in ["cx", "cy", "r", "fx", "fy"]: if coord in gradient: attrib[coord] = str(gradient[coord]) # Filter out None values for ET.SubElement compatibility filtered_attrib = {k: v for k, v in attrib.items() if v is not None} grad_elem = ET.SubElement(defs, "radialGradient", attrib=filtered_attrib) # Add gradient stops from test format stops = gradient.get("stops", []) if stops: for stop in stops: if isinstance(stop, dict): stop_attrib = {} if "offset" in stop: stop_attrib["offset"] = str(stop["offset"]) if "color" in stop: stop_attrib["stop-color"] = stop["color"] if "opacity" in stop: stop_attrib["stop-opacity"] = str(stop["opacity"]) else: stop_attrib["stop-opacity"] = "1" ET.SubElement(grad_elem, "stop", attrib=stop_attrib) def _add_pattern(self, defs: ET.Element, pattern: Dict[str, Any]) -> None: """Add a pattern definition to the defs element. Args: defs: Defs element to add pattern to pattern: Pattern configuration dictionary """ pattern_elem = ET.SubElement( defs, "pattern", attrib={ "id": pattern["id"], "x": str(pattern.get("x", 0)), "y": str(pattern.get("y", 0)), "width": str(pattern["width"]), "height": str(pattern["height"]), "patternUnits": pattern.get("patternUnits", "userSpaceOnUse"), }, ) # Add pattern content if "content" in pattern: # Pattern content should be a list of element configurations for element in pattern["content"]: self._create_pattern_element(pattern_elem, element) def _add_filter(self, defs: ET.Element, filter_config: Dict[str, Any]) -> None: """Add a filter definition to the defs element. Args: defs: Defs element to add filter to filter_config: Filter configuration dictionary """ filter_elem = ET.SubElement(defs, "filter", attrib={"id": filter_config["id"]}) # Add filter primitives based on type filter_type = filter_config.get("type", "custom") if filter_type == "blur": ET.SubElement( filter_elem, "feGaussianBlur", attrib={"stdDeviation": str(filter_config.get("stdDeviation", 2))}, ) elif filter_type in ("shadow", "dropShadow"): # Drop shadow filter _ = ET.SubElement( filter_elem, "feGaussianBlur", attrib={ "in": "SourceAlpha", "stdDeviation": str(filter_config.get("blur", 3)), }, ) _ = ET.SubElement( filter_elem, "feOffset", attrib={ "dx": str(filter_config.get("dx", 2)), "dy": str(filter_config.get("dy", 2)), "result": "offsetblur", }, ) _ = ET.SubElement( filter_elem, "feFlood", attrib={ "flood-color": filter_config.get("color", "#000000"), "flood-opacity": str(filter_config.get("opacity", 0.5)), }, ) _ = ET.SubElement( filter_elem, "feComposite", attrib={"in2": "offsetblur", "operator": "in"}, ) merge = ET.SubElement(filter_elem, "feMerge") ET.SubElement(merge, "feMergeNode") ET.SubElement(merge, "feMergeNode", attrib={"in": "SourceGraphic"}) elif filter_type == "custom" and "primitives" in filter_config: # Custom filter with explicit primitives for primitive in filter_config["primitives"]: self._create_filter_primitive(filter_elem, primitive) def _create_pattern_element(self, pattern_elem: ET.Element, element_config: Dict[str, Any]) -> None: """Create an element within a pattern. Args: pattern_elem: Pattern element to add content to element_config: Element configuration """ elem_type = element_config.get("type", "rect") attribs = {k: str(v) for k, v in element_config.items() if k != "type"} ET.SubElement(pattern_elem, elem_type, attrib=attribs) def _create_filter_primitive(self, filter_elem: ET.Element, primitive_config: Dict[str, Any]) -> None: """Create a filter primitive element. Args: filter_elem: Filter element to add primitive to primitive_config: Primitive configuration """ prim_type = primitive_config.get("type") if prim_type: attribs = {k: str(v) for k, v in primitive_config.items() if k != "type"} ET.SubElement(filter_elem, prim_type, attrib=attribs)