Skip to content

Instantly share code, notes, and snippets.

@jerlendds
Created May 29, 2026 17:21
Show Gist options
  • Select an option

  • Save jerlendds/00f9a32e3eb3e40a9c0bc567ed4231cb to your computer and use it in GitHub Desktop.

Select an option

Save jerlendds/00f9a32e3eb3e40a9c0bc567ed4231cb to your computer and use it in GitHub Desktop.
Trace all jpg images in current directoy to SVGs
#!/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