2026-10-05 01:36:05 +08:00
|
|
|
"""Native frame image with a separate, high-contrast caption area."""
|
|
|
|
|
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
|
|
|
|
from pigway_frame_flow.renderers.images import convert
|
|
|
|
|
|
|
|
|
|
|
2026-10-05 22:44:41 +08:00
|
|
|
def render_captioned_photo(
|
|
|
|
|
data, caption, font_loader, fit="contain", profile="color", *, size=(1200, 1600)
|
|
|
|
|
):
|
|
|
|
|
width, height = size
|
|
|
|
|
landscape = width > height
|
|
|
|
|
caption_height = 240 if landscape else 320
|
|
|
|
|
photo_height = height - caption_height
|
|
|
|
|
photo_data = convert(data, fit, profile, size=(width, photo_height))
|
|
|
|
|
canvas = Image.new("RGB", size, "white")
|
2026-10-05 01:36:05 +08:00
|
|
|
with Image.open(BytesIO(photo_data)) as photo:
|
|
|
|
|
canvas.paste(photo, (0, 0))
|
|
|
|
|
draw = ImageDraw.Draw(canvas)
|
2026-10-05 22:44:41 +08:00
|
|
|
rows = (
|
|
|
|
|
[(24, 44), (88, 36), (140, 34)]
|
|
|
|
|
if landscape
|
|
|
|
|
else [(40, 52), (130, 40), (200, 38)]
|
|
|
|
|
)
|
|
|
|
|
values = [caption["title"], caption["date"], caption.get("place", "")]
|
|
|
|
|
for text, (offset, font_size) in zip(values, rows, strict=True):
|
|
|
|
|
if not text:
|
|
|
|
|
continue
|
|
|
|
|
font = font_loader(font_size)
|
|
|
|
|
while font_size > 24 and draw.textlength(text, font=font) > width - 120:
|
|
|
|
|
font_size -= 2
|
|
|
|
|
font = font_loader(font_size)
|
|
|
|
|
while draw.textlength(text, font=font) > width - 120 and text:
|
2026-10-05 01:36:05 +08:00
|
|
|
text = text[:-2] + "…"
|
2026-10-05 22:44:41 +08:00
|
|
|
draw.text(
|
|
|
|
|
(width / 2, photo_height + offset),
|
|
|
|
|
text,
|
|
|
|
|
font=font,
|
|
|
|
|
fill="black",
|
|
|
|
|
anchor="mt",
|
|
|
|
|
)
|
2026-10-05 01:36:05 +08:00
|
|
|
out = BytesIO()
|
|
|
|
|
canvas.save(out, "JPEG", quality=93, optimize=True)
|
|
|
|
|
return out.getvalue()
|