79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
"""
|
|
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
|
|
|
|
from io import BytesIO
|
|
|
|
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()
|