Created
May 29, 2026 17:21
-
-
Save jerlendds/00f9a32e3eb3e40a9c0bc567ed4231cb to your computer and use it in GitHub Desktop.
Trace all jpg images in current directoy to SVGs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| from pathlib import Path | |
| from PIL import Image | |
| import subprocess | |
| import tempfile | |
| import shutil | |
| import sys | |
| THRESHOLD = 0.45 # Inkscape brightness cutoff | |
| SPECKLES = 2 # Potrace --turdsize | |
| SMOOTH_CORNERS = 1 # Potrace --alphamax | |
| OPTIMIZE = 0.2 # Potrace --opttolerance | |
| def require_cmd(cmd: str) -> None: | |
| if shutil.which(cmd) is None: | |
| raise RuntimeError(f"Required command not found: {cmd}") | |
| def jpg_to_pbm(jpg_path: Path, pbm_path: Path) -> None: | |
| """ | |
| Convert JPG to 1-bit PBM. | |
| Potrace traces black pixels. This maps pixels darker than the | |
| brightness cutoff to black, and all others to white. | |
| """ | |
| img = Image.open(jpg_path).convert("L") | |
| cutoff = int(THRESHOLD * 255) | |
| bw = img.point(lambda p: 0 if p < cutoff else 255, mode="1") | |
| bw.save(pbm_path) | |
| def trace_with_potrace(pbm_path: Path, svg_path: Path) -> None: | |
| subprocess.run( | |
| [ | |
| "potrace", | |
| str(pbm_path), | |
| "--svg", | |
| "--output", | |
| str(svg_path), | |
| "--turdsize", | |
| str(SPECKLES), | |
| "--alphamax", | |
| str(SMOOTH_CORNERS), | |
| "--opttolerance", | |
| str(OPTIMIZE), | |
| ], | |
| check=True, | |
| ) | |
| def fit_canvas_with_inkscape(svg_path: Path) -> None: | |
| """ | |
| Optional: fit the SVG page to the traced vector drawing. | |
| Requires Inkscape 1.x. If you do not need Inkscape post-processing, | |
| you can remove this function and its call. | |
| """ | |
| if shutil.which("inkscape") is None: | |
| return | |
| subprocess.run( | |
| [ | |
| "inkscape", | |
| str(svg_path), | |
| "--batch-process", | |
| "--actions", | |
| ( | |
| "select-all;" | |
| "page-fit-to-selection;" | |
| f"export-filename:{svg_path};" | |
| "export-plain-svg;" | |
| "export-overwrite;" | |
| "export-do" | |
| ), | |
| ], | |
| check=True, | |
| ) | |
| def process_file(jpg_path: Path) -> None: | |
| svg_path = jpg_path.with_suffix(".svg") | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| pbm_path = Path(tmpdir) / f"{jpg_path.stem}.pbm" | |
| jpg_to_pbm(jpg_path, pbm_path) | |
| trace_with_potrace(pbm_path, svg_path) | |
| fit_canvas_with_inkscape(svg_path) | |
| print(f"{jpg_path.name} -> {svg_path.name}") | |
| def main() -> int: | |
| require_cmd("potrace") | |
| jpgs = sorted(Path.cwd().glob("*.jpg")) + sorted(Path.cwd().glob("*.jpeg")) | |
| if not jpgs: | |
| print("No .jpg or .jpeg files found in the current directory.") | |
| return 0 | |
| for jpg_path in jpgs: | |
| try: | |
| process_file(jpg_path) | |
| except Exception as exc: | |
| print(f"ERROR processing {jpg_path}: {exc}", file=sys.stderr) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment