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

441 lines
16 KiB
Python

import xml.etree.ElementTree as ET
import logging
import sys
from xml.dom import minidom
# Import modules from the package
from . import svg_math
from . import svg_utils
from . import svg_parser
from . import element_processor
# Logging configuration
logger = logging.getLogger('svg_transformer')
def configure_logging(debug=False):
"""Configure logging for the SVG transformer."""
level = logging.DEBUG if debug else logging.INFO
logger.setLevel(level)
# Clear existing handlers to avoid duplicates
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Setup handlers for both stdout and stderr
stdout_handler = logging.StreamHandler(sys.stdout)
stderr_handler = logging.StreamHandler()
formatter = logging.Formatter('%(levelname)s: %(message)s')
stdout_handler.setFormatter(formatter)
stderr_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)
return logger
def log_message(message, level=logging.INFO):
"""Log a message to both the logger and stdout."""
logger.log(level, message)
# Print directly to stdout and flush for UI feedback
print(message)
sys.stdout.flush()
class SVGTransformer:
"""Class to handle SVG parsing and transformation of SVG elements."""
# SVG element types supported for processing
ELEMENT_TYPES = [
('rect', 'rectangles'),
('circle', 'circles'),
('ellipse', 'ellipses'),
('line', 'lines'),
('polyline', 'polylines'),
('polygon', 'polygons'),
('path', 'paths')
]
def __init__(self, svg_path, custom_options=None, debug=False):
"""Initialize with the path to the SVG file and optional custom options.
Args:
svg_path (str): Path to the SVG file.
custom_options (dict, optional): Custom processing options.
debug (bool, optional): Enable debug logging.
Raises:
ValueError: If the SVG file cannot be loaded or parsed.
"""
self.svg_path = svg_path
self.custom_options = custom_options or {}
self.debug = debug
# Configure logging
configure_logging(debug)
# Load the SVG document
self.doc, self.svg_element = svg_parser.load_svg(svg_path)
# Handle parsing failures
if not self.doc or not self.svg_element:
raise ValueError(f"Failed to load or parse SVG: {svg_path}")
# Log configuration details when in debug mode
if self.debug:
self._log_configuration()
def _log_configuration(self):
"""Log configuration details for debugging."""
log_message("DEBUG: Loaded custom_options:", logging.DEBUG)
if 'element_mappings' in self.custom_options:
mappings = self.custom_options['element_mappings']
log_message(f"DEBUG: Found {len(mappings)} element mappings", logging.DEBUG)
for i, mapping in enumerate(mappings):
log_message(
f"DEBUG: Mapping #{i+1}: "
f"svg_type='{mapping.get('svg_type')}', "
f"label_prefix='{mapping.get('label_prefix')}', "
f"props_path='{mapping.get('props_path')}'",
logging.DEBUG
)
else:
log_message("DEBUG: No element_mappings found in custom_options", logging.DEBUG)
def get_svg_dimensions(self):
"""Get the dimensions of the SVG document.
Returns:
tuple: The width and height of the SVG.
"""
return svg_parser.get_svg_dimensions(self.svg_element)
def process_svg(self, center_mode='bbox'):
"""Process SVG file and extract elements with calculated centers.
Args:
center_mode (str): How to calculate centers ('bbox' or 'centroid').
Returns:
list: Processed elements in JSON format.
"""
results = []
svg_dims = self.get_svg_dimensions()
# Process statistics
stats = {
'total_elements': 0,
'processed_elements': 0,
'group_count': 0
}
# Process top-level elements
self._process_top_level_elements(results, svg_dims, center_mode, stats)
# Process groups
self._process_groups(results, svg_dims, center_mode, stats)
# Log final processing statistics
final_msg = (
f"Total: Processed {stats['total_elements']} elements "
f"({stats['group_count']} top-level groups), "
f"converted {stats['processed_elements']}"
)
log_message(final_msg)
return results
def _process_top_level_elements(self, results, svg_dims, center_mode, stats):
"""Process all top-level SVG elements (direct children of the root svg).
Args:
results (list): List to append processed elements to.
svg_dims (tuple): SVG dimensions as (width, height).
center_mode (str): Center calculation mode.
stats (dict): Statistics dictionary to update.
"""
for svg_type, plural in self.ELEMENT_TYPES:
elements = self.doc.getElementsByTagName(svg_type)
count = 0
successful_conversions = 0
for element in elements:
# Process only direct children of the root SVG element
if element.parentNode == self.svg_element:
count += 1
stats['total_elements'] += 1
# Process the element
element_json = element_processor.process_element(
element, count, svg_type, svg_dims,
self.custom_options, self.debug,
center_mode=center_mode
)
# Store successfully processed elements
if element_json:
results.append(element_json)
stats['processed_elements'] += 1
successful_conversions += 1
# Log results for this element type if any were found
if count > 0:
msg = f"Processed {count} top-level {plural}, converted {successful_conversions}"
log_message(msg)
def _process_groups(self, results, svg_dims, center_mode, stats):
"""Process all group elements in the SVG.
Args:
results (list): List to append processed elements to.
svg_dims (tuple): SVG dimensions as (width, height).
center_mode (str): Center calculation mode.
stats (dict): Statistics dictionary to update.
"""
groups = self.doc.getElementsByTagName('g')
for group in groups:
# Process only top-level groups
if group.parentNode == self.svg_element:
stats['group_count'] += 1
group_data = self._process_group(
group,
stats['group_count'],
svg_dims,
center_mode
)
if group_data:
# Add group elements to results
results.extend(group_data['elements'])
stats['total_elements'] += group_data['count']
stats['processed_elements'] += group_data['converted']
# Log group processing results
log_message(
f"Processed group #{stats['group_count']} ({group_data['id']}): "
f"{group_data['count']} elements, converted {group_data['converted']}"
)
def _process_group(self, group, group_index, svg_dims, center_mode='bbox'):
"""Process a group element and all its children.
Args:
group (minidom.Element): The group element.
group_index (int): Index of this group.
svg_dims (tuple): SVG dimensions as (width, height).
center_mode (str): Center calculation mode.
Returns:
dict: Contains processed elements and statistics.
"""
# Get group attributes and context
group_id = group.getAttribute('id') or f"group{group_index}"
group_context = self._extract_group_context(group, group_index)
# Log group processing start if in debug mode
if self.debug:
self._log_group_processing_start(group_index, group_id, group_context)
# Initialize processing statistics
results = {
'elements': [],
'id': group_id,
'count': 0,
'converted': 0
}
# Track element counts by type within this group
element_types = [t[0] for t in self.ELEMENT_TYPES]
element_count_by_type = {t: 0 for t in element_types}
nested_group_count = 0
# Process all child nodes in the group
for child in group.childNodes:
if child.nodeType != child.ELEMENT_NODE:
continue
svg_type = child.tagName
# Process SVG element
if svg_type in element_types:
results['count'] += 1
element_count_by_type[svg_type] += 1
# Process this element
element_result = self._process_group_element(
child,
svg_type,
element_count_by_type[svg_type],
svg_dims,
group_context,
center_mode
)
if element_result:
results['elements'].append(element_result)
results['converted'] += 1
# Process nested group with recursive call
elif svg_type == 'g':
nested_group_result = self._process_nested_group(
child,
group_index,
nested_group_count + 1,
svg_dims,
center_mode
)
if nested_group_result:
results['elements'].extend(nested_group_result['elements'])
results['count'] += nested_group_result['count']
results['converted'] += nested_group_result['converted']
nested_group_count += 1
return results
def _extract_group_context(self, group, group_index):
"""Extract prefix and suffix information from group attributes.
Args:
group (minidom.Element): The group element.
group_index (int): Index of this group.
Returns:
dict: Group context including prefix and suffix.
"""
group_label = group.getAttribute('inkscape:label') or ""
context = {
'prefix': None,
'suffix': None
}
# Extract prefix from label (before underscore or whole label)
if group_label and "_" in group_label:
context['prefix'] = group_label.split("_")[0]
elif group_label:
context['prefix'] = group_label
# Extract suffix (direction indicator as last character)
if group_label and len(group_label) >= 2:
last_char = group_label[-1].lower()
if last_char in ['r', 'd', 'l', 'u']:
context['suffix'] = last_char
return context
def _log_group_processing_start(self, group_index, group_id, context):
"""Log the start of group processing in debug mode.
Args:
group_index (int): Group index.
group_id (str): Group ID.
context (dict): Group context containing prefix and suffix.
"""
if context['prefix']:
log_message(
f"DEBUG: Group #{group_index} using potential prefix: '{context['prefix']}'",
logging.DEBUG
)
if context['suffix']:
log_message(
f"DEBUG: Group #{group_index} has suffix: '{context['suffix']}'",
logging.DEBUG
)
log_message(
f"PROCESSING GROUP #{group_index}: id='{group_id}', "
f"potential_prefix='{context['prefix']}', suffix='{context['suffix']}'",
logging.DEBUG
)
def _process_group_element(self, element, svg_type, type_count, svg_dims, group_context, center_mode):
"""Process a single element within a group.
Args:
element (minidom.Element): The SVG element.
svg_type (str): Type of SVG element.
type_count (int): Count of this element type within the group.
svg_dims (tuple): SVG dimensions.
group_context (dict): Group context containing prefix and suffix.
center_mode (str): Center calculation mode.
Returns:
dict: Processed element data or None if processing failed.
"""
element_label = (
element.getAttribute('inkscape:label') or
element.getAttribute('id') or
f"{svg_type}#{type_count}"
)
if self.debug:
log_message(
f"DEBUG: Processing {svg_type} #{type_count} ('{element_label}')",
logging.DEBUG
)
# Process the element
element_json = element_processor.process_element(
element,
type_count,
svg_type,
svg_dims,
self.custom_options,
self.debug,
group_prefix=group_context['prefix'],
group_suffix=group_context['suffix'],
center_mode=center_mode
)
# Log processing result in debug mode
if self.debug:
if element_json:
log_message(
f"DEBUG: ... Success: {element_json.get('meta',{}).get('name')}",
logging.DEBUG
)
else:
log_message(f"DEBUG: ... Failed: {element_label}", logging.DEBUG)
return element_json
def _process_nested_group(self, nested_group, parent_index, nested_index, svg_dims, center_mode):
"""Process a nested group within another group.
Args:
nested_group (minidom.Element): The nested group element.
parent_index (int): Parent group index.
nested_index (int): Index of this nested group within parent.
svg_dims (tuple): SVG dimensions.
center_mode (str): Center calculation mode.
Returns:
dict: Processed group data.
"""
nested_group_index = f"{parent_index}.{nested_index}"
nested_group_id = nested_group.getAttribute('id') or f"group{nested_group_index}"
if self.debug:
log_message(
f"DEBUG: Processing nested group <{nested_group_id}>",
logging.DEBUG
)
# Process the nested group recursively
nested_data = self._process_group(
nested_group,
nested_group_index,
svg_dims,
center_mode
)
# Log nested group results in debug mode
if self.debug and nested_data:
log_message(
f"DEBUG: Nested group #{nested_group_index} ({nested_data['id']}): "
f"{nested_data['count']} elements, converted {nested_data['converted']}",
logging.DEBUG
)
return nested_data