diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 61c6a249..c566c358 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -156,6 +156,9 @@ jobs: fi echo "docs/gallery/ matches scripts/build_gallery.py output." + - name: Check gallery hero and preview sizes + run: python3 tests/check_gallery_images.py + - name: Validate template Python syntax run: | echo "Checking template Python syntax..." diff --git a/showcase/anvil/preview.webp b/showcase/anvil/preview.webp index 39b42162..61f49b02 100644 Binary files a/showcase/anvil/preview.webp and b/showcase/anvil/preview.webp differ diff --git a/showcase/campfire/preview.webp b/showcase/campfire/preview.webp index 9f1d0ac4..106ae2f3 100644 Binary files a/showcase/campfire/preview.webp and b/showcase/campfire/preview.webp differ diff --git a/showcase/cart/preview.webp b/showcase/cart/preview.webp index 28b5a011..f2016f39 100644 Binary files a/showcase/cart/preview.webp and b/showcase/cart/preview.webp differ diff --git a/showcase/chopping-block/preview.webp b/showcase/chopping-block/preview.webp index a1327ed4..9ba38ad0 100644 Binary files a/showcase/chopping-block/preview.webp and b/showcase/chopping-block/preview.webp differ diff --git a/showcase/crate-stack/preview.webp b/showcase/crate-stack/preview.webp index c2cbd00b..178123a4 100644 Binary files a/showcase/crate-stack/preview.webp and b/showcase/crate-stack/preview.webp differ diff --git a/showcase/fence-kit/preview.webp b/showcase/fence-kit/preview.webp index 836bf9d7..66ae37f5 100644 Binary files a/showcase/fence-kit/preview.webp and b/showcase/fence-kit/preview.webp differ diff --git a/showcase/hand-pump/preview.webp b/showcase/hand-pump/preview.webp index d52e9080..06e69f45 100644 Binary files a/showcase/hand-pump/preview.webp and b/showcase/hand-pump/preview.webp differ diff --git a/showcase/hitching-post/preview.webp b/showcase/hitching-post/preview.webp index 6954d2a8..2c93144a 100644 Binary files a/showcase/hitching-post/preview.webp and b/showcase/hitching-post/preview.webp differ diff --git a/showcase/market-stall/preview.webp b/showcase/market-stall/preview.webp index a223a5aa..91aec637 100644 Binary files a/showcase/market-stall/preview.webp and b/showcase/market-stall/preview.webp differ diff --git a/showcase/park-bench/preview.webp b/showcase/park-bench/preview.webp index 3305f65e..c17c89b4 100644 Binary files a/showcase/park-bench/preview.webp and b/showcase/park-bench/preview.webp differ diff --git a/showcase/signpost/preview.webp b/showcase/signpost/preview.webp index c8c1ef4e..3048328b 100644 Binary files a/showcase/signpost/preview.webp and b/showcase/signpost/preview.webp differ diff --git a/showcase/stone-archway/preview.webp b/showcase/stone-archway/preview.webp index 04f851b8..dbb481b1 100644 Binary files a/showcase/stone-archway/preview.webp and b/showcase/stone-archway/preview.webp differ diff --git a/showcase/stone-well/preview.webp b/showcase/stone-well/preview.webp index c4f7637d..040ca98d 100644 Binary files a/showcase/stone-well/preview.webp and b/showcase/stone-well/preview.webp differ diff --git a/showcase/street-lantern/preview.webp b/showcase/street-lantern/preview.webp index b3780938..d11dab4b 100644 Binary files a/showcase/street-lantern/preview.webp and b/showcase/street-lantern/preview.webp differ diff --git a/showcase/terrain-scatter/preview.webp b/showcase/terrain-scatter/preview.webp index 8325bc55..8399ff29 100644 Binary files a/showcase/terrain-scatter/preview.webp and b/showcase/terrain-scatter/preview.webp differ diff --git a/showcase/wall-torch/preview.webp b/showcase/wall-torch/preview.webp index a790e58b..438de785 100644 Binary files a/showcase/wall-torch/preview.webp and b/showcase/wall-torch/preview.webp differ diff --git a/showcase/watchtower/preview.webp b/showcase/watchtower/preview.webp index 9f5cdc00..edbfd271 100644 Binary files a/showcase/watchtower/preview.webp and b/showcase/watchtower/preview.webp differ diff --git a/showcase/water-trough/preview.webp b/showcase/water-trough/preview.webp index 39c58dc8..ad6c9f65 100644 Binary files a/showcase/water-trough/preview.webp and b/showcase/water-trough/preview.webp differ diff --git a/showcase/wooden-ladder/preview.webp b/showcase/wooden-ladder/preview.webp index 2b6c2c42..95df9bf3 100644 Binary files a/showcase/wooden-ladder/preview.webp and b/showcase/wooden-ladder/preview.webp differ diff --git a/tests/check_gallery_images.py b/tests/check_gallery_images.py new file mode 100644 index 00000000..d010d23d --- /dev/null +++ b/tests/check_gallery_images.py @@ -0,0 +1,81 @@ +"""Every gallery entry's hero and preview webp must be the specified size. + +CLAUDE.md fixes the sizes: hero 1280x720 (docs/gallery/assets/), preview +1200x675 (next to the entry's script). Nothing enforced them, and 19 of the +28 showcase previews shipped at the hero size (#214). + +Stdlib only, so it runs on a bare CI runner: the WebP header is parsed +directly (RIFF container; VP8, VP8L and VP8X chunks). + +Exit codes: 0 clean, 1 a size or format is wrong, 2 usage. +""" +from __future__ import annotations + +import json +import os +import struct +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SOURCES = (("examples/gallery.json", "examples"), ("showcase/gallery.json", "pieces")) +EXPECTED = {"hero": (1280, 720), "preview": (1200, 675)} + + +def webp_size(path: str) -> tuple[int, int]: + """(width, height) from a WebP file header. Raises ValueError otherwise.""" + with open(path, "rb") as f: + head = f.read(30) + if len(head) < 30 or head[:4] != b"RIFF" or head[8:12] != b"WEBP": + raise ValueError("not a RIFF/WEBP file") + chunk = head[12:16] + if chunk == b"VP8X": # extended: 24-bit canvas size minus one + w = int.from_bytes(head[24:27], "little") + 1 + h = int.from_bytes(head[27:30], "little") + 1 + return w, h + if chunk == b"VP8L": # lossless: 14-bit fields after the 0x2f signature + if head[20] != 0x2F: + raise ValueError("bad VP8L signature") + bits = int.from_bytes(head[21:25], "little") + return (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1 + if chunk == b"VP8 ": # lossy: keyframe start code, then 14-bit sizes + if head[23:26] != b"\x9d\x01\x2a": + raise ValueError("bad VP8 start code") + w, h = struct.unpack(" int: + failures, checked = [], 0 + for rel, key in SOURCES: + with open(os.path.join(ROOT, rel), encoding="utf-8") as f: + entries = json.load(f)[key] + for entry in entries: + for field, want in EXPECTED.items(): + path = entry.get(field) + if not path: + failures.append(f"{entry['name']}: no '{field}' path in {rel}") + continue + checked += 1 + try: + got = webp_size(os.path.join(ROOT, path)) + except (OSError, ValueError) as exc: + failures.append(f"{entry['name']}: {field} {path}: {exc}") + continue + if got != want: + failures.append( + f"{entry['name']}: {field} {path} is {got[0]}x{got[1]}, " + f"expected {want[0]}x{want[1]}" + ) + for msg in failures: + print(f" ERROR: {msg}") + if failures: + print(f"\n{len(failures)} gallery image(s) off spec " + "(hero 1280x720, preview 1200x675).") + return 1 + print(f"gallery image sizes ok: {checked} image(s) across both gallery.json files.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())