mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-22 14:30:53 +08:00
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Normalize an image-generated pet sheet into the app's v2 atlas contract."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageOps
|
|
|
|
|
|
COLUMNS = 8
|
|
TARGET_ROWS = 11
|
|
CELL_WIDTH = 192
|
|
CELL_HEIGHT = 208
|
|
FRAME_COUNTS = (6, 8, 8, 4, 5, 8, 6, 6, 6, 8, 8)
|
|
NINE_ROW_MAPPING = (0, 1, 1, 2, 3, 4, 5, 6, 6, 7, 8)
|
|
|
|
|
|
def source_cell(image: Image.Image, row: int, column: int, rows: int) -> Image.Image:
|
|
left = round(column * image.width / COLUMNS)
|
|
right = round((column + 1) * image.width / COLUMNS)
|
|
top = round(row * image.height / rows)
|
|
bottom = round((row + 1) * image.height / rows)
|
|
return image.crop((left, top, right, bottom))
|
|
|
|
|
|
def fit_cell(cell: Image.Image, gutter: int, recenter_x: bool) -> Image.Image:
|
|
available_width = CELL_WIDTH - gutter * 2
|
|
available_height = CELL_HEIGHT - gutter * 2
|
|
scale = min(available_width / cell.width, available_height / cell.height)
|
|
size = (
|
|
max(1, round(cell.width * scale)),
|
|
max(1, round(cell.height * scale)),
|
|
)
|
|
resized = cell.resize(size, Image.Resampling.LANCZOS)
|
|
output = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))
|
|
x = (CELL_WIDTH - resized.width) // 2
|
|
y = (CELL_HEIGHT - resized.height) // 2
|
|
output.alpha_composite(resized, (x, y))
|
|
|
|
if recenter_x:
|
|
bounds = output.getchannel("A").getbbox()
|
|
if bounds:
|
|
left, _, right, _ = bounds
|
|
desired_shift = round(CELL_WIDTH / 2 - (left + right) / 2)
|
|
min_shift = gutter - left
|
|
max_shift = CELL_WIDTH - gutter - right
|
|
shift = max(min_shift, min(max_shift, desired_shift))
|
|
if shift:
|
|
centered = Image.new("RGBA", output.size, (0, 0, 0, 0))
|
|
centered.alpha_composite(output, (shift, 0))
|
|
output = centered
|
|
return output
|
|
|
|
|
|
def clear_transparent_rgb(image: Image.Image) -> Image.Image:
|
|
data = bytearray(image.tobytes())
|
|
for index in range(0, len(data), 4):
|
|
if data[index + 3] == 0:
|
|
data[index : index + 3] = b"\x00\x00\x00"
|
|
return Image.frombytes("RGBA", image.size, bytes(data))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--input", required=True, type=Path)
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
parser.add_argument("--source-rows", required=True, type=int, choices=(9, 11))
|
|
parser.add_argument(
|
|
"--gutter",
|
|
type=int,
|
|
default=0,
|
|
choices=range(0, 33),
|
|
metavar="PX",
|
|
help="Inset every source cell by this many pixels to prevent edge clipping.",
|
|
)
|
|
parser.add_argument(
|
|
"--recenter-x",
|
|
action="store_true",
|
|
help="Center each visible silhouette horizontally after fitting the cell.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
with Image.open(args.input) as opened:
|
|
source = opened.convert("RGBA")
|
|
|
|
atlas = Image.new(
|
|
"RGBA",
|
|
(COLUMNS * CELL_WIDTH, TARGET_ROWS * CELL_HEIGHT),
|
|
(0, 0, 0, 0),
|
|
)
|
|
row_mapping = range(TARGET_ROWS) if args.source_rows == TARGET_ROWS else NINE_ROW_MAPPING
|
|
|
|
for target_row, source_row in enumerate(row_mapping):
|
|
for column in range(FRAME_COUNTS[target_row]):
|
|
cell = source_cell(source, source_row, column, args.source_rows)
|
|
if args.source_rows == 9 and target_row == 2:
|
|
cell = ImageOps.mirror(cell)
|
|
atlas.alpha_composite(
|
|
fit_cell(cell, args.gutter, args.recenter_x),
|
|
(column * CELL_WIDTH, target_row * CELL_HEIGHT),
|
|
)
|
|
|
|
neutral = fit_cell(
|
|
source_cell(source, 0, 0, args.source_rows),
|
|
args.gutter,
|
|
args.recenter_x,
|
|
)
|
|
atlas.alpha_composite(neutral, (6 * CELL_WIDTH, 0))
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
atlas = clear_transparent_rgb(atlas)
|
|
if args.output.suffix.lower() == ".webp":
|
|
atlas.save(args.output, "WEBP", lossless=True, method=6, exact=True)
|
|
else:
|
|
atlas.save(args.output, "PNG", optimize=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|