"""Route (d): code-as-art via SVG, rasterised with ``rsvg-convert``. SVG is emitted as text and rendered by a system binary, so this route adds no Python dependency. It is resolution-independent by construction, which is the property that makes it the natural fit for flat vector/UI work: the same source renders crisply at any target size. Gate consequence worth stating: an SVG rasteriser anti-aliases by default, so output fails ``ALPHA_NOT_BINARY`` unless alpha is explicitly thresholded. That is a real property of the route, not a defect -- it is why flat-vector output needs a finishing step before it can be gated as pixel art. """ from __future__ import annotations import shutil import subprocess from typing import TYPE_CHECKING, Final if TYPE_CHECKING: from pathlib import Path # Same locked palette as the procedural route, so the two cells are comparable. PALETTE: Final[dict[str, str]] = { "outline": "#1a141c", "shadow": "#483c42", "mid": "#7c6c68", "light": "#c6baaa", "gold": "#d6a43e", "red": "#b03e3c", "cyan": "#5cb0be", } STROKE: Final = 'stroke="#1a141c" stroke-width="1.5" stroke-linejoin="round"' def _svg(body: str, size: int = 64) -> str: """Wrap shape markup in an SVG document. Args: body: The inner markup. size: The viewBox edge length. Returns: A complete SVG document. """ return ( f'{body}' ) SHAPES: Final[dict[str, str]] = { "key": ( f'' f'" f'" f'" ), "gem": ( f'" f'' ), "potion": ( f'' f'' f'" ), "coin": ( f'' f'' f'' ), "shield": ( f'' f'' f'' ), } _REGISTRY: dict[str, str] = dict(SHAPES) class RenderError(RuntimeError): """Raised when the SVG rasteriser is unavailable or fails.""" def all_shapes() -> dict[str, str]: """Return every vector subject registered so far. ``vector_shapes`` calls :func:`register` at import time, so the extra seven subjects appear here without this module importing them -- which keeps the dependency one-directional and avoids a circular import. Returns: A mapping of subject name to SVG markup. """ return dict(_REGISTRY) def register(shapes: dict[str, str]) -> None: """Add subjects to the vector registry. Args: shapes: A mapping of subject name to SVG markup. """ _REGISTRY.update(shapes) def render(name: str, out: Path, size: int = 64) -> Path: """Rasterise one vector subject to PNG. Args: name: A key of :data:`SHAPES`. out: Destination PNG path. size: Output edge length in pixels. Returns: The written path. Raises: KeyError: If the subject has no vector shape. RenderError: If ``rsvg-convert`` is missing or exits non-zero. """ shapes = all_shapes() if name not in shapes: msg = f"no vector shape for {name!r}" raise KeyError(msg) binary = shutil.which("rsvg-convert") if binary is None: msg = "rsvg-convert is not installed" raise RenderError(msg) source = out.with_suffix(".svg") source.write_text(_svg(shapes[name], size), encoding="utf-8") result = subprocess.run( [binary, "-w", str(size), "-h", str(size), str(source), "-o", str(out)], capture_output=True, check=False, ) if result.returncode != 0: msg = f"rsvg-convert failed for {name}: {result.stderr.decode()}" raise RenderError(msg) return out