#!/usr/bin/env python3 ''' svgtool — tiny SVG recolor + preview helper for fast logo tweaking. Rendering goes through headless firefox so CSS (masks, `prefers-color-scheme`, webfonts) is honored exactly like the real docs. Subcommands ----------- colors SVG list the distinct fill/stroke/stop colors in an svg. recolor SVG OUT old=new [old=new ...] literal-replace color tokens, write OUT (great for swapping `.stN{fill:#aaa}` style values or hex codes). preview SVG OUT.png [--bg C] [--w N] [--scheme light|dark] render an svg alone on a bg colour (default white, or #1a1a1a under --scheme dark) to eyeball transparency + shape. --scheme forces `prefers-color-scheme`. page URL_or_path OUT.png [--w N --h N] [--scheme light|dark] screenshot a *built* html page (e.g. docs/_build/html/ index.html). --scheme dark|light forces the page's color-scheme so a pydata `data-theme=auto` site renders its dark/light variant — the way to verify theme-adaptive logos without clicking the toggle. Examples -------- svgtool.py colors docs/_static/tractor_logo_side.svg svgtool.py recolor in.svg out.svg '#FCFCFB=none' '#0A0A0A=#fff' svgtool.py preview out.svg /tmp/p.png --bg '#ff00ff' --w 600 svgtool.py page docs/_build/html/index.html /tmp/dark.png \ --w 1300 --h 900 --scheme dark ''' from __future__ import annotations import argparse from pathlib import Path import re import subprocess as sp import tempfile FF: str = 'firefox' _color_re = re.compile( r'(?:fill|stroke|stop-color)\s*[:=]\s*"?' r'(#[0-9a-fA-F]{3,8}|none|transparent|currentColor)' ) def _colors(text: str) -> list[str]: return sorted(set(_color_re.findall(text))) def _profile(scheme: str|None) -> str: ''' Spin up a throwaway firefox profile, optionally forcing `prefers-color-scheme` via the content-override pref (0 = dark, 1 = light). ''' d: str = tempfile.mkdtemp(prefix='svgtool-ff-') if scheme: val: int = {'dark': 0, 'light': 1}[scheme] Path(d, 'user.js').write_text( f'user_pref(' f'"layout.css.prefers-color-scheme.content-override",' f' {val});\n' ) return d def _shot( url: str, out: str, w: int, h: int, scheme: str|None, ) -> None: prof: str = _profile(scheme) sp.run( [ FF, '--headless', '--no-remote', '-profile', prof, '--screenshot', str(out), f'--window-size={w},{h}', url, ], stdout=sp.DEVNULL, stderr=sp.DEVNULL, timeout=90, ) print(out) def cmd_colors(a: argparse.Namespace) -> None: print('\n'.join(_colors(Path(a.svg).read_text()))) def cmd_recolor(a: argparse.Namespace) -> None: text: str = Path(a.svg).read_text() for pair in a.pairs: old, new = pair.split('=', 1) if old not in text: print(f' ! {old!r} not found (skipped)') continue text = text.replace(old, new) Path(a.out).write_text(text) print(f'wrote {a.out}') def cmd_preview(a: argparse.Namespace) -> None: bg: str = a.bg or ( '#1a1a1a' if a.scheme == 'dark' else '#ffffff' ) src: str = Path(a.svg).resolve().as_uri() html = Path(tempfile.mktemp(suffix='.html')) html.write_text( f'
' f'