← back to Beverlyhillsvideos

filmgen/make_card.py

69 lines

#!/usr/bin/env python3
"""Render a luxe Beverly Hills title card PNG (1920x1080) for a place."""
import sys, json, textwrap
from PIL import Image, ImageDraw, ImageFont, ImageFilter

W, H = 1920, 1080
INK   = (21, 19, 16)
CREAM = (246, 243, 236)
GOLD  = (184, 147, 90)
MUTED = (150, 143, 130)
LINE  = (70, 66, 60)

SERIF = "/System/Library/Fonts/Supplemental/Didot.ttc"      # display
SANS  = "/System/Library/Fonts/Supplemental/Futura.ttc"     # eyebrow / meta

def font(path, size, idx=0):
    return ImageFont.truetype(path, size, index=idx)

def tracked(draw, xy, text, fnt, fill, track, anchor_center_x):
    """Draw letter-spaced text centered on anchor_center_x at y=xy[1]."""
    widths = [draw.textlength(c, font=fnt) for c in text]
    total = sum(widths) + track * (len(text) - 1)
    x = anchor_center_x - total / 2
    y = xy[1]
    for c, w in zip(text, widths):
        draw.text((x, y), c, font=fnt, fill=fill)
        x += w + track

def center(draw, y, text, fnt, fill):
    w = draw.textlength(text, font=fnt)
    draw.text((W/2 - w/2, y), text, font=fnt, fill=fill)
    return w

def render(place, out):
    name = place["name"]; cat = place.get("category",""); addr = place.get("address","")
    img = Image.new("RGB", (W, H), INK)
    d = ImageDraw.Draw(img)
    # vignette
    vig = Image.new("L", (W, H), 0); vd = ImageDraw.Draw(vig)
    vd.ellipse([-W*0.3, -H*0.3, W*1.3, H*1.3], fill=90)
    vig = vig.filter(ImageFilter.GaussianBlur(220))
    dark = Image.new("RGB", (W, H), (8,7,6))
    img = Image.composite(img, dark, vig)
    d = ImageDraw.Draw(img)
    # eyebrow (category, tracked, gold)
    eb = font(SANS, 34)
    tracked(d, (0, H*0.30), cat.upper(), eb, GOLD, 10, W/2)
    # title (Didot, wrap long names)
    lines = textwrap.wrap(name, width=18) or [name]
    tsize = 150 if len(lines) == 1 and len(name) <= 14 else (120 if len(lines)==1 else 104)
    tf = font(SERIF, tsize)
    ty = H*0.5 - (len(lines)*tsize*0.62)/2 - 10
    for ln in lines:
        center(d, ty, ln, tf, CREAM); ty += tsize*0.92
    # hairline rule
    ry = H*0.66
    d.line([(W/2-70, ry), (W/2+70, ry)], fill=GOLD, width=2)
    # address (muted sans)
    af = font(SANS, 32)
    center(d, ry+40, addr.replace(", Beverly Hills, CA", "").split(", CA")[0], af, MUTED)
    # wordmark
    wf = font(SANS, 24)
    tracked(d, (0, H-90), "BEVERLY HILLS VIDEOS", wf, MUTED, 8, W/2)
    img.save(out, "PNG")
    print("card ->", out)

if __name__ == "__main__":
    place = json.loads(sys.argv[1]); render(place, sys.argv[2])