"""Regenerate every brand artefact from app/branding.py. Run after changing the geometry: uv run python tools/render_brand.py Writes the Jinja partial the app includes, the favicon, and the README banners. `tests/test_branding.py` re-runs this in memory and fails if anything on disk has drifted, so a redraw cannot land in the top bar and miss the favicon. The PNGs need headless Chrome (same dependency as tools/screenshot.py). """ from __future__ import annotations import subprocess import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from app import branding # noqa: E402 from tools.browser import find_chrome # noqa: E402 ROOT = Path(__file__).resolve().parent.parent def partial() -> str: """The markup base.html includes — mark plus word, one source.""" return ( "{# GENERATED by tools/render_brand.py from app/branding.py — do not\n" " edit. The wordmark is drawn paths because the app ships no webfont\n" " and its CSP blocks remote ones. #}\n" f"{branding.mark_svg()}\n{branding.wordmark_svg()}\n" ) def _standalone(inner: str, viewbox: str) -> str: """A file that renders outside the page, so it carries its own colours.""" return ( f'\n' " \n" f" {inner}\n\n" ) def icon_svg() -> str: rects = "".join( f'' for x, y, accent in branding.MARK_RECTS ) return _standalone(rects, "0 0 24 24") def banner_html(dark: bool) -> str: """A README banner: mark and word side by side, on a flat ground.""" ink = branding.INK_DARK if dark else branding.INK_LIGHT acc = branding.ACCENT_DARK if dark else branding.ACCENT_LIGHT bg = "#0d1018" if dark else "#eef0f7" return f"""{branding.mark_svg()}{branding.wordmark_svg()}""" def _chrome() -> str: found = find_chrome() if not found: raise SystemExit("headless Chrome not found — PNGs not regenerated") return found def _shoot(html: str, out: Path, width: int, height: int) -> None: with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as fh: fh.write(html) tmp = fh.name subprocess.run([_chrome(), "--headless", "--disable-gpu", "--hide-scrollbars", f"--window-size={width},{height}", "--virtual-time-budget=2000", f"--screenshot={out}", f"file://{tmp}"], check=True, capture_output=True) def main() -> None: (ROOT / "app/templates/_brand.html").write_text(partial(), encoding="utf-8") (ROOT / "app/static/icon.svg").write_text(icon_svg(), encoding="utf-8") square = ('' '' + "".join(f'" for x, y, accent in branding.MARK_RECTS) + "") _shoot(square, ROOT / "docs/assets/icon.png", 512, 512) (ROOT / "app/static/icon.png").write_bytes((ROOT / "docs/assets/icon.png").read_bytes()) for dark in (False, True): name = f"wordmark-{'dark' if dark else 'light'}.png" _shoot(banner_html(dark), ROOT / "docs/assets" / name, 1200, 260) print("regenerated: _brand.html, icon.svg, icon.png, docs/assets/{icon,wordmark-*}.png") if __name__ == "__main__": main()