Ctrl K
ComunidadGuías…

tools/art_prep.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Turn a generated or supplied illustration into a shippable theme figure.

Keys out the flat background, crops to what is actually drawn, scales it to
something a corner watermark can use, and strips the metadata. What comes back
from an image model is a megabyte of 1408x768 with the figure floating in the
middle of a magenta field; what belongs in the wheel is about 40KB of exactly
the character.

    python3 tools/art_prep.py in.png out.png [--key ff00ff] [--height 560]

Needs Pillow, which the panel does not: this runs once when art arrives, and
the thing it produces is a plain PNG. Nothing at runtime imports it.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

try:
    from PIL import Image
except ImportError:  # pragma: no cover - a developer tool, not the product
    sys.exit("art_prep needs Pillow: pip install Pillow")

# How far a pixel may sit from the key colour and still count as background.
# Generous, because a JPEG-ish encode leaves a halo of near-key pixels around
# every edge and a tight threshold keeps them as a magenta fringe.
TOLERANCE = 60
# Below this alpha a pixel is background outright. Above it, the pixel keeps
# its colour but gets pulled away from the key, which is what stops a magenta
# rim appearing wherever the figure was antialiased against it.
EDGE = 200


def key_out(im: Image.Image, key: tuple[int, int, int]) -> Image.Image:
    im = im.convert("RGBA")
    out = []
    kr, kg, kb = key
    for r, g, b, a in im.get_flattened_data():
        near = max(abs(r - kr), abs(g - kg), abs(b - kb))
        if near <= TOLERANCE:
            out.append((r, g, b, 0))
        elif near <= TOLERANCE * 3:
            # An edge pixel: keep it, but unmix the key out of it so the
            # figure does not ship wearing a pink outline.
            f = (near - TOLERANCE) / (TOLERANCE * 2)
            out.append((
                min(255, int(r - kr * (1 - f) * 0.55)),
                min(255, int(g - kg * (1 - f) * 0.55)),
                min(255, int(b - kb * (1 - f) * 0.55)),
                int(255 * f),
            ))
        else:
            out.append((r, g, b, a))
    im.putdata(out)
    return im


def prep(src: Path, dst: Path, key: tuple[int, int, int], height: int,
         colors: int) -> None:
    im = key_out(Image.open(src), key)
    box = im.getbbox()
    if box:
        im = im.crop(box)
    if im.height > height:
        im = im.resize(
            (max(1, round(im.width * height / im.height)), height), Image.LANCZOS
        )
    # Quantised on purpose. These are cel-shaded illustrations: flat fills,
    # hard edges, a handful of tones per region, which is exactly the case a
    # palette handles without showing. Full colour came out at 2.2MB across
    # the seven, against about 400KB here for a picture that is drawn at nine
    # percent opacity behind somebody's terminal output. `colors` is the one
    # knob worth touching if a future drawing does have a gradient in it.
    flat = Image.new("RGBA", im.size)
    flat.putdata(list(im.get_flattened_data()))
    alpha = flat.getchannel("A")
    small = flat.convert("RGB").quantize(colors=colors, method=Image.MEDIANCUT, dither=Image.NONE)
    small = small.convert("RGBA")
    small.putalpha(alpha)
    small.save(dst, "PNG", optimize=True)
    print(f"  {dst.name:<16} {im.width}x{im.height}  {dst.stat().st_size // 1024} KB")


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("src", type=Path)
    ap.add_argument("dst", type=Path)
    ap.add_argument("--key", default="ff00ff", help="background colour to remove")
    ap.add_argument("--height", type=int, default=420)
    ap.add_argument("--colors", type=int, default=96)
    args = ap.parse_args()
    raw = args.key.lstrip("#")
    prep(args.src, args.dst, tuple(int(raw[i:i + 2], 16) for i in (0, 2, 4)),
         args.height, args.colors)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())