mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 13:10:51 +02:00
123 lines
4.3 KiB
Python
123 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Assemble the per-map battle-arena screenshots into contact sheets.
|
|
|
|
One shot per map is a lot of files to open one at a time, so this lays them
|
|
out eight to a page -- a 2x4 grid, each tile labelled with its map id -- which
|
|
is enough to judge a whole region at a glance and spot the one that is wrong.
|
|
|
|
python mods/DramaticShapeVoxelMod/tools/contact_sheets.py \
|
|
.scratchpad/areas .scratchpad/sheets [order.txt]
|
|
|
|
Reads every PNG in the source directory and writes sheet_01.png,
|
|
sheet_02.png ... into the destination.
|
|
|
|
The optional third argument is a file of map ids, one per line, which both
|
|
FILTERS and ORDERS the sheets. That is what makes them worth looking at: the
|
|
authored list in data/battle_arenas.lua is grouped by region, so feeding its
|
|
keys through puts the routes together and the caves together, instead of
|
|
alphabetical order interleaving a gym with a tower floor. Without it every
|
|
PNG in the directory is used, sorted by name.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
PER_SHEET = 8
|
|
COLS, ROWS = 2, 4
|
|
TILE_W = 960 # each shot is 3840 wide; a quarter of that
|
|
LABEL_H = 34
|
|
PAD = 8
|
|
BG = (18, 18, 20)
|
|
LABEL_BG = (32, 32, 36)
|
|
LABEL_FG = (232, 232, 236)
|
|
|
|
|
|
def font(size):
|
|
for name in ("consola.ttf", "DejaVuSansMono.ttf", "arial.ttf"):
|
|
try:
|
|
return ImageFont.truetype(name, size)
|
|
except OSError:
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def tile(path, w, h, label, f):
|
|
"""One labelled cell: the shot scaled to fit, with its map id under it."""
|
|
cell = Image.new("RGB", (w, h + LABEL_H), LABEL_BG)
|
|
try:
|
|
shot = Image.open(path).convert("RGB")
|
|
except OSError as err:
|
|
d = ImageDraw.Draw(cell)
|
|
d.text((8, 8), "unreadable: %s" % err, fill=(220, 90, 90), font=f)
|
|
return cell
|
|
shot.thumbnail((w, h), Image.LANCZOS)
|
|
cell.paste(shot, ((w - shot.width) // 2, (h - shot.height) // 2))
|
|
d = ImageDraw.Draw(cell)
|
|
d.rectangle([0, h, w, h + LABEL_H], fill=LABEL_BG)
|
|
d.text((10, h + 7), label, fill=LABEL_FG, font=f)
|
|
return cell
|
|
|
|
|
|
def wanted(src, order_path):
|
|
"""The PNGs to lay out, in the order to lay them out in."""
|
|
have = {n.lower(): n for n in os.listdir(src)
|
|
if n.lower().endswith(".png")}
|
|
if not order_path:
|
|
return sorted(have.values()), []
|
|
names, missing = [], []
|
|
with open(order_path, encoding="utf-8") as fh:
|
|
for line in fh:
|
|
key = line.strip()
|
|
if not key or key.startswith("#"):
|
|
continue
|
|
hit = have.get(key.lower() + ".png")
|
|
if hit:
|
|
names.append(hit)
|
|
else:
|
|
missing.append(key)
|
|
return names, missing
|
|
|
|
|
|
def main(src, dst, order_path=None):
|
|
names, missing = wanted(src, order_path)
|
|
if not names:
|
|
sys.exit("no PNGs to lay out from " + src)
|
|
# said out loud rather than silently skipped: a sheet set that quietly
|
|
# dropped a map would read as "every area is covered" when it is not
|
|
for key in missing:
|
|
print("MISSING no shot for %s" % key)
|
|
os.makedirs(dst, exist_ok=True)
|
|
|
|
probe = Image.open(os.path.join(src, names[0]))
|
|
tile_h = round(TILE_W * probe.height / probe.width)
|
|
f = font(24)
|
|
sheet_w = COLS * TILE_W + (COLS + 1) * PAD
|
|
sheet_h = ROWS * (tile_h + LABEL_H) + (ROWS + 1) * PAD
|
|
|
|
made = []
|
|
for page, start in enumerate(range(0, len(names), PER_SHEET), 1):
|
|
batch = names[start:start + PER_SHEET]
|
|
sheet = Image.new("RGB", (sheet_w, sheet_h), BG)
|
|
for i, name in enumerate(batch):
|
|
cell = tile(os.path.join(src, name), TILE_W, tile_h,
|
|
os.path.splitext(name)[0].upper(), f)
|
|
col, row = i % COLS, i // COLS
|
|
x = PAD + col * (TILE_W + PAD)
|
|
y = PAD + row * (tile_h + LABEL_H + PAD)
|
|
sheet.paste(cell, (x, y))
|
|
out = os.path.join(dst, "sheet_%02d.png" % page)
|
|
sheet.save(out, optimize=True)
|
|
made.append((out, [os.path.splitext(n)[0] for n in batch]))
|
|
print("%s %s" % (out, ", ".join(m for _, b in [made[-1]] for m in b)))
|
|
|
|
print("\n%d shots -> %d sheets" % (len(names), len(made)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) not in (3, 4):
|
|
sys.exit(__doc__)
|
|
main(sys.argv[1], sys.argv[2],
|
|
sys.argv[3] if len(sys.argv) == 4 else None)
|