#!/usr/bin/env python3 """PROCITY Lane E — skin_sheet.py (R39) Contact sheet for 2-D skins that answers the two questions a thumbnail cannot: **does it tile**, and **what aspect is it actually displayed at**. R38 shipped four grounds and only afterwards recorded that the eight flux-era ones are 512x279 and stretch under a square UV tile — that is the class of defect this sheet exists to catch before a harvest, not after. PY=~/Documents/MODELBEAST/venvs/mflux/bin/python $PY pipeline/skin_sheet.py OUT.png "title" FILE.jpg "caption" [FILE "caption" ...] [--note "line" ...] Each row: the skin as shipped, a 3x3 wrap preview (what RepeatWrapping does to it), and the measured wrap-seam error - mean |difference| between the opposite edges, 0-255, printed as a number rather than left to the eye. """ import os import sys import textwrap import numpy as np from PIL import Image, ImageDraw PAD, LBL, TILE = 12, 15, 220 def seam(a): x = float(np.abs(a[:, 0].astype(np.float32) - a[:, -1].astype(np.float32)).mean()) y = float(np.abs(a[0].astype(np.float32) - a[-1].astype(np.float32)).mean()) return x, y def main(): out_path, title = sys.argv[1], sys.argv[2] args = sys.argv[3:] notes = [] if "--note" in args: i = args.index("--note") notes, args = args[i + 1:], args[:i] rows = [(args[i], args[i + 1]) for i in range(0, len(args), 2)] W = PAD + TILE + PAD + TILE + PAD + 700 H = 26 + len(rows) * (TILE + LBL + PAD) + PAD + LBL * (sum(len(n) // 110 + 2 for n in notes) + 1) sheet = Image.new("RGB", (W, H), (24, 24, 26)) d = ImageDraw.Draw(sheet) d.text((PAD, 6), title, fill=(240, 240, 240)) y = 26 for path, cap in rows: im = Image.open(path).convert("RGB") a = np.asarray(im) sx, sy = seam(a) # as shipped, letterboxed into a square cell so a non-square skin LOOKS non-square cell = Image.new("RGB", (TILE, TILE), (40, 40, 44)) s = im.copy() s.thumbnail((TILE, TILE), Image.LANCZOS) cell.paste(s, ((TILE - s.width) // 2, (TILE - s.height) // 2)) sheet.paste(cell, (PAD, y)) # 3x3 wrap preview t = Image.new("RGB", (im.width * 3, im.height * 3)) for i in range(3): for j in range(3): t.paste(im, (i * im.width, j * im.height)) sheet.paste(t.resize((TILE, TILE), Image.LANCZOS), (PAD + TILE + PAD, y)) x0 = PAD + 2 * (TILE + PAD) d.text((x0, y + 2), os.path.basename(path), fill=(250, 210, 120)) d.text((x0, y + 2 + LBL), f"{im.width}x{im.height} {im.width / im.height:.2f}:1 " f"{os.path.getsize(path)} B wrap-seam x={sx:.1f} y={sy:.1f} (0-255)", fill=(180, 180, 180)) for k, line in enumerate(textwrap.wrap(cap, 96)): d.text((x0, y + 2 + LBL * (2 + k)), line, fill=(215, 215, 215)) d.text((PAD + 3, y + TILE + 1), "as shipped", fill=(150, 150, 150)) d.text((PAD + TILE + PAD + 3, y + TILE + 1), "3x3 wrap (RepeatWrapping)", fill=(150, 150, 150)) y += TILE + LBL + PAD for line in notes: for part in textwrap.wrap(line, 150): d.text((PAD, y), part, fill=(190, 190, 190)) y += LBL sheet.crop((0, 0, W, min(sheet.height, y + PAD))).save(out_path) print(f"skin sheet -> {out_path}") main()