Ctrl K
ComunidadGuías…

tools/render_brand.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{viewbox}" '
        'role="img" aria-label="Stocktake">\n'
        "  <style>\n"
        f"    .c {{ fill: {branding.INK_LIGHT}; stroke: none; }}\n"
        f"    .s {{ fill: none; stroke: {branding.INK_LIGHT}; }}\n"
        f"    .top {{ fill: none; stroke: {branding.ACCENT_LIGHT}; }}\n"
        "    @media (prefers-color-scheme: dark) {\n"
        f"      .c {{ fill: {branding.INK_DARK}; }}\n"
        f"      .s {{ stroke: {branding.INK_DARK}; }}\n"
        f"      .top {{ stroke: {branding.ACCENT_DARK}; }}\n"
        "    }\n"
        "  </style>\n"
        f"  {inner}\n</svg>\n"
    )


def icon_svg() -> str:
    rects = "".join(
        f'<rect class="{"top" if accent else "s"}" x="{x}" y="{y}" '
        f'width="5.5" height="5.5" rx="1" stroke-width="2" stroke-linejoin="round"/>'
        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"""<!doctype html><html><head><meta charset="utf-8"><style>
      html,body{{margin:0;width:1200px;height:260px;background:{bg};
        display:flex;align-items:center;justify-content:center;gap:28px}}
      svg{{display:block}}
      .brandmark{{width:104px;height:104px;fill:none;stroke:{ink};stroke-width:2;
        stroke-linecap:round;stroke-linejoin:round}}
      .brandmark .top{{stroke:{acc}}}
      .brandword{{height:80px;width:auto;fill:{ink};color:{ink}}}
    </style></head><body>{branding.mark_svg()}{branding.wordmark_svg()}</body></html>"""


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 = ('<!doctype html><html><head><meta charset="utf-8"><style>'
              'html,body{margin:0;width:512px;height:512px;background:#eef0f7}'
              'svg{display:block;width:512px;height:512px}</style></head><body>'
              '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 28 28" fill="none" '
              f'stroke="{branding.INK_LIGHT}" stroke-width="2" stroke-linejoin="round" '
              'stroke-linecap="round">'
              + "".join(f'<rect x="{x}" y="{y}" width="5.5" height="5.5" rx="1"'
                        + (f' stroke="{branding.ACCENT_LIGHT}"' if accent else "") + "/>"
                        for x, y, accent in branding.MARK_RECTS)
              + "</svg></body></html>")
    _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()