#!/usr/bin/env python3 """Replace public-site t-shirt mockups with 3D printer / printed-toy photos.""" from __future__ import annotations import io import urllib.request from pathlib import Path from PIL import Image, ImageOps ROOT = Path(__file__).resolve().parent.parent IMAGES = ROOT / "site" / "print_forge" / "static" / "images" CACHE = ROOT / ".pexels-cache" USER_HERO = [ Path( "/home/westfarn/.cursor/projects/home-westfarn-Documents-repos-print-forge/assets/" "pexels-jakubzerdzicki-20688553-82987c3e-5bc2-4514-9eb0-2eb9518d5d63.png" ), Path( "/home/westfarn/.cursor/projects/home-westfarn-Documents-repos-print-forge/assets/" "pexels-jakubzerdzicki-24859620-43b11020-90b7-4d02-9c78-d28b2cc14a4e.png" ), ] PEXELS = { "figurine": 13624760, "miniatures": 35063297, "tray": 19583534, "neon": 30720501, "extruder": 31336922, "spool": 30658376, "lab": 23533991, "neon2": 18296466, "workshop": 30482193, "nozzle": 23533982, "hex": 30620861, "orange_parts": 20688553, "maker": 24859620, } UA = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" ) def fetch_pexels(photo_id: int) -> Path: CACHE.mkdir(parents=True, exist_ok=True) dest = CACHE / f"pexels-{photo_id}.jpg" if dest.exists() and dest.stat().st_size > 20_000: return dest url = ( f"https://images.pexels.com/photos/{photo_id}/" f"pexels-photo-{photo_id}.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=2000" ) req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "image/*"}) with urllib.request.urlopen(req, timeout=60) as resp: data = resp.read() if len(data) < 5000: raise RuntimeError(f"Pexels {photo_id} too small ({len(data)} bytes)") dest.write_bytes(data) return dest def open_image(path: Path) -> Image.Image: image = Image.open(path) image.load() image = ImageOps.exif_transpose(image) or image return image.convert("RGB") def cover_crop(image: Image.Image, width: int, height: int, focus: str = "center") -> Image.Image: src_w, src_h = image.size scale = max(width / src_w, height / src_h) new_w = max(width, int(src_w * scale + 0.5)) new_h = max(height, int(src_h * scale + 0.5)) resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS) if focus == "left": left = 0 elif focus == "right": left = new_w - width else: left = (new_w - width) // 2 if focus == "top": top = 0 elif focus == "bottom": top = new_h - height else: top = (new_h - height) // 2 return resized.crop((left, top, left + width, top + height)) def save_jpeg(image: Image.Image, dest: Path, quality: int = 86) -> None: dest.parent.mkdir(parents=True, exist_ok=True) image.save(dest, format="JPEG", quality=quality, optimize=True, progressive=True) def save_png(image: Image.Image, dest: Path) -> None: dest.parent.mkdir(parents=True, exist_ok=True) image.save(dest, format="PNG", optimize=True) def main() -> None: sources: dict[str, Image.Image] = { "hero1": open_image(USER_HERO[0]), "hero2": open_image(USER_HERO[1]), } failed = [] for name, photo_id in PEXELS.items(): try: sources[name] = open_image(fetch_pexels(photo_id)) print(f"ok {name} {photo_id} {sources[name].size}") except Exception as exc: # noqa: BLE001 failed.append(f"{name}/{photo_id}: {exc}") print(f"fail {name} {photo_id}: {exc}") def src(*names: str) -> Image.Image: for name in names: if name in sources: return sources[name] raise KeyError(names) jobs = [ ("slider-image-01-1132x657.jpg", src("hero1"), (1132, 657), "center", "jpeg"), ("slider-image-02-1132x657.jpg", src("hero2"), (1132, 657), "center", "jpeg"), ("box-nina-img-364x474.jpg", src("figurine", "tray", "hero1"), (364, 474), "center", "jpeg"), ("box-nina-img-mini-217x328.jpg", src("spool", "extruder", "hero2"), (217, 328), "center", "jpeg"), ("gallery-22-1200x800-original.jpg", src("figurine", "hero1"), (1200, 800), "center", "jpeg"), ("gallery-22-582x299.jpg", src("figurine", "hero1"), (582, 299), "center", "jpeg"), ("gallery-22-582x558.jpg", src("workshop", "hero2"), (582, 558), "center", "jpeg"), ("gallery-4-1200x800-original.jpg", src("miniatures", "tray", "hero1"), (1200, 800), "center", "jpeg"), ("gallery-23-287x252.jpg", src("miniatures", "tray", "hero1"), (287, 252), "center", "jpeg"), ("gallery-23-1200x800-original.jpg", src("tray", "miniatures", "hero1"), (1200, 800), "center", "jpeg"), ("quote-1-287x252.jpg", src("tray", "miniatures", "hero1"), (287, 252), "center", "jpeg"), ("gallery-20-800x1200-original.jpg", src("spool", "extruder", "hero2"), (800, 1200), "center", "jpeg"), ("gallery-20-287x252.jpg", src("spool", "extruder", "hero2"), (287, 252), "center", "jpeg"), ("gallery-2-1200x800-original.jpg", src("neon", "neon2", "hero2"), (1200, 800), "center", "jpeg"), ("gallery-21-287x252.jpg", src("neon", "neon2", "hero2"), (287, 252), "center", "jpeg"), ("gallery-21-1200x800-original.jpg", src("lab", "workshop", "hero2"), (1200, 800), "center", "jpeg"), ("video-1-549x384.jpg", src("nozzle", "extruder", "hero1"), (549, 384), "center", "jpeg"), ("image-1-333x262.jpg", src("tray", "miniatures", "hero1"), (333, 262), "center", "jpeg"), ("bg-breadcrumbs.jpg", src("hero2", "workshop", "lab"), (1920, 414), "center", "jpeg"), ("bg-image-1.jpg", src("hero2", "workshop", "lab"), (1920, 950), "center", "jpeg"), ("product-1-292x256.png", src("figurine", "hero1"), (292, 256), "center", "png"), ("product-2-292x256.png", src("miniatures", "tray", "hero1"), (292, 256), "center", "png"), ("product-3-292x256.png", src("tray", "hero1"), (292, 256), "center", "png"), ("product-4-292x256.png", src("hex", "neon", "hero2"), (292, 256), "center", "png"), ("product-5-292x256.png", src("spool", "extruder", "hero2"), (292, 256), "center", "png"), ("product-6-292x256.png", src("lab", "workshop", "hero2"), (292, 256), "center", "png"), ("product-mini-1-146x132.png", src("figurine", "hero1"), (146, 132), "center", "png"), ("product-mini-2-146x132.png", src("tray", "hero1"), (146, 132), "center", "png"), ("single-product-1-530x480.png", src("figurine", "hero1"), (530, 480), "center", "png"), ("single-product-2-530x480.png", src("miniatures", "tray", "hero1"), (530, 480), "center", "png"), ("single-product-3-530x480.png", src("hero1", "tray"), (530, 480), "center", "png"), ] for filename, image, size, focus, kind in jobs: framed = cover_crop(image, *size, focus=focus) dest = IMAGES / filename if kind == "png": save_png(framed, dest) else: save_jpeg(framed, dest) print(f"wrote {filename} {framed.size}") if failed: print("download warnings:") for item in failed: print(" ", item) if __name__ == "__main__": main()