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