🗝 KeyzHub
19Keys · community archive
4270 bytes raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
"""Make a transparent 1080x1920 overlay PNG: a clean ROUNDED box (black, white text)
holding a full multi-line quote, positioned UNDER the subject's face, + a cursive
signature top-right. Overlay it on a 9:16 video with ffmpeg for a quote reel.

Usage: quote_reel_overlay.py <out.png> "<quote>" [--cy 1080] [--sig "19 Keys"]
       [--box dark|light] [--size 42] [--width 900]
The quote is wrapped automatically; force line breaks with " | ".
"""
import argparse
import os
import subprocess
from PIL import Image, ImageDraw, ImageFont, ImageFilter

W, H = 1080, 1920
TEXT_FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",
              "/Library/Fonts/Arial Bold.ttf", "/System/Library/Fonts/HelveticaNeue.ttc"]
SIG_FONTS = ["/System/Library/Fonts/SnellRoundhand.ttc",
             "/System/Library/Fonts/Supplemental/SnellRoundhand.ttc"]


def load(paths, size):
    for p in paths:
        try:
            return ImageFont.truetype(p, size)
        except Exception:
            continue
    return ImageFont.load_default()


def resolve_font_file(spec):
    """spec is a font FILE path OR a family NAME. Return a usable .ttf/.otf path or None.
    PIL needs a file — a family name is resolved to its file via fc-match (fontconfig)."""
    if not spec:
        return None
    if os.path.isfile(spec):
        return spec
    for fc in ("fc-match", "/opt/homebrew/bin/fc-match"):
        try:
            r = subprocess.run([fc, "-f", "%{file}", spec],
                               capture_output=True, text=True, timeout=10)
            p = (r.stdout or "").strip()
            if p and os.path.isfile(p):
                return p
        except Exception:
            continue
    return None


def wrap(draw, text, font, maxw):
    out = []
    for para in text.split("|"):
        words, cur = para.strip().split(), ""
        for w in words:
            t = (cur + " " + w).strip()
            if draw.textlength(t, font=font) <= maxw:
                cur = t
            else:
                out.append(cur); cur = w
        if cur:
            out.append(cur)
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("out")
    ap.add_argument("quote")
    ap.add_argument("--cy", type=int, default=1090)   # vertical center of the box (under face)
    ap.add_argument("--sig", default="19 Keys")
    ap.add_argument("--box", default="dark", choices=["dark", "light"])
    ap.add_argument("--size", type=int, default=42)
    ap.add_argument("--width", type=int, default=900)  # max text width before wrap
    ap.add_argument("--radius", type=int, default=28)  # box corner radius (0 = sharp/square)
    ap.add_argument("--font", default=None)            # family name OR file path for the quote text
    a = ap.parse_args()

    boxc = (0, 0, 0, 255) if a.box == "dark" else (255, 255, 255, 255)
    txtc = (255, 255, 255, 255) if a.box == "dark" else (17, 17, 17, 255)

    img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    d = ImageDraw.Draw(img)
    ff = resolve_font_file(a.font)
    font = load([ff] + TEXT_FONTS if ff else TEXT_FONTS, a.size)
    lines = wrap(d, a.quote, font, a.width)
    lh = a.size + 18
    th = lh * len(lines)
    tw = max(d.textlength(ln, font=font) for ln in lines)
    padx, pady = 46, 40
    bw, bh = tw + 2 * padx, th + 2 * pady
    bx, by = (W - bw) // 2, a.cy - bh // 2
    # soft drop shadow for a clean lift
    sh = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    ImageDraw.Draw(sh).rounded_rectangle([bx, by + 10, bx + bw, by + bh + 10], radius=a.radius, fill=(0, 0, 0, 110))
    img.alpha_composite(sh.filter(ImageFilter.GaussianBlur(16)))
    d = ImageDraw.Draw(img)
    d.rounded_rectangle([bx, by, bx + bw, by + bh], radius=a.radius, fill=boxc)
    ty = by + pady
    for ln in lines:
        w = d.textlength(ln, font=font)
        d.text(((W - w) // 2, ty), ln, font=font, fill=txtc)
        ty += lh
    # cursive signature, top-right
    sig = load(SIG_FONTS, 74)
    sw = d.textlength(a.sig, font=sig)
    d.text((W - sw - 58, 64), a.sig, font=sig, fill=(255, 255, 255, 240),
           stroke_width=1, stroke_fill=(0, 0, 0, 120))
    img.save(a.out)
    print(f"{a.out}: {len(lines)} lines, box {bw}x{bh}")


if __name__ == "__main__":
    main()