#!/usr/bin/env python3 """ Build a contact sheet of every texture under art/ — each PNG upscaled with nearest-neighbour (so pixels stay crisp) and labelled with its relative path. Output: art/CATALOG.png Run from repo root: python3 scripts/build-art-catalog.py """ import os from pathlib import Path from PIL import Image, ImageDraw, ImageFont REPO = Path(__file__).resolve().parent.parent ART = REPO / "art" OUT = ART / "CATALOG.png" TILE = 128 # upscaled texture size COLS = 6 # tiles per row PAD_X = 20 # horizontal padding around each tile PAD_Y = 60 # vertical padding (extra room for label below) LABEL_PX = 14 # label font size BG = (30, 30, 46) # dark background FG = (230, 230, 240) SECTION_BG = (50, 60, 90) def load_font(size): for cand in ( "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "/mnt/c/Windows/Fonts/consola.ttf", "/mnt/c/Windows/Fonts/arial.ttf", ): if os.path.exists(cand): return ImageFont.truetype(cand, size) return ImageFont.load_default() def collect(): """Return {pack_name: [(relative_path, abs_path), ...]} grouped & sorted.""" groups = {} for png in sorted(ART.rglob("*.png")): if png.name == "CATALOG.png": continue rel = png.relative_to(ART) pack = rel.parts[0] groups.setdefault(pack, []).append((rel, png)) return groups def upscale(png_path, size): img = Image.open(png_path).convert("RGBA") return img.resize((size, size), Image.NEAREST) def main(): groups = collect() if not groups: print("No PNGs found under art/. Nothing to do.") return font_label = load_font(LABEL_PX) font_section = load_font(LABEL_PX + 8) # Calculate overall canvas size section_height = LABEL_PX + 24 tile_block_h = TILE + PAD_Y total_h = 40 for pack, items in groups.items(): rows = (len(items) + COLS - 1) // COLS total_h += section_height + rows * tile_block_h + 20 total_w = COLS * (TILE + PAD_X) + PAD_X + 40 canvas = Image.new("RGBA", (total_w, total_h), BG) draw = ImageDraw.Draw(canvas) y = 20 for pack, items in groups.items(): # Section header draw.rectangle([20, y, total_w - 20, y + section_height - 4], fill=SECTION_BG) draw.text((32, y + 4), pack, font=font_section, fill=FG) y += section_height + 6 # Tiles for i, (rel, abs_path) in enumerate(items): col = i % COLS row = i // COLS x = 20 + PAD_X // 2 + col * (TILE + PAD_X) ty = y + row * tile_block_h try: tile = upscale(abs_path, TILE) canvas.paste(tile, (x, ty), tile) except Exception as e: draw.rectangle([x, ty, x + TILE, ty + TILE], outline=(200, 80, 80), width=2) draw.text((x + 4, ty + 4), f"ERR: {e}", font=font_label, fill=(255, 120, 120)) # Label: show subpath + filename below the tile label = "/".join(rel.parts[1:]) # drop pack prefix draw.text((x, ty + TILE + 4), label, font=font_label, fill=FG) rows = (len(items) + COLS - 1) // COLS y += rows * tile_block_h + 20 canvas.save(OUT) print(f"Wrote {OUT} — {sum(len(v) for v in groups.values())} textures across {len(groups)} packs.") if __name__ == "__main__": main()