- New card-renderer FastAPI service (Python 3.11 + Pillow) - GET /health, GET /templates - POST /render (URL input) - POST /render/file (multipart upload) - POST /render/meta (dry-run layout metadata) - nova-artwork-v1 template: cover crop, gradient overlay, text, logo - SSRF-safe async image fetch with redirect validation - Smart center cover crop isolated for future YOLO focal-point support - Graceful font/logo fallback when assets are absent - docker-compose.yml: add card-renderer service + healthcheck; extend gateway with CARD_RENDERER_URL and depends_on - gateway/main.py: proxy endpoints under /cards/* - GET /cards/templates - POST /cards/render - POST /cards/render/file - POST /cards/render/meta All protected by existing APIKeyMiddleware
39 lines
1.1 KiB
Python
39 lines
1.1 KiB
Python
from __future__ import annotations
|
||
|
||
from PIL import Image
|
||
|
||
|
||
def smart_cover_crop(
|
||
img: Image.Image,
|
||
target_w: int,
|
||
target_h: int,
|
||
) -> tuple[Image.Image, tuple[int, int, int, int]]:
|
||
"""Center-weighted cover crop that fills target_w × target_h exactly.
|
||
|
||
The crop box is returned alongside the cropped image so callers can
|
||
record it for metadata or later focal-point refinement.
|
||
"""
|
||
src_w, src_h = img.size
|
||
target_ratio = target_w / target_h
|
||
src_ratio = src_w / src_h
|
||
|
||
if src_ratio > target_ratio:
|
||
# Image is wider than needed — crop sides
|
||
crop_h = src_h
|
||
crop_w = int(crop_h * target_ratio)
|
||
left = max((src_w - crop_w) // 2, 0)
|
||
top = 0
|
||
else:
|
||
# Image is taller than needed — crop top/bottom
|
||
crop_w = src_w
|
||
crop_h = int(crop_w / target_ratio)
|
||
left = 0
|
||
top = max((src_h - crop_h) // 2, 0)
|
||
|
||
right = min(left + crop_w, src_w)
|
||
bottom = min(top + crop_h, src_h)
|
||
box = (left, top, right, bottom)
|
||
|
||
cropped = img.crop(box).resize((target_w, target_h), Image.LANCZOS)
|
||
return cropped, box
|