#!/usr/bin/env python3 """Split the original PSD scan artwork for deterministic, code-driven motion.""" import argparse import hashlib import json from pathlib import Path from PIL import Image, ImageDraw from psd_tools import PSDImage ROOT = Path(__file__).resolve().parents[1] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("psd", type=Path) args = parser.parse_args() psd = PSDImage.open(args.psd) page = psd[9] if page.name != "扫描" or page[2][1].name != "矩形 661": raise ValueError("Unexpected scan layout; nothing exported") left, top, right, bottom = page.bbox viewport = (left, top + 248, right, bottom) source = page[2][2] source.visible = False base = page.composite(viewport=viewport).convert("RGB") # Recover the connected white face from the unmodified cached PSD pixels. # Disconnected frame/text/icon whites must never become part of the mask. candidate = Image.eval(base.convert("L"), lambda v: 255 if v > 245 else 0) ImageDraw.floodfill(candidate, (540, 720), 128) mask = Image.eval(candidate, lambda v: 255 if v == 128 else 0) if mask.getbbox() is None or mask.getpixel((540, 1200)) != 255: raise ValueError(f"PSD white face was lost to cross-layer clipping: bbox={mask.getbbox()}, seed={base.getpixel((540, 720))}, lower={base.getpixel((540, 1200))}") target = ROOT / "h5/assets/img/ui-20261012" target.mkdir(parents=True, exist_ok=True) alpha_mask = Image.new("RGBA", mask.size, (255, 255, 255, 0)) alpha_mask.putalpha(mask) assets = [] for name, image in [("scan-base.webp", base), ("scan-face-mask.png", alpha_mask)]: output = target / name if output.suffix == ".webp": image.save(output, lossless=True, method=6) else: image.save(output, optimize=True) data = output.read_bytes() assets.append({"file": str(output.relative_to(ROOT)), "size": list(image.size), "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}) print(f"Exported {name}: {image.size}, {len(data)} bytes", flush=True) digest = hashlib.sha256() with args.psd.open("rb") as stream: for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""): digest.update(chunk) manifest = {"source": str(args.psd), "source_sha256": digest.hexdigest(), "source_page": "9", "hidden_scan_layer": "9/2/2", "viewport": list(viewport), "face_bbox": list(mask.getbbox()), "cycle_ms": 1000, "yellow": "#F5F078", "assets": assets} design = ROOT / "design/ui-20261012" (design / "scan-motion-manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n") if __name__ == "__main__": main()