""" SVG to half-block terminal art rasterization. Pipeline: SVG -> cairosvg -> PIL -> greyscale threshold -> half-block encode. Follows the same pixel-pair approach as engine/render.py for OTF fonts. """ from __future__ import annotations import os import sys from io import BytesIO # cairocffi (used by cairosvg) calls dlopen() to find the Cairo C library. # On macOS with Homebrew, Cairo lives in /opt/homebrew/lib (Apple Silicon) or # /usr/local/lib (Intel), which are not in dyld's default search path. # Setting DYLD_LIBRARY_PATH before the import directs dlopen() to those paths. if sys.platform == "darwin" and not os.environ.get("DYLD_LIBRARY_PATH"): for _brew_lib in ("/opt/homebrew/lib", "/usr/local/lib"): if os.path.exists(os.path.join(_brew_lib, "libcairo.2.dylib")): os.environ["DYLD_LIBRARY_PATH"] = _brew_lib break import cairosvg from PIL import Image _cache: dict[tuple[str, int, int], list[str]] = {} def rasterize_svg(svg_path: str, width: int, height: int) -> list[str]: """Convert SVG file to list of half-block terminal rows (uncolored). Args: svg_path: Path to SVG file. width: Target terminal width in columns. height: Target terminal height in rows. Returns: List of strings, one per terminal row, containing block characters. """ cache_key = (svg_path, width, height) if cache_key in _cache: return _cache[cache_key] # SVG -> PNG in memory png_bytes = cairosvg.svg2png( url=svg_path, output_width=width, output_height=height * 2, # 2 pixel rows per terminal row ) # PNG -> greyscale PIL image # Composite RGBA onto white background so transparent areas become white (255) # and drawn pixels retain their luminance values. img_rgba = Image.open(BytesIO(png_bytes)).convert("RGBA") img_rgba = img_rgba.resize((width, height * 2), Image.Resampling.LANCZOS) background = Image.new("RGBA", img_rgba.size, (255, 255, 255, 255)) background.paste(img_rgba, mask=img_rgba.split()[3]) img = background.convert("L") data = img.tobytes() pix_w = width pix_h = height * 2 # White (255) = empty space, dark (< threshold) = filled pixel threshold = 128 # Half-block encode: walk pixel pairs rows: list[str] = [] for y in range(0, pix_h, 2): row: list[str] = [] for x in range(pix_w): top = data[y * pix_w + x] < threshold bot = data[(y + 1) * pix_w + x] < threshold if y + 1 < pix_h else False if top and bot: row.append("█") elif top: row.append("▀") elif bot: row.append("▄") else: row.append(" ") rows.append("".join(row)) _cache[cache_key] = rows return rows def clear_cache() -> None: """Clear the rasterization cache (e.g., on terminal resize).""" _cache.clear()