"""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"
)
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 = (''
'")
_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()