#!/usr/bin/env python3 """SVG portrait -> 48x48 PNG in the Kissaten palette. python3 tools/svg2portrait.py art/aki-neutral.svg [more.svg ...] Why this route beats exporting a bitmap and resizing: Magnific's SVG export is flat-shaded vector — hundreds of paths, each a solid fill, with only a handful of gradients. Rendering that at an exact multiple of 48 and then taking the *most common* colour in each block is a true pixelate: no blending, no half-pixels, no anti-aliased edges. Bicubic resizing of a bitmap averages instead, which turns every flat region into a gradient and makes the later palette reduction speckle badly. Requires qlmanage (macOS built-in) to rasterise. No third-party deps. """ import subprocess, sys, os, tempfile from collections import Counter sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from pixtool import read_png, write_png, zoom, PALETTE SIZE = 48 SCALE = 16 # render at SIZE*SCALE, then block-reduce PORTRAIT_IDX = [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15] # no sky, no index 0 PP = [PALETTE[i] for i in PORTRAIT_IDX] def nearest(c): r, g, b = c[:3] # green weighted heaviest: keeps eyes and mouth from dissolving into skin return min(PP, key=lambda p: 3*(p[0]-r)**2 + 6*(p[1]-g)**2 + (p[2]-b)**2) def rasterise(svg, px): """SVG -> PNG at px wide, via Quick Look. Returns decoded pixels.""" with tempfile.TemporaryDirectory() as td: subprocess.run(["qlmanage", "-t", "-s", str(px), "-o", td, svg], capture_output=True, check=True) out = os.path.join(td, os.path.basename(svg) + ".png") if not os.path.exists(out): raise SystemExit(f"qlmanage produced nothing for {svg}") return read_png(out) def mode_reduce(rows, n): """Each n*n block collapses to its most common colour — a real pixelate.""" return [[Counter(rows[by*n + y][bx*n + x][:3] for y in range(n) for x in range(n)).most_common(1)[0][0] for bx in range(SIZE)] for by in range(SIZE)] def convert(svg): w, h, big = rasterise(svg, SIZE * SCALE) if w != SIZE * SCALE: raise SystemExit(f"{svg}: expected {SIZE*SCALE}px render, got {w}") small = mode_reduce(big, SCALE) quant = [[nearest(p) for p in r] for r in small] stem = os.path.splitext(svg)[0] write_png(f"{stem}-{SIZE}.png", [[p + (255,) for p in r] for r in quant]) write_png(f"{stem}-{SIZE}-x6.png", zoom([[p + (255,) for p in r] for r in quant], 6)) print(f"{os.path.basename(svg)}: {len({p for r in small for p in r})} colours " f"after pixelate -> {len({p for r in quant for p in r})} after palette") print(f" wrote {stem}-{SIZE}.png and {stem}-{SIZE}-x6.png (preview)") if __name__ == "__main__": if len(sys.argv) < 2: raise SystemExit(__doc__) for f in sys.argv[1:]: convert(f)