commit c202acc82311d5ad8111657384b536f34e39258fa694b856eaae469d450c1e51 Author: 51m0n Date: Mon Mar 30 00:26:18 2026 +0000 initial diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100755 index 0000000..da08b1f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +{ + "name": "Existing Dockerfile", + "build": { + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerfile": "../Dockerfile" + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..977acd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.jpg +*.png +*_optimized.svg +*.pyc diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..26dc9a7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File with Arguments", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": ["3T_logo_master.svg", "configs/3t_full.json", "dbgout"] + } + ] +} \ No newline at end of file diff --git a/3T_logo_master.svg b/3T_logo_master.svg new file mode 100644 index 0000000..c743b10 --- /dev/null +++ b/3T_logo_master.svg @@ -0,0 +1,327 @@ + + + + + + + 3t.network logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3t + + .network + + + + 3t.network logo + + + + diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..7228dd3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python + +# Set working directory +WORKDIR /app + +COPY requirements.txt /app/ + +# Install Python packages +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the script +COPY svg_processor.py /app/ + +# TODO, actually make this do the generation + +# Default command (can be overridden) +CMD ["python", "svg_processor.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..221b02d --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# SVG Processor + +This Python script processes an SVG file by removing specified elements, embedding fonts, and exporting to multiple formats. + +## Features + +- Remove SVG elements based on IDs specified in a JSON file +- Embed fonts from external files into the SVG as base64 +- Optimize the SVG using scour +- Export to optimized SVG, PNG, JPEG, and thumbnail JPEG + +## Requirements + +- Python 3.9+ +- Libraries: lxml, cairosvg, scour + +## Docker Usage + +Build the Docker image: + +```bash +docker build -t svg-processor . +``` + +Run the container: + +```bash +docker run -v /path/to/input:/input -v /path/to/output:/output svg-processor python svg_processor.py /input/input.svg /input/config.json /output +``` + +Replace `/path/to/input` and `/path/to/output` with actual paths. + +## JSON Configuration Format + +See `sample_config.json` for an example. + +- `remove`: Array of element IDs to remove +- `fonts`: Object mapping font-family names to font file paths +- `size`: Override the size - useful when the size of the new image cannot be automatically calulated + +## Output + +- `optimized.svg`: Optimized SVG with embedded fonts +- `output.png`: PNG version +- `output.jpg`: JPEG version +- `thumbnail.jpg`: Thumbnail JPEG (128x128 max) \ No newline at end of file diff --git a/Tienne-Regular.ttf b/Tienne-Regular.ttf new file mode 100755 index 0000000..a125d19 Binary files /dev/null and b/Tienne-Regular.ttf differ diff --git a/configs/3t_angry.json b/configs/3t_angry.json new file mode 100644 index 0000000..94f2e84 --- /dev/null +++ b/configs/3t_angry.json @@ -0,0 +1,7 @@ +{ + "file_name":"3t_full_angry", + "remove": [], + "fonts": { + "tienne": "Tienne-Regular.ttf" + } +} \ No newline at end of file diff --git a/configs/3t_full.json b/configs/3t_full.json new file mode 100755 index 0000000..72476c6 --- /dev/null +++ b/configs/3t_full.json @@ -0,0 +1,8 @@ +{ + "file_name":"3t_full", + "remove": [ + "Angry eye brows"], + "fonts": { + "tienne": "Tienne-Regular.ttf" + } +} \ No newline at end of file diff --git a/configs/3t_logo_green.json b/configs/3t_logo_green.json new file mode 100644 index 0000000..58ec703 --- /dev/null +++ b/configs/3t_logo_green.json @@ -0,0 +1,19 @@ +{ + "file_name": "3t_logo_green", + "remove": [ + "Angry eye brows", + ".network", + "background", + "3t", + "background_purple" + ], + "fonts": { + "tienne": "Tienne-Regular.ttf" + }, + "size":{ + "vx":"165", + "vy":"88", + "x":"1936.4955", + "y":"1842.5645" + } +} \ No newline at end of file diff --git a/configs/3t_logo_purple.json b/configs/3t_logo_purple.json new file mode 100644 index 0000000..b8c1624 --- /dev/null +++ b/configs/3t_logo_purple.json @@ -0,0 +1,19 @@ +{ + "file_name": "3t_logo_purple", + "remove": [ + "Angry eye brows", + ".network", + "background", + "3t", + "background_green" + ], + "fonts": { + "tienne": "Tienne-Regular.ttf" + }, + "size":{ + "vx":"165", + "vy":"88", + "x":"1936.4955", + "y":"1842.5645" + } +} \ No newline at end of file diff --git a/configs/3t_mid.json b/configs/3t_mid.json new file mode 100755 index 0000000..3df371a --- /dev/null +++ b/configs/3t_mid.json @@ -0,0 +1,11 @@ +{ + "file_name": "3t_mid", + "remove": [ + "Angry eye brows", + ".network", + "full_background" + ], + "fonts": { + "tienne": "Tienne-Regular.ttf" + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..73adf15 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +lxml +cairosvg +scour \ No newline at end of file diff --git a/svg_processor.py b/svg_processor.py new file mode 100755 index 0000000..327e13c --- /dev/null +++ b/svg_processor.py @@ -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() \ No newline at end of file