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:
Executable
+150
@@ -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 9–10 (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()
|
||||
Reference in New Issue
Block a user