190 lines
7.1 KiB
Python
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() |