Files
egyptian-escape-room/scripts/extract-artifact-room-map.py
T
clocklear 7c68aa3bd3 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.
2026-06-11 09:57:23 -04:00

108 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Extract puzzle 5 map pages from the printables PDF and stitch into one image."""
from __future__ import annotations
import argparse
import io
import sys
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, ImageOps
def render_page(doc: fitz.Document, page_number: int, dpi: float) -> Image.Image:
page = doc[page_number - 1]
zoom = dpi / 72.0
matrix = fitz.Matrix(zoom, zoom)
pixmap = page.get_pixmap(matrix=matrix, alpha=False)
return Image.open(io.BytesIO(pixmap.tobytes("png")))
def trim_white(image: Image.Image, *, pad: int = 8, threshold: int = 245) -> Image.Image:
gray = image.convert("L")
mask = gray.point(lambda value: 255 if value > threshold else 0)
bbox = mask.getbbox()
if bbox is None:
return image
left, top, right, bottom = bbox
left = max(0, left - pad)
top = max(0, top - pad)
right = min(image.width, right + pad)
bottom = min(image.height, bottom + pad)
return image.crop((left, top, right, bottom))
def match_height(left: Image.Image, right: Image.Image) -> tuple[Image.Image, Image.Image]:
height = max(left.height, right.height)
if left.height != height:
canvas = Image.new("RGB", (left.width, height), "white")
canvas.paste(left, (0, 0))
left = canvas
if right.height != height:
canvas = Image.new("RGB", (right.width, height), "white")
canvas.paste(right, (0, 0))
right = canvas
return left, right
def stitch_map(
pdf_path: str,
*,
left_page: int = 9,
right_page: int = 10,
dpi: float = 200,
) -> Image.Image:
doc = fitz.open(pdf_path)
try:
if doc.page_count < max(left_page, right_page):
raise ValueError(
f"PDF has {doc.page_count} pages; need at least page {max(left_page, right_page)}"
)
left = trim_white(render_page(doc, left_page, dpi))
right = trim_white(render_page(doc, right_page, dpi))
left, right = match_height(left, right)
combined = Image.new("RGB", (left.width + right.width, left.height), "white")
combined.paste(left, (0, 0))
combined.paste(right, (left.width, 0))
return combined
finally:
doc.close()
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("pdf", help="Path to EgyptEscapeRoomPrintables.pdf")
parser.add_argument("output", help="Output image path (.jpg or .png)")
parser.add_argument("--left-page", type=int, default=9, help="Left half page number (default: 9)")
parser.add_argument("--right-page", type=int, default=10, help="Right half page number (default: 10)")
parser.add_argument("--dpi", type=float, default=200, help="Render DPI (default: 200)")
args = parser.parse_args()
image = stitch_map(
args.pdf,
left_page=args.left_page,
right_page=args.right_page,
dpi=args.dpi,
)
output = args.output.lower()
if output.endswith(".png"):
image.save(args.output, optimize=True)
else:
image.save(args.output, quality=90, optimize=True)
print(f"Wrote {args.output} ({image.width}x{image.height})")
if __name__ == "__main__":
main()