Files
juzi/tools/export_fourth_feedback_assets.py

63 lines
2.8 KiB
Python

#!/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()