7c68aa3bd3
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.
151 lines
5.5 KiB
Python
Executable File
151 lines
5.5 KiB
Python
Executable File
#!/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()
|