svg-processor/processing/geometry_extractor.py
2025-05-16 18:15:31 +04:00

190 lines
7.1 KiB
Python

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()