#!/usr/bin/env python3 # 拼板图(contact sheet):批次样张 → 单张大图,创始人直接打开逐张眼验(复用批次一脚本,加可选格尺寸参数) # 用法: python3 contact-sheet.py <输出.png> [列数] [标题] [格宽] [格高] # python3 contact-sheet.py --files <输出.png> <标题> <标签=路径>... (任意文件模式) # 排序按 shots.json 镜头单原序;每格附镜头 id 标签;中文标题用霞鹜文楷(OFL,scratchpad 依赖)。 import sys, os, json from PIL import Image, ImageDraw, ImageFont FONT_TTF = '/private/tmp/claude-501/-Users-lili-Project-games-development-ai/69248bd4-16ac-4906-aea1-4157edf7b10f/scratchpad/deps/LXGWWenKai-Regular.ttf' def load_font(size): try: return ImageFont.truetype(FONT_TTF, size) except Exception: return ImageFont.load_default() def make_sheet(items, out_path, cols, title, tile_w=360, tile_h=420): label_h, title_h, gap = 30, 56, 8 rows = (len(items) + cols - 1) // cols W = cols * (tile_w + gap) + gap H = title_h + rows * (tile_h + label_h + gap) + gap sheet = Image.new('RGB', (W, H), (16, 27, 51)) # 墨蓝深空底,配暖夜霓虹批次 d = ImageDraw.Draw(sheet) d.text((gap + 4, 12), title, fill=(246, 184, 75), font=load_font(30)) f_label = load_font(20) for i, (label, path) in enumerate(items): r, c = divmod(i, cols) x = gap + c * (tile_w + gap) y = title_h + r * (tile_h + label_h + gap) try: im = Image.open(path).convert('RGB') except Exception as e: d.text((x, y), f'{label}: 打开失败 {e}', fill=(200, 80, 80), font=f_label); continue im.thumbnail((tile_w, tile_h)) ox, oy = x + (tile_w - im.width) // 2, y + (tile_h - im.height) // 2 sheet.paste(im, (ox, oy)) d.rectangle([x, y + tile_h, x + tile_w, y + tile_h + label_h], fill=(12, 20, 38)) d.text((x + 6, y + tile_h + 4), label, fill=(220, 220, 210), font=f_label) sheet.save(out_path) print(f'拼板完成:{out_path} ({W}x{H}, {len(items)} 格, {os.path.getsize(out_path)//1024}KB)') if __name__ == '__main__': if sys.argv[1] == '--files': out, title = sys.argv[2], sys.argv[3] items = [(kv.split('=', 1)[0], kv.split('=', 1)[1]) for kv in sys.argv[4:]] make_sheet(items, out, cols=min(4, max(1, len(items))), title=title) else: shots_p, img_root, game, out = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] cols = int(sys.argv[5]) if len(sys.argv) > 5 else 6 title = sys.argv[6] if len(sys.argv) > 6 else f'风格版批次二 · {game}' tile_w = int(sys.argv[7]) if len(sys.argv) > 7 else 360 tile_h = int(sys.argv[8]) if len(sys.argv) > 8 else 420 shots = [s for s in json.load(open(shots_p, encoding='utf-8'))['shots'] if s['game'] == game] items = [] for s in shots: hit = None for ext in ('.jpg', '.jpeg', '.png', '.webp'): p = os.path.join(img_root, game, s['id'] + ext) if os.path.exists(p): hit = p; break if hit: items.append((s['id'], hit)) else: print(f"警告:缺 {s['id']}") make_sheet(items, out, cols=cols, title=title)