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

254 lines
11 KiB
Python

import logging
import re
import numpy as np
import math
from typing import Dict, Any, Tuple, Optional
# Group inheritance behavior:
# - Group prefixes always override individual element prefixes
# - Group suffixes always override individual element suffixes
# - This ensures consistent behavior for all elements within a group
# Import necessary functions/modules
from . import svg_math
from . import svg_utils
from . import json_builder
from . import element_mapper
from .geometry_extractor import GeometryExtractor, get_element_geometry, DEFAULT_GEOMETRY
from .prefix_resolver import PrefixResolver
from .position_utils import extract_position_from_attributes
logger = logging.getLogger(__name__)
def process_element(element, element_count, svg_type, svg_dimensions, custom_options,
debug=False, group_prefix=None, group_suffix=None, center_mode='bbox') -> Dict[str, Any]:
"""Process a single SVG element and return its JSON representation.
Args:
element: The SVG element
element_count: A counter for this element type
svg_type: The tag name (e.g., 'rect')
svg_dimensions: (width, height) of the parent SVG
custom_options: Configuration options including mappings
debug: Enable debug logging
group_prefix: Prefix inherited from a parent group
group_suffix: Suffix inherited from a parent group
center_mode: How to calculate the center for paths ('bbox' or 'centroid')
Returns:
dict: The JSON representation of the element, or default on error
"""
try:
debug_buffer = []
if debug:
logger.debug(f"Processing {svg_type} #{element_count} (Group context: prefix='{group_prefix}', suffix='{group_suffix}')")
debug_buffer.append(f"Processing {svg_type} #{element_count}")
# --- Get element name and identifiers ---
element_id = element.getAttribute('id') or ""
element_label = element.getAttribute('inkscape:label') or element_id
element_name = element_label or f"{svg_type}{element_count}"
original_name = element_name
# --- Mapping Logic ---
element_mappings = custom_options.get('element_mappings', [])
element_candidate_prefix = PrefixResolver.get_element_prefix(element_label)
if debug and element_candidate_prefix:
logger.debug(f"Element '{element_label}' has candidate prefix: '{element_candidate_prefix}'")
# Determine which prefix to use for mapping lookup
prefix_to_find, prefix_source = PrefixResolver.determine_prefix_to_use(
element_candidate_prefix, group_prefix, element_mappings, svg_type
)
if debug:
if prefix_source == "element":
logger.debug(f"Using element's own prefix '{prefix_to_find}' (no group prefix override)")
elif prefix_source == "group":
logger.debug(f"Group prefix '{prefix_to_find}' overrides element prefix '{element_candidate_prefix}'")
else:
logger.debug(f"No valid prefix found, will look for fallback mapping")
# Find the appropriate mapping
mapping_to_use, match_type = element_mapper.find_mapping_for_element(
svg_type, prefix_to_find, element_mappings, debug
)
# Determine label prefix for JSON meta
label_prefix = ""
has_prefix_mapping = False
if prefix_source == "element" and mapping_to_use:
label_prefix = mapping_to_use.get('label_prefix', '')
has_prefix_mapping = True
elif prefix_source == "group" and mapping_to_use:
label_prefix = group_prefix
has_prefix_mapping = True
if debug:
logger.debug(f"Element '{element_label}' inherited mapping via group prefix '{prefix_to_find}'.")
if debug:
logger.debug(f"Mapping result: type='{match_type}', mapping={mapping_to_use}")
# --- Geometry and Transformation ---
geometry = get_element_geometry(element, svg_type, center_mode)
orig_center_x = geometry['center_x']
orig_center_y = geometry['center_y']
geom_width = geometry['width']
geom_height = geometry['height']
logger.debug(f"Geometric Center=({orig_center_x:.2f}, {orig_center_y:.2f}), Size=({geom_width:.2f}x{geom_height:.2f})")
# Get transformation matrix and apply it
transform_matrix = svg_math.get_combined_transform_matrix(element)
logger.debug(f"Transform matrix:\n{transform_matrix}")
transformed_center_x, transformed_center_y = svg_math.apply_transform(
(orig_center_x, orig_center_y), transform_matrix
)
logger.debug(f"Transformed Center=({transformed_center_x:.2f}, {transformed_center_y:.2f})")
# Extract rotation angle
rotation_angle = svg_math.extract_rotation_from_transform(element)
# --- Determine Final Size ---
# Start with mapped values, then fall back to geometric size
element_width = mapping_to_use.get('width') if mapping_to_use else None
element_height = mapping_to_use.get('height') if mapping_to_use else None
if element_width is not None or element_height is not None:
logger.debug(f"Using dimensions from {match_type} mapping: W={element_width}, H={element_height}")
# Use geometric size if not specified in mapping
if element_width is None:
element_width = geom_width
if debug:
logger.debug(f"Using geometric width as fallback: {element_width:.2f}")
if element_height is None:
element_height = geom_height
if debug:
logger.debug(f"Using geometric height as fallback: {element_height:.2f}")
# Ensure final dimensions are non-zero
element_width = max(element_width, 1.0)
element_height = max(element_height, 1.0)
logger.debug(f"Final dimensions: {element_width:.2f}x{element_height:.2f}")
# --- Calculate Final Position ---
# Calculate top-left from transformed center and final size
final_x = transformed_center_x - element_width / 2
final_y = transformed_center_y - element_height / 2
logger.debug(f"Calculated initial centered position: ({final_x:.2f}, {final_y:.2f})")
# Check for suspicious default position (-10,-10)
if abs(final_x + 10.0) < 1e-6 and abs(final_y + 10.0) < 1e-6:
logger.warning(f"Position is suspiciously close to (-10,-10) for {element_name}.")
logger.debug(f"SVG type={svg_type}, element_id={element_id}")
logger.debug(f"Original geometry: {geometry}")
logger.debug(f"Transform attribute: {element.getAttribute('transform')}")
if abs(final_x + 10.0) < 1e-9 and abs(final_y + 10.0) < 1e-9:
# Try to use the original center coordinates if they're valid
if abs(orig_center_x) > 1e-6 or abs(orig_center_y) > 1e-6:
logger.info(f"Overriding suspicious position with original center")
final_x = orig_center_x - element_width / 2
final_y = orig_center_y - element_height / 2
else:
# Try to extract position from element attributes
extracted_x, extracted_y = extract_position_from_attributes(element, svg_type)
if extracted_x is not None and extracted_y is not None:
logger.info(f"Overriding suspicious position with attribute-based position")
final_x = extracted_x
final_y = extracted_y
# Apply offsets from mapping
x_offset = mapping_to_use.get('x_offset', 0) if mapping_to_use else 0
y_offset = mapping_to_use.get('y_offset', 0) if mapping_to_use else 0
if x_offset != 0 or y_offset != 0:
final_x += x_offset
final_y += y_offset
if debug:
logger.debug(f"Applied offsets: x={x_offset}, y={y_offset} -> ({final_x}, {final_y})")
# --- Suffix Handling (Rotation Override) ---
suffix = None
suffix_source = None
# First check group suffix since it should take precedence
if group_suffix:
suffix = group_suffix
suffix_source = "group"
if debug:
logger.debug(f"Using group suffix '{suffix}' for element {element_name}")
# Only check element suffix if no group suffix exists
elif element_name and len(element_name) >= 2:
last_char = element_name[-1].lower()
if last_char in ['r', 'd', 'l', 'u']:
suffix = last_char
suffix_source = "element"
if debug:
logger.debug(f"Using element's own suffix '{suffix}'")
# Apply rotation override if a suffix was found
if suffix:
original_rotation = rotation_angle
rotation_map = {'r': 0, 'd': 90, 'l': 180, 'u': 270}
rotation_angle = rotation_map.get(suffix, rotation_angle)
if debug:
logger.debug(f"SUFFIX ROTATION OVERRIDE ({suffix_source}): '{suffix}' "
f"changed {original_rotation:.1f}deg to {rotation_angle}deg")
if debug:
logger.debug(f"Final state: Pos=({final_x:.2f}, {final_y:.2f}), "
f"Size=({element_width:.2f}x{element_height:.2f}), "
f"Rot={rotation_angle:.1f}deg, Prefix='{label_prefix}', Suffix={suffix}")
# --- Name Cleaning ---
cleaned_name = svg_utils.clean_element_name(
element_name,
label_prefix if has_prefix_mapping else None,
suffix,
has_prefix_mapping,
mapping_to_use
)
if debug and cleaned_name != element_name:
logger.debug(f"Cleaned name: '{element_name}' -> '{cleaned_name}'")
element_name = cleaned_name
# --- JSON Construction ---
return json_builder.create_element_json(
element_name=element_name,
element_id=element_id,
element_label=element_label,
element_count=element_count,
x=final_x,
y=final_y,
svg_type=svg_type,
label_prefix=label_prefix,
rotation_angle=rotation_angle,
element_width=element_width,
element_height=element_height,
x_offset=x_offset,
y_offset=y_offset,
original_name=original_name,
debug_buffer=debug_buffer,
has_prefix_mapping=has_prefix_mapping,
custom_options=custom_options,
debug=debug
)
except Exception as e:
logger.error(f"Failed processing {svg_type} #{element_count}: {str(e)}")
import traceback
traceback.print_exc()
return svg_utils.create_default_element(element_count, svg_type, str(e))
# Placeholder for process_element function (to be moved next)
# def process_element(...):
# pass