Add museum map reveal, artifact path puzzle, and homelab media tooling.

Puzzle 1 crossfades to the stitched map with a link to /museum-path; puzzle 2 is a touch-friendly drag-and-order challenge with PDF extract scripts for map and artifact assets.
This commit is contained in:
2026-06-11 09:57:23 -04:00
parent c810efd8e5
commit 7c68aa3bd3
20 changed files with 1179 additions and 29 deletions
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Extract the 16 museum-map artifact images from EgyptEscapeRoomPrintables.pdf."""
from __future__ import annotations
import argparse
import io
import json
import sys
from pathlib import Path
try:
import fitz # PyMuPDF
except ImportError:
print("PyMuPDF is required: pip install pymupdf", file=sys.stderr)
raise SystemExit(1) from None
from PIL import Image
# Identified by xref on pages 910 (puzzle 5 map). Slugs match the answer sheet.
ARTIFACTS: list[dict[str, int | str]] = [
{"page": 9, "xref": 56, "slug": "double-crown", "name": "Double Crown"},
{"page": 9, "xref": 57, "slug": "obelisk", "name": "Obelisk"},
{"page": 9, "xref": 63, "slug": "goddess-isis", "name": "Goddess Isis"},
{"page": 9, "xref": 53, "slug": "canopic-jars", "name": "Canopic Jars"},
{"page": 9, "xref": 61, "slug": "howard-carter-excavating-king-tut", "name": "Howard Carter excavating King Tut"},
{"page": 9, "xref": 54, "slug": "anubis", "name": "Anubis"},
{"page": 9, "xref": 58, "slug": "bust-of-nefertiti", "name": "Bust of Nefertiti"},
{"page": 9, "xref": 60, "slug": "god-osiris", "name": "God Osiris"},
{"page": 10, "xref": 68, "slug": "pyramid-of-giza", "name": "Pyramid of Giza"},
{"page": 10, "xref": 75, "slug": "temple-of-hatshepsut", "name": "Temple of Hatshepsut"},
{"page": 10, "xref": 72, "slug": "map-of-the-nile", "name": "Map of the Nile"},
{"page": 10, "xref": 67, "slug": "egyptian-painting", "name": "Egyptian painting"},
{"page": 10, "xref": 74, "slug": "sarcophagus", "name": "Sarcophagus"},
{"page": 10, "xref": 70, "slug": "statue-of-hatshepsut", "name": "Statue of Hatshepsut"},
{"page": 10, "xref": 76, "slug": "step-pyramid", "name": "Step Pyramid"},
{"page": 10, "xref": 71, "slug": "mastaba", "name": "Mastaba"},
]
def find_image_info(page: fitz.Page, xref: int) -> dict:
for info in page.get_image_info(xrefs=True):
if info["xref"] == xref:
return info
raise ValueError(f"Image xref {xref} not found on page {page.number + 1}")
def render_clip(page: fitz.Page, bbox: tuple[float, float, float, float], dpi: float) -> Image.Image:
matrix = fitz.Matrix(dpi / 72.0, dpi / 72.0)
pixmap = page.get_pixmap(matrix=matrix, clip=fitz.Rect(bbox), alpha=False)
return Image.open(io.BytesIO(pixmap.tobytes("png")))
def extract_embedded(doc: fitz.Document, xref: int) -> tuple[Image.Image, str]:
data = doc.extract_image(xref)
ext = data["ext"]
image = Image.open(io.BytesIO(data["image"]))
smask = data.get("smask")
if smask:
mask_data = doc.extract_image(smask)
mask = Image.open(io.BytesIO(mask_data["image"])).convert("L")
if mask.size != image.size:
mask = mask.resize(image.size, Image.Resampling.LANCZOS)
rgba = image.convert("RGBA")
rgba.putalpha(mask)
background = Image.new("RGBA", rgba.size, (255, 255, 255, 255))
image = Image.alpha_composite(background, rgba).convert("RGB")
ext = "png"
return image, ext
def extract_artifacts(
pdf_path: str,
output_dir: Path,
*,
dpi: float = 300,
mode: str = "render",
) -> list[dict[str, object]]:
doc = fitz.open(pdf_path)
output_dir.mkdir(parents=True, exist_ok=True)
manifest: list[dict[str, object]] = []
try:
for artifact in ARTIFACTS:
page_number = int(artifact["page"])
xref = int(artifact["xref"])
slug = str(artifact["slug"])
name = str(artifact["name"])
page = doc[page_number - 1]
info = find_image_info(page, xref)
if mode == "render":
image = render_clip(page, info["bbox"], dpi)
filename = f"{slug}.png"
image.save(output_dir / filename, optimize=True)
else:
image, ext = extract_embedded(doc, xref)
filename = f"{slug}.{ext}"
if ext == "jpeg":
image.save(output_dir / filename, quality=92, optimize=True)
else:
image.save(output_dir / filename, optimize=True)
manifest.append(
{
"slug": slug,
"name": name,
"page": page_number,
"xref": xref,
"file": filename,
"width": image.width,
"height": image.height,
"bbox": info["bbox"],
}
)
print(f"Wrote {filename} ({image.width}x{image.height})")
finally:
doc.close()
manifest_path = output_dir / "manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {manifest_path}")
return manifest
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("pdf", help="Path to EgyptEscapeRoomPrintables.pdf")
parser.add_argument(
"output_dir",
nargs="?",
default="public/media/artifacts",
help="Output directory (default: public/media/artifacts)",
)
parser.add_argument("--dpi", type=float, default=300, help="Render DPI for --mode render (default: 300)")
parser.add_argument(
"--mode",
choices=("render", "embedded"),
default="render",
help="render: clip from page (default, best for illustrations); embedded: raw image bytes",
)
args = parser.parse_args()
extract_artifacts(args.pdf, Path(args.output_dir), dpi=args.dpi, mode=args.mode)
if __name__ == "__main__":
main()