This commit is contained in:
51m0n
2026-03-30 00:26:18 +00:00
commit c202acc823
14 changed files with 715 additions and 0 deletions

212
svg_processor.py Executable file
View File

@@ -0,0 +1,212 @@
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()