Files
vision/card-renderer/app/crop.py
Gregor Klevze 58ee1b3bdd 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
2026-03-31 10:39:29 +02:00

39 lines
1.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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