441 lines
16 KiB
Python
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 |