import argparse import json import base64 import os from pathlib import Path import re import shutil from lxml import etree import cairosvg import scour.scour from PIL import Image def convert_to_pixels(value, dpi=96): """Convert SVG length values to pixels""" if value is None: return 0 value = str(value).strip() # Handle different units match = re.match(r'^([\d.]+)(in|cm|mm|pt|px)?$', value) if not match: return float(value) if value else 0 num, unit = match.groups() num = float(num) if unit == 'in': return num * dpi elif unit == 'cm': return num * dpi / 2.54 elif unit == 'mm': return num * dpi / 25.4 elif unit == 'pt': return num * dpi / 72 else: # px or no unit return num def get_bounding_box(svg_tree): """Calculate bounds of all visible elements""" max_x = float('-inf') max_y = float('-inf') for element in svg_tree.xpath("//*"): # Skip non-visual elements tag_name = etree.QName(element).localname if tag_name in ('defs', 'style', 'metadata', "svg", "page"): continue # Get position/size attributes - simplified approach x = float(element.get('x', 0)) y = float(element.get('y', 0)) # need to detect transform in parent transformx = 0 transformy = 0 parent = element.getparent() transform_parent = parent.get('transform', "") if transform_parent and "translate" in transform_parent: t = transform_parent.removeprefix("translate").replace("(","").replace(")","") transformx_str, transformy_str = transform_parent.removeprefix("translate").replace("(","").replace(")","").split(",") transformx = float(transformx_str) transformy = float(transformy_str) transform_element = element.get('transform', "") if transform_element and "translate" in transform_element: transformx_str, transformy_str = transform_element.removeprefix("translate").replace("(","").replace(")","").split(",") transformx = transformx + float(transformx_str) transformy = transformy + float(transformy_str) width = float(convert_to_pixels(element.get('width', 0))) height = float(convert_to_pixels(element.get('height', 0))) x = x + transformx y = y + transformy max_x = max(max_x, x + width) max_y = max(max_y, y + height) if max_x == float('inf'): # no elements found return 100, 100 return max_x , max_y def update_size(svg_tree, size_dict): max_x = 0 max_y = 0 vx = 0 vy = 0 if size_dict: max_x = size_dict.get("x",0) max_y = size_dict.get("y",0) vx = size_dict.get("vx",0) vy = size_dict.get("vy",0) if max_x ==0 or max_y == 0: max_x , max_y = get_bounding_box(svg_tree) svg_root = svg_tree.getroot() svg_root.set('viewBox', f"{vx} {vy} {max_x} {max_y}") svg_root.set('width', f"{max_x}") svg_root.set('height', f"{max_y}") def remove_elements(svg_tree, remove_ids): """Remove elements with specified IDs from the SVG tree.""" namespaces = { 'inkscape': 'http://www.inkscape.org/namespaces/inkscape' } for element_id in remove_ids: element = svg_tree.find(f".//*[@id='{element_id}']") if element is not None: element.getparent().remove(element) element = svg_tree.find(f".//*[@inkscape:label='{element_id}']", namespaces=namespaces) if element is not None: element.getparent().remove(element) def embed_fonts(svg_tree, fonts_dict): """Embed fonts specified in fonts_dict into the SVG.""" defs = svg_tree.find(".//{http://www.w3.org/2000/svg}defs") if defs is None: defs = etree.SubElement(svg_tree.getroot(), "{http://www.w3.org/2000/svg}defs") style = etree.SubElement(defs, "{http://www.w3.org/2000/svg}style") style.text = "" for font_family, font_path in fonts_dict.items(): if os.path.exists(font_path): with open(font_path, 'rb') as f: font_data = base64.b64encode(f.read()).decode('utf-8') mime_type = "font/ttf" # Assume TTF, adjust if needed data_url = f"data:{mime_type};base64,{font_data}" style.text += f"@font-face {{ font-family: '{font_family}'; src: url('{data_url}'); }}\n" if not os.path.exists(f"/usr/local/share/fonts/{Path(font_path).name}"): shutil.copy2(font_path, "/usr/local/share/fonts",) def optimize_svg(svg_tree): """Optimize the SVG using scour.""" svg_string = etree.tostring(svg_tree, encoding='unicode') options = scour.scour.sanitizeOptions() #options.enable_viewboxing = True options.enable_id_stripping = True options.enable_comment_stripping = True options.shorten_ids = True options.disable_style_to_xml = True options.indent_type = None optimized_svg = scour.scour.scourString(svg_string, options) return optimized_svg def main(): parser = argparse.ArgumentParser(description="Process SVG file: remove elements, embed fonts, export optimized formats.") parser.add_argument("input_svg", help="Path to input SVG file") parser.add_argument("json_file", help="Path to JSON file with removal and font info") parser.add_argument("output_dir", help="Output directory for generated files") args = parser.parse_args() # Load JSON with open(args.json_file, 'r') as f: config = json.load(f) remove_ids = config.get("remove", []) fonts_dict = config.get("fonts", {}) size_dict = config.get("size", {}) # Parse SVG svg_tree = etree.parse(args.input_svg) # Remove elements remove_elements(svg_tree, remove_ids) # Embed fonts embed_fonts(svg_tree, fonts_dict) update_size(svg_tree, size_dict) # Optimize SVG optimized_svg = optimize_svg(svg_tree) # Ensure output directory exists os.makedirs(args.output_dir, exist_ok=True) prefix = "" if config.get("file_name"): prefix = f"{config.get('file_name')}_" # Write optimized SVG svg_output = os.path.join(args.output_dir, f"{prefix}optimized.svg") with open(svg_output, 'w') as f: f.write(optimized_svg) # Convert to PNG png_output = os.path.join(args.output_dir, f"{prefix}output.png") cairosvg.svg2png(bytestring=optimized_svg.encode('utf-8'), write_to=png_output, unsafe=True) # Convert to JPEG jpeg_output = os.path.join(args.output_dir, f"{prefix}output.jpg") # Cairosvg doesn't directly support JPEG, so use the PNG to JPEG img = Image.open(jpeg_output.replace('.jpg', '.png')) img.convert("RGB").save(jpeg_output, 'JPEG') # Create thumbnail JPEG thumbnail_output = os.path.join(args.output_dir, f"{prefix}thumbnail.jpg") img.thumbnail((128, 128)) # Example size, adjust as needed img.convert("RGB").save(thumbnail_output, 'JPEG') if __name__ == "__main__": main()