feat: add card-renderer internal service (v1)
- 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
This commit is contained in:
38
card-renderer/app/crop.py
Normal file
38
card-renderer/app/crop.py
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
Reference in New Issue
Block a user