chore: share game-studio hermes plugin
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a simple contact sheet from a directory of normalized sprite frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
NUMBER_RE = re.compile(r"(\d+)")
|
||||
|
||||
|
||||
def natural_key(path: Path) -> list[int | str]:
|
||||
parts: list[int | str] = []
|
||||
for chunk in NUMBER_RE.split(path.stem):
|
||||
if not chunk:
|
||||
continue
|
||||
if chunk.isdigit():
|
||||
parts.append(int(chunk))
|
||||
else:
|
||||
parts.append(chunk)
|
||||
parts.append(path.suffix)
|
||||
return parts
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a preview contact sheet from a directory of sprite frames."
|
||||
)
|
||||
parser.add_argument("--frames-dir", required=True, help="Directory containing PNG frames.")
|
||||
parser.add_argument("--out", required=True, help="Output PNG path.")
|
||||
parser.add_argument(
|
||||
"--columns",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of columns in the preview sheet. Default: 4.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gap",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Gap between frames in pixels. Default: 8.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def paint_checkerboard(image: Image.Image, tile: int = 16) -> None:
|
||||
draw = ImageDraw.Draw(image)
|
||||
colors = ((240, 243, 246, 255), (225, 230, 235, 255))
|
||||
for top in range(0, image.height, tile):
|
||||
for left in range(0, image.width, tile):
|
||||
color = colors[((left // tile) + (top // tile)) % 2]
|
||||
draw.rectangle((left, top, left + tile, top + tile), fill=color)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.columns < 1:
|
||||
raise SystemExit("--columns must be at least 1.")
|
||||
if args.gap < 0:
|
||||
raise SystemExit("--gap cannot be negative.")
|
||||
|
||||
frame_dir = Path(args.frames_dir)
|
||||
frames = sorted(frame_dir.glob("*.png"), key=natural_key)
|
||||
if not frames:
|
||||
raise SystemExit("No PNG frames were found in --frames-dir.")
|
||||
|
||||
images = [Image.open(path).convert("RGBA") for path in frames]
|
||||
frame_width = max(image.width for image in images)
|
||||
frame_height = max(image.height for image in images)
|
||||
rows = math.ceil(len(images) / args.columns)
|
||||
sheet_width = args.columns * frame_width + max(0, args.columns - 1) * args.gap
|
||||
sheet_height = rows * frame_height + max(0, rows - 1) * args.gap
|
||||
sheet = Image.new("RGBA", (sheet_width, sheet_height), (255, 255, 255, 255))
|
||||
paint_checkerboard(sheet)
|
||||
|
||||
for index, image in enumerate(images):
|
||||
row = index // args.columns
|
||||
column = index % args.columns
|
||||
left = column * (frame_width + args.gap) + (frame_width - image.width) // 2
|
||||
top = row * (frame_height + args.gap) + (frame_height - image.height) // 2
|
||||
sheet.alpha_composite(image, (left, top))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user