generated from westfarn/web_django_template
Stand up Print Forge as a 3D-printed toy shop with color variants and printer photography.
Replaces the client_site template branding, adds shop/shipping, and points beta CI at master for easy deploy. Closes #1
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user