Newer
Older
veriq / backend / og.py
"""VerIQ — OG image generation (1200x630 PNG cards for social scrapers).

Scrapers (X/Twitter, Telegram, Facebook) don't render SVG and need a real
PNG. We draw the card with Pillow: dark brutalist background, big IQ number,
percentile line, signature badge. Cached on disk per result_id.
"""
import os
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

CACHE_DIR = Path(os.environ.get("VERIQ_OG_CACHE", "/var/lib/veriq/og_cache"))

W, H = 1200, 630
BG = (13, 13, 15)
LINE = (42, 42, 51)
TEXT = (232, 232, 234)
MUTED = (138, 138, 148)
ACCENT = (122, 162, 247)
GOOD = (158, 206, 106)
BAD = (247, 118, 142)
WARN = (224, 175, 104)

_FONT_DIRS = [
    "/usr/share/fonts/truetype/dejavu/",
    "/usr/share/fonts/dejavu/",
]


def _font(name, size):
    for d in _FONT_DIRS:
        p = Path(d) / name
        if p.exists():
            return ImageFont.truetype(str(p), size)
    return ImageFont.load_default()


def _fonts():
    return {
        "logo": _font("DejaVuSans-Bold.ttf", 44),
        "big": _font("DejaVuSans-Bold.ttf", 240),
        "sub": _font("DejaVuSans.ttf", 40),
        "small": _font("DejaVuSans.ttf", 28),
        "badge": _font("DejaVuSans-Bold.ttf", 30),
    }


def _center(draw, text, y, font, fill):
    bbox = draw.textbbox((0, 0), text, font=font)
    w = bbox[2] - bbox[0]
    draw.text(((W - w) / 2, y), text, font=font, fill=fill)


def generate_card(iq, percentile, signature_valid, flagged, result_id):
    """Render the OG card, return path to cached PNG."""
    try:
        CACHE_DIR.mkdir(parents=True, exist_ok=True)
        cache = CACHE_DIR
    except PermissionError:
        import tempfile
        cache = Path(tempfile.gettempdir()) / "veriq_og"
        cache.mkdir(parents=True, exist_ok=True)
    out = cache / (result_id + ".png")
    if out.exists():
        return out

    img = Image.new("RGB", (W, H), BG)
    d = ImageDraw.Draw(img)
    f = _fonts()

    # frame
    d.rectangle([8, 8, W - 9, H - 9], outline=LINE, width=2)
    # accent corner blocks (brutalist)
    d.rectangle([8, 8, 8 + 56, 8 + 10], fill=ACCENT)
    d.rectangle([8, 8, 8 + 10, 8 + 56], fill=ACCENT)
    d.rectangle([W - 9 - 56, H - 9 - 10, W - 9, H - 9], fill=ACCENT)
    d.rectangle([W - 9 - 10, H - 9 - 56, W - 9, H - 9], fill=ACCENT)

    # logo
    d.text((48, 44), "Ver", font=f["logo"], fill=TEXT)
    logo_w = d.textbbox((0, 0), "Ver", font=f["logo"])[2]
    d.text((48 + logo_w, 44), "IQ", font=f["logo"], fill=ACCENT)

    # big score
    _center(d, str(iq), 130, f["big"], ACCENT)

    # percentile line
    _center(d, "higher than " + str(percentile) + "% of test takers", 400,
            f["sub"], TEXT)

    # badges row
    y = 480
    if signature_valid:
        badge_text = "SIGNATURE VALID"
        badge_color = GOOD
    else:
        badge_text = "SIGNATURE INVALID"
        badge_color = BAD
    bbox = d.textbbox((0, 0), badge_text, font=f["badge"])
    bw = bbox[2] - bbox[0] + 48
    bx = (W - bw) / 2
    d.rectangle([bx, y, bx + bw, y + 56], outline=badge_color, width=3)
    d.text((bx + 24, y + 10), badge_text, font=f["badge"], fill=badge_color)
    if flagged:
        fx = bx + bw + 16
        fb = d.textbbox((0, 0), "FLAGGED", font=f["badge"])
        fw = fb[2] - fb[0] + 40
        d.rectangle([fx, y, fx + fw, y + 56], outline=WARN, width=3)
        d.text((fx + 20, y + 10), "FLAGGED", font=f["badge"], fill=WARN)

    # footer
    _center(d, "veriq · verify at /v/" + result_id + " · signed with Ed25519",
            560, f["small"], MUTED)

    img.save(out, "PNG", optimize=True)
    return out