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:
2026-09-06 08:28:04 -05:00
parent 8a97e3fbe2
commit b2029b6c0f
336 changed files with 9241 additions and 2537 deletions
+121
View File
@@ -0,0 +1,121 @@
"""Turn a raw product photo into a centered cutout on a square canvas."""
from __future__ import annotations
import logging
from io import BytesIO
from pathlib import PurePosixPath
from PIL import Image, ImageOps
logger = logging.getLogger(__name__)
CANVAS_SIZE = 1200
CUTOUT_MAX_SIDE = 1600
PADDING = 0.1
ALPHA_THRESHOLD = 128
_session = None
def _rembg_session():
global _session
if _session is None:
from rembg import new_session
_session = new_session("u2net")
return _session
def open_image(data: bytes) -> Image.Image:
image = Image.open(BytesIO(data))
image.load()
image = ImageOps.exif_transpose(image) or image
if getattr(image, "n_frames", 1) > 1:
image.seek(0)
image = image.copy()
return image
def downscale(image: Image.Image, max_side: int) -> Image.Image:
width, height = image.size
longest = max(width, height)
if longest <= max_side:
return image
scale = max_side / longest
return image.resize(
(max(1, int(width * scale)), max(1, int(height * scale))),
Image.Resampling.LANCZOS,
)
def harden_cutout(image: Image.Image, threshold: int = ALPHA_THRESHOLD) -> Image.Image:
"""Drop faint shadow/halo pixels rembg leaves around the subject."""
rgba = image.convert("RGBA")
red, green, blue, alpha = rgba.split()
alpha = alpha.point(lambda value: value if value >= threshold else 0)
return Image.merge("RGBA", (red, green, blue, alpha))
def cutout_subject(image: Image.Image) -> Image.Image:
"""Knock out the background. Falls back to the original on failure."""
rgba = downscale(image.convert("RGBA"), CUTOUT_MAX_SIDE)
try:
from rembg import remove
buf = BytesIO()
rgba.save(buf, format="PNG")
result = remove(
buf.getvalue(),
session=_rembg_session(),
post_process_mask=True,
)
return harden_cutout(Image.open(BytesIO(result)).convert("RGBA"))
except Exception:
logger.exception("product photo cutout failed; using original")
return rgba
def center_on_square(
image: Image.Image,
*,
size: int = CANVAS_SIZE,
padding: float = PADDING,
) -> Image.Image:
rgba = image.convert("RGBA")
alpha = rgba.getchannel("A")
mask = alpha.point(lambda value: 255 if value > ALPHA_THRESHOLD else 0)
bbox = mask.getbbox()
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
if bbox is None:
return canvas
cropped = rgba.crop(bbox)
max_inner = max(1, int(size * (1 - 2 * padding)))
width, height = cropped.size
scale = min(max_inner / width, max_inner / height)
new_w = max(1, int(width * scale))
new_h = max(1, int(height * scale))
resized = cropped.resize((new_w, new_h), Image.Resampling.LANCZOS)
x = (size - new_w) // 2
y = (size - new_h) // 2
canvas.paste(resized, (x, y), resized)
return canvas
def encode_png(image: Image.Image) -> bytes:
buf = BytesIO()
image.save(buf, format="PNG", optimize=True)
return buf.getvalue()
def prepare_product_photo(data: bytes) -> tuple[bytes, str]:
"""Return a PNG cutout on a square canvas, plus content type."""
image = open_image(data)
cut = cutout_subject(image)
framed = center_on_square(cut)
return encode_png(framed), "image/png"
def product_image_filename(original: str) -> str:
stem = PurePosixPath(original or "product").stem.strip() or "product"
return f"{stem[:200]}.png"