63 lines
2.8 KiB
Python
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()
|