"""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''
)
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