#!/usr/bin/env python3 """ Combine two SVG files vertically (top over bottom) with centering and spacing. Useful for combining separated logo elements for 3D printing. """ import argparse import xml.etree.ElementTree as ET from pathlib import Path def load_svg(svg_path: Path): """Load SVG and extract its dimensions and content""" tree = ET.parse(svg_path) root = tree.getroot() # Get dimensions from viewBox or width/height attributes viewbox = root.get('viewBox') if viewbox: parts = viewbox.split() x, y, width, height = map(float, parts) else: width = float(root.get('width', 0)) height = float(root.get('height', 0)) x, y = 0, 0 return { 'root': root, 'width': width, 'height': height, 'viewbox_x': x, 'viewbox_y': y } def combine_svgs_vertical(top_svg_path: Path, bottom_svg_path: Path, output_path: Path, spacing: float = 20, margin: float = 10): """ Combine two SVGs vertically with the top one centered above the bottom one. Args: top_svg_path: Path to top SVG (e.g., icon) bottom_svg_path: Path to bottom SVG (e.g., text) output_path: Path for output combined SVG spacing: Vertical space between top and bottom elements (default: 20) margin: Margin around the entire composition (default: 10) """ # Load both SVGs top_svg = load_svg(top_svg_path) bottom_svg = load_svg(bottom_svg_path) # Calculate combined dimensions max_width = max(top_svg['width'], bottom_svg['width']) combined_height = top_svg['height'] + spacing + bottom_svg['height'] # Add margins canvas_width = max_width + (2 * margin) canvas_height = combined_height + (2 * margin) # Calculate centering offsets top_x_offset = margin + (max_width - top_svg['width']) / 2 top_y_offset = margin bottom_x_offset = margin + (max_width - bottom_svg['width']) / 2 bottom_y_offset = margin + top_svg['height'] + spacing # Create new SVG root combined = ET.Element('svg', { 'xmlns': 'http://www.w3.org/2000/svg', 'width': str(canvas_width), 'height': str(canvas_height), 'viewBox': f'0 0 {canvas_width} {canvas_height}' }) # Add comment for clarity comment = ET.Comment(f' Combined: {top_svg_path.name} (top) + {bottom_svg_path.name} (bottom) ') combined.append(comment) # Create group for top SVG with transform top_group = ET.SubElement(combined, 'g', { 'id': 'top', 'transform': f'translate({top_x_offset}, {top_y_offset})' }) # Copy all elements from top SVG for child in top_svg['root']: if child.tag != '{http://www.w3.org/2000/svg}metadata': top_group.append(child) # Create group for bottom SVG with transform bottom_group = ET.SubElement(combined, 'g', { 'id': 'bottom', 'transform': f'translate({bottom_x_offset}, {bottom_y_offset})' }) # Copy all elements from bottom SVG for child in bottom_svg['root']: if child.tag != '{http://www.w3.org/2000/svg}metadata': bottom_group.append(child) # Write output tree = ET.ElementTree(combined) ET.indent(tree, space=' ') tree.write(output_path, encoding='utf-8', xml_declaration=True) print(f"Created {output_path}") print(f" Top ({top_svg_path.name}): {top_svg['width']:.0f}x{top_svg['height']:.0f} at ({top_x_offset:.1f}, {top_y_offset:.1f})") print(f" Bottom ({bottom_svg_path.name}): {bottom_svg['width']:.0f}x{bottom_svg['height']:.0f} at ({bottom_x_offset:.1f}, {bottom_y_offset:.1f})") print(f" Canvas: {canvas_width:.0f}x{canvas_height:.0f}") print(f" Spacing: {spacing:.0f}, Margin: {margin:.0f}") def main(): parser = argparse.ArgumentParser( description='Combine two SVG files vertically with centering and spacing' ) parser.add_argument('top', help='Top SVG file (e.g., icon)') parser.add_argument('bottom', help='Bottom SVG file (e.g., text)') parser.add_argument('output', help='Output SVG file') parser.add_argument('--spacing', '-s', type=float, default=20, help='Vertical spacing between elements (default: 20)') parser.add_argument('--margin', '-m', type=float, default=10, help='Margin around entire composition (default: 10)') args = parser.parse_args() top_path = Path(args.top) bottom_path = Path(args.bottom) output_path = Path(args.output) if not top_path.exists(): print(f"ERROR: Top SVG file not found: {top_path}") return 1 if not bottom_path.exists(): print(f"ERROR: Bottom SVG file not found: {bottom_path}") return 1 combine_svgs_vertical(top_path, bottom_path, output_path, args.spacing, args.margin) return 0 if __name__ == '__main__': exit(main())