#!/usr/bin/env python3 """Rend UNE carte de stat KA (1080x1350) à partir d'un insight JSON sur stdin. Style maison Groupe KA : charbon + accent du site + crème. Nombre-héros, 3 tuiles, et barres à étiquettes directes si l'insight en fournit. Imprime le chemin du PNG produit sur stdout. """ import json, sys, os, datetime from PIL import Image, ImageDraw, ImageFont CHARCOAL = (20, 24, 20) CHARCOAL2 = (30, 36, 30) CREAM = (242, 241, 234) GRAY = (150, 156, 148) RING = (46, 54, 46) BLACK_F = "/System/Library/Fonts/Supplemental/Arial Black.ttf" BOLD_F = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" MONO_F = "/System/Library/Fonts/Supplemental/Courier New Bold.ttf" W, H = 1080, 1350 def hex2rgb(h): h = h.lstrip("#") return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) def f(p, s): return ImageFont.truetype(p, s) def fit(d, text, path, size, maxw, floor=40): while size > floor: fo = f(path, size) if d.textlength(text, font=fo) <= maxw: return fo size -= 4 return f(path, size) def wrap(d, text, font, maxw, maxlines=2): words, lines, cur = text.split(), [], "" for w in words: probe = (cur + " " + w).strip() if d.textlength(probe, font=font) <= maxw: cur = probe else: lines.append(cur); cur = w lines.append(cur) return lines[:maxlines] def render(ins, out_dir): accent = hex2rgb(ins["accent"]) domain = "www." + ins["site"] + ".com" img = Image.new("RGB", (W, H), CHARCOAL) d = ImageDraw.Draw(img) d.rounded_rectangle([16, 16, W - 16, H - 16], radius=26, outline=RING, width=3) # Eyebrow avec la date du jour today = datetime.date.today() mois = ["janvier","février","mars","avril","mai","juin","juillet","août", "septembre","octobre","novembre","décembre"][today.month - 1] d.line([64, 92, 112, 92], fill=accent, width=6) d.text((130, 92), f"{ins['label'].upper()} · STAT DU JOUR · {today.day} {mois.upper()} {today.year}", font=f(BOLD_F, 25), fill=accent, anchor="lm") # Boîte KA inclinée box = Image.new("RGBA", (150, 96), (0, 0, 0, 0)) bd = ImageDraw.Draw(box) bd.rounded_rectangle([0, 0, 149, 95], radius=20, fill=accent) bd.text((75, 48), "KA", font=f(BLACK_F, 58), fill=CHARCOAL, anchor="mm") box = box.rotate(-4, expand=True, resample=Image.BICUBIC) img.paste(box, (W - 64 - box.width, 52), box) # Nom + tagline d.text((64, 165), ins["label"], font=f(BLACK_F, 88), fill=CREAM) if ins.get("tagline"): d.text((66, 292), ins["tagline"], font=f(BOLD_F, 31), fill=GRAY) # Héros hf = fit(d, ins["headline"], BLACK_F, 176, W - 128) d.text((60, 372), ins["headline"], font=hf, fill=accent) hl = wrap(d, ins["headline_label"], f(BOLD_F, 40), W - 128, 2) for i, ln in enumerate(hl): d.text((66, 372 + hf.size + 44 + i * 46), ln, font=f(BOLD_F, 40), fill=CREAM) y = 372 + hf.size + 44 + len(hl) * 46 + 40 # Tuiles tiles = ins.get("tiles") or [] if tiles: d.line([64, y, W - 64, y], fill=RING, width=2) y += 26 col = (W - 128) / len(tiles) for i, (val, lab) in enumerate(tiles): x = 64 + int(i * col) vf = fit(d, str(val), BLACK_F, 52, col - 24) d.text((x, y), str(val), font=vf, fill=CREAM) for j, ln in enumerate(wrap(d, lab, f(BOLD_F, 25), col - 24, 2)): d.text((x, y + 70 + j * 32), ln, font=f(BOLD_F, 25), fill=GRAY) y += 150 # Barres (optionnelles) bars = ins.get("bars") if bars: maxv = max(b[1] for b in bars) or 1 avail = (1150 if not ins.get("note") else 1120) - y rowh = min(avail / len(bars), 118) for i, b in enumerate(bars): label, v, vtxt = b[0], b[1], b[2] sub = b[3] if len(b) > 3 else None yy = y + int(i * rowh) d.text((64, yy), str(label), font=f(BOLD_F, 31), fill=CREAM) d.text((W - 64, yy), str(vtxt), font=f(BLACK_F, 32), fill=accent, anchor="ra") if sub: d.text((64, yy + 42), sub, font=f(BOLD_F, 24), fill=GRAY) by = yy + (78 if sub else 46) d.rounded_rectangle([64, by, W - 64, by + 15], radius=7, fill=CHARCOAL2) fw = max(18, int((W - 128) * v / maxv)) d.rounded_rectangle([64, by, 64 + fw, by + 15], radius=7, fill=accent) if ins.get("note"): d.text((64, 1150), ins["note"], font=f(BOLD_F, 27), fill=GRAY) # Bandeau bas d.rounded_rectangle([16, 1246, W - 16, H - 16], radius=26, fill=accent) d.text((64, 1290), domain, font=f(MONO_F, 40), fill=CHARCOAL, anchor="lm") d.text((W - 64, 1290), "GROUPE ·KA", font=f(BLACK_F, 34), fill=CHARCOAL, anchor="rm") os.makedirs(out_dir, exist_ok=True) path = os.path.join(out_dir, f"card-{ins['site']}-{ins['insight_id']}.png") img.save(path) return path if __name__ == "__main__": ins = json.load(sys.stdin) out_dir = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ka-social-cards" print(render(ins, out_dir))