import logging import math from typing import Dict, Optional try: from svg.path import parse_path, Path except ImportError: Path = None logger = logging.getLogger(__name__) # Default values to use when calculations fail DEFAULT_SIZE = 10.0 DEFAULT_POSITION = 0.0 DEFAULT_GEOMETRY = { 'center_x': DEFAULT_POSITION, 'center_y': DEFAULT_POSITION, 'width': DEFAULT_SIZE, 'height': DEFAULT_SIZE } class GeometryExtractor: """Extract geometry based on SVG element type""" @staticmethod def extract_rect(element) -> Dict[str, float]: """Extract geometry for a rectangle""" x = float(element.getAttribute('x') or 0) y = float(element.getAttribute('y') or 0) width = float(element.getAttribute('width') or 0) height = float(element.getAttribute('height') or 0) return { 'center_x': x + width / 2, 'center_y': y + height / 2, 'width': width, 'height': height } @staticmethod def extract_circle(element) -> Dict[str, float]: """Extract geometry for a circle""" cx = float(element.getAttribute('cx') or 0) cy = float(element.getAttribute('cy') or 0) r = float(element.getAttribute('r') or 0) return { 'center_x': cx, 'center_y': cy, 'width': r * 2, 'height': r * 2 } @staticmethod def extract_ellipse(element) -> Dict[str, float]: """Extract geometry for an ellipse""" cx = float(element.getAttribute('cx') or 0) cy = float(element.getAttribute('cy') or 0) rx = float(element.getAttribute('rx') or 0) ry = float(element.getAttribute('ry') or 0) return { 'center_x': cx, 'center_y': cy, 'width': rx * 2, 'height': ry * 2 } @staticmethod def extract_line(element) -> Dict[str, float]: """Extract geometry for a line""" x1 = float(element.getAttribute('x1') or 0) y1 = float(element.getAttribute('y1') or 0) x2 = float(element.getAttribute('x2') or 0) y2 = float(element.getAttribute('y2') or 0) return { 'center_x': (x1 + x2) / 2, 'center_y': (y1 + y2) / 2, 'width': abs(x2 - x1), 'height': abs(y2 - y1) } @staticmethod def extract_path(element, center_mode='bbox') -> Dict[str, float]: """Extract geometry for a path""" if Path is None: logger.warning("svg.path library not found. Cannot calculate accurate path bounding box.") return DEFAULT_GEOMETRY.copy() d_attr = element.getAttribute('d') if not d_attr: logger.debug("Path element has no 'd' attribute.") return DEFAULT_GEOMETRY.copy() try: parsed_path = parse_path(d_attr) if not isinstance(parsed_path, Path) or len(parsed_path) == 0: logger.warning(f"Parsed path is empty or invalid: {d_attr[:50]}...") return DEFAULT_GEOMETRY.copy() # Calculate bounding box xmin, xmax = math.inf, -math.inf ymin, ymax = math.inf, -math.inf for i, segment in enumerate(parsed_path): if not hasattr(segment, 'start') or not hasattr(segment, 'end'): logger.warning(f"Segment {i} of type {type(segment)} lacks 'start' or 'end' attributes. Skipping.") continue # Process start and end points points = [(segment.start.real, segment.start.imag), (segment.end.real, segment.end.imag)] # Add control points if present if hasattr(segment, 'control1'): points.append((segment.control1.real, segment.control1.imag)) if hasattr(segment, 'control2'): points.append((segment.control2.real, segment.control2.imag)) elif hasattr(segment, 'control'): points.append((segment.control.real, segment.control.imag)) # Update bounds for x, y in points: xmin = min(xmin, x) xmax = max(xmax, x) ymin = min(ymin, y) ymax = max(ymax, y) # Check if bounds were updated if not all(map(math.isfinite, [xmin, xmax, ymin, ymax])): logger.warning(f"Could not calculate finite bounding box for path: {d_attr[:50]}...") return DEFAULT_GEOMETRY.copy() width = max(xmax - xmin, 1.0) # Ensure non-zero width height = max(ymax - ymin, 1.0) # Ensure non-zero height # Calculate center based on mode if center_mode == 'centroid': # Fallback to bbox center for now logger.warning("Centroid calculation not yet implemented for path. Falling back to bbox center.") center_x = xmin + width / 2 center_y = ymin + height / 2 else: # Default to bounding box center center_x = xmin + width / 2 center_y = ymin + height / 2 logger.debug(f"Calculated path geom (mode={center_mode}): xmin={xmin:.2f}, xmax={xmax:.2f}, " f"ymin={ymin:.2f}, ymax={ymax:.2f} -> W={width:.2f}, H={height:.2f}, " f"C=({center_x:.2f}, {center_y:.2f})") return { 'center_x': center_x, 'center_y': center_y, 'width': width, 'height': height } except Exception as e: logger.error(f"Error processing path data '{d_attr[:50]}...': {e}") return DEFAULT_GEOMETRY.copy() def get_element_geometry(element, svg_type, center_mode='bbox') -> Dict[str, float]: """Extract geometry information (center, size) from an SVG element before applying transformations. Args: element: The SVG element svg_type: The tag name of the element (e.g., 'rect', 'path') center_mode: How to calculate the center ('bbox' or 'centroid') Returns: dict: Contains 'center_x', 'center_y', 'width', 'height' before transform """ extractors = { 'rect': GeometryExtractor.extract_rect, 'circle': GeometryExtractor.extract_circle, 'ellipse': GeometryExtractor.extract_ellipse, 'line': GeometryExtractor.extract_line, 'path': lambda elem: GeometryExtractor.extract_path(elem, center_mode) } try: if svg_type in extractors: return extractors[svg_type](element) else: logger.warning(f"Unsupported element type for geometry extraction: {svg_type}") return DEFAULT_GEOMETRY.copy() except (ValueError, TypeError, NameError) as e: logger.error(f"Error parsing geometry attributes for {svg_type}: {e}") return DEFAULT_GEOMETRY.copy()