initial
This commit is contained in:
212
svg_processor.py
Executable file
212
svg_processor.py
Executable 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()
|
||||
Reference in New Issue
Block a user