| 2 |
2 |
"""Rendu d'images sociales KA haute qualité via Chrome headless (HTML/CSS). |
| 3 |
3 |
|
| 4 |
4 |
Lit un JSON « creative » sur stdin et produit une image 1080x1350 spectaculaire. |
| 5 |
|
−Plusieurs « genres » de mise en page, dégradés par site, photos produit réelles. |
| 6 |
5 |
|
| 7 |
6 |
Genres : hero (nombre géant), ranking (palmarès), spotlight (produit vedette photo), |
| 8 |
|
− deal (aubaine prix), pulse (activité live). |
|
7 |
+ list (sélection d'items photo), grid (mosaïque 2x2 de photos), |
|
8 |
+ donut (part en % en anneau SVG), quote (« le saviez-vous » typographique). |
|
9 |
+Thèmes (rotation via `variant` ou hash) : nuit (sombre), creme (clair néo-brutaliste Ka), |
|
10 |
+ cadre (sombre encadré). Les genres photo (spotlight/list/grid) restent en nuit. |
| 9 |
11 |
Sortie : chemin du PNG sur stdout. |
| 10 |
12 |
""" |
| 11 |
|
−import json, sys, os, base64, datetime, urllib.request, ssl, hashlib, subprocess, glob |
|
13 |
+import json, sys, os, re, base64, datetime, urllib.request, ssl, hashlib, subprocess, glob |
| 12 |
14 |
|
| 13 |
15 |
CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE |
| 14 |
16 |
|
| 23 |
25 |
MOIS = ["janvier","février","mars","avril","mai","juin","juillet","août", |
| 24 |
26 |
"septembre","octobre","novembre","décembre"] |
| 25 |
27 |
|
| 26 |
|
−# palettes de dégradé par site (2 arrêts sombres tintés + couleur d'accent vive) |
|
28 |
+# palettes par site : accent vif (a), accent secondaire (b), dégradé sombre (g1,g2), |
|
29 |
+# accent assombri lisible sur fond clair (ad) |
| 27 |
30 |
PALETTES = { |
| 28 |
|
− "lou-ka": {"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"}, |
| 29 |
|
− "immo-ka": {"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b"}, |
| 30 |
|
− "food-ka": {"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b"}, |
| 31 |
|
− "fabri-ka": {"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09"}, |
| 32 |
|
− "resto-ka": {"a":"#f08c00","b":"#ffb84d","g1":"#241804","g2":"#0d0b08"}, |
| 33 |
|
− "auto-ka": {"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09"}, |
| 34 |
|
− "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08"}, |
| 35 |
|
− "trouve-ka":{"a":"#d9f26b","b":"#8fd3ff","g1":"#0a1a20","g2":"#080b0d"}, |
| 36 |
|
− "sorti-ka": {"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c"}, |
| 37 |
|
− "crea-ka": {"a":"#d9f26b","b":"#c08bff","g1":"#160a24","g2":"#0a090d"}, |
| 38 |
|
− "job-ka": {"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"}, |
|
31 |
+ "lou-ka": {"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09","ad":"#c24f00"}, |
|
32 |
+ "immo-ka": {"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b","ad":"#b02532"}, |
|
33 |
+ "food-ka": {"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b","ad":"#157a41"}, |
|
34 |
+ "fabri-ka": {"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09","ad":"#9c3f21"}, |
|
35 |
+ "resto-ka": {"a":"#f08c00","b":"#ffb84d","g1":"#241804","g2":"#0d0b08","ad":"#b96c00"}, |
|
36 |
+ "auto-ka": {"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09","ad":"#cc3f14"}, |
|
37 |
+ "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08","ad":"#6b7f1d"}, |
|
38 |
+ "trouve-ka":{"a":"#d9f26b","b":"#8fd3ff","g1":"#0a1a20","g2":"#080b0d","ad":"#6b7f1d"}, |
|
39 |
+ "sorti-ka": {"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c","ad":"#6b7f1d"}, |
|
40 |
+ "crea-ka": {"a":"#d9f26b","b":"#c08bff","g1":"#160a24","g2":"#0a090d","ad":"#6b7f1d"}, |
|
41 |
+ "job-ka": {"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d","ad":"#6b7f1d"}, |
| 39 |
42 |
} |
| 40 |
43 |
|
| 41 |
44 |
def pal(site): return PALETTES.get(site, PALETTES["vrai-prix"]) |
| 58 |
61 |
t = datetime.date.today() |
| 59 |
62 |
return f"{t.day} {MOIS[t.month-1]} {t.year}" |
| 60 |
63 |
|
|
64 |
+def parse_pct(h): |
|
65 |
+ m = re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip()) |
|
66 |
+ return float(m.group(1).replace(",", ".")) if m else None |
|
67 |
+ |
| 61 |
68 |
FONT = "'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif" |
|
69 |
+THEMES = ["nuit","creme","cadre"] |
| 62 |
70 |
|
| 63 |
|
−def base_css(p): |
| 64 |
|
− return f""" |
| 65 |
|
− *{{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased;}} |
| 66 |
|
− html,body{{width:1080px;height:1350px;overflow:hidden;font-family:{FONT};color:#fff;}} |
| 67 |
|
− .stage{{position:relative;width:1080px;height:1350px; |
| 68 |
|
− background: |
| 69 |
|
− radial-gradient(1200px 700px at 82% -6%, {p['a']}44 0%, transparent 55%), |
| 70 |
|
− radial-gradient(900px 900px at -12% 108%, {p['b']}33 0%, transparent 55%), |
| 71 |
|
− linear-gradient(160deg, {p['g1']} 0%, {p['g2']} 100%);}} |
| 72 |
|
− .grain{{position:absolute;inset:0;opacity:.045;mix-blend-mode:overlay; |
| 73 |
|
− background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");}} |
| 74 |
|
− .pad{{position:absolute;inset:0;padding:70px 72px 250px;display:flex;flex-direction:column;}} |
| 75 |
|
− .eyebrow{{display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em; |
| 76 |
|
− font-size:22px;text-transform:uppercase;color:{p['a']};}} |
| 77 |
|
− .eyebrow .dot{{width:40px;height:5px;border-radius:3px;background:{p['a']};}} |
| 78 |
|
− .kabadge{{position:absolute;top:60px;right:72px;background:{p['a']};color:#0d0d0d; |
| 79 |
|
− font-weight:900;font-size:40px;padding:8px 20px;border-radius:16px;transform:rotate(-4deg); |
| 80 |
|
− box-shadow:0 10px 30px {p['a']}55;}} |
| 81 |
|
− .site{{font-size:88px;font-weight:900;letter-spacing:-.02em;margin-top:26px;}} |
| 82 |
|
− .tag{{font-size:31px;font-weight:600;color:#ffffffb0;margin-top:6px;}} |
| 83 |
|
− .foot{{position:absolute;left:72px;right:72px;bottom:66px;display:flex;justify-content:space-between; |
| 84 |
|
− align-items:center;font-weight:800;}} |
| 85 |
|
− .foot .url{{font-size:34px;color:{p['a']};letter-spacing:.01em;}} |
| 86 |
|
− .foot .brand{{font-size:26px;color:#ffffff88;letter-spacing:.12em;}} |
| 87 |
|
− .sig{{position:absolute;left:72px;bottom:150px;font-size:20px;color:#ffffff66;font-weight:600; |
| 88 |
|
− display:flex;align-items:center;gap:8px;}} |
| 89 |
|
− .glass{{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12); |
| 90 |
|
− border-radius:22px;backdrop-filter:blur(8px);}} |
| 91 |
|
− """ |
|
71 |
+def theme_of(ins): |
|
72 |
+ t = ins.get("theme") |
|
73 |
+ if t in THEMES: return t |
|
74 |
+ v = ins.get("variant") |
|
75 |
+ if v is None: |
|
76 |
+ v = int(hashlib.md5((ins["site"]+str(ins.get("insight_id",""))).encode()).hexdigest(), 16) |
|
77 |
+ return THEMES[int(v) % len(THEMES)] |
| 92 |
78 |
|
| 93 |
|
−def hdr(ins, p): |
| 94 |
|
− return f""" |
| 95 |
|
− <div class="eyebrow"><span class="dot"></span>{esc(ins['label'].upper())} · STAT DU JOUR · {today_str().upper()}</div> |
| 96 |
|
− <div class="kabadge">KA</div> |
| 97 |
|
− <div class="site">{esc(ins['label'])}</div> |
| 98 |
|
− {f'<div class="tag">{esc(ins.get("tagline",""))}</div>' if ins.get('tagline') else ''} |
|
79 |
+def tokens(p, theme): |
|
80 |
+ if theme == "creme": |
|
81 |
+ return { |
|
82 |
+ "fg":"#141814","muted":"#141814b3","muted2":"#14181487","sig":"#14181466", |
|
83 |
+ "acc":p["ad"], |
|
84 |
+ "glass":"background:#ffffff;border:3px solid #141814;box-shadow:9px 9px 0 #141814;", |
|
85 |
+ "badge_extra":"border:3px solid #141814;box-shadow:7px 7px 0 #141814;", |
|
86 |
+ "numgrad":"background:linear-gradient(180deg,#141814 0%,"+p["ad"]+" 150%);", |
|
87 |
+ "numglow":"filter:drop-shadow(0 8px 22px "+p["a"]+"40);", |
|
88 |
+ "track":"#1418141f", |
|
89 |
+ "grain_op":".05", |
|
90 |
+ "stage":("background:radial-gradient(1100px 680px at 85% -8%, "+p["a"]+"30 0%, transparent 55%)," |
|
91 |
+ "radial-gradient(900px 900px at -10% 112%, "+p["b"]+"26 0%, transparent 55%)," |
|
92 |
+ "linear-gradient(168deg,#f7f6ef 0%,#edece4 100%);"), |
|
93 |
+ "frame":"", |
|
94 |
+ } |
|
95 |
+ t = { |
|
96 |
+ "fg":"#ffffff","muted":"#ffffffb0","muted2":"#ffffff8c","sig":"#ffffff66", |
|
97 |
+ "acc":p["a"], |
|
98 |
+ "glass":"background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(8px);", |
|
99 |
+ "badge_extra":"box-shadow:0 10px 30px "+p["a"]+"55;", |
|
100 |
+ "numgrad":"background:linear-gradient(180deg,#fff 0%,"+p["a"]+" 130%);", |
|
101 |
+ "numglow":"filter:drop-shadow(0 12px 40px "+p["a"]+"44);", |
|
102 |
+ "track":"rgba(255,255,255,.09)", |
|
103 |
+ "grain_op":".045", |
|
104 |
+ "stage":("background:radial-gradient(1200px 700px at 82% -6%, "+p["a"]+"44 0%, transparent 55%)," |
|
105 |
+ "radial-gradient(900px 900px at -12% 108%, "+p["b"]+"33 0%, transparent 55%)," |
|
106 |
+ "linear-gradient(160deg, "+p["g1"]+" 0%, "+p["g2"]+" 100%);"), |
|
107 |
+ "frame":"", |
|
108 |
+ } |
|
109 |
+ if theme == "cadre": |
|
110 |
+ t["stage"] = ("background:radial-gradient(1100px 800px at 50% -12%, "+p["a"]+"3a 0%, transparent 60%)," |
|
111 |
+ "radial-gradient(800px 800px at 100% 110%, "+p["b"]+"2a 0%, transparent 55%)," |
|
112 |
+ "linear-gradient(200deg, "+p["g1"]+" 0%, "+p["g2"]+" 100%);") |
|
113 |
+ t["frame"] = (".stage::before{content:'';position:absolute;inset:26px;border:2px solid "+p["a"]+"66;" |
|
114 |
+ "border-radius:36px;pointer-events:none;z-index:5}" |
|
115 |
+ ".stage::after{content:'';position:absolute;inset:38px;border:1px solid #ffffff1c;" |
|
116 |
+ "border-radius:28px;pointer-events:none;z-index:5}") |
|
117 |
+ return t |
|
118 |
+ |
|
119 |
+def base_css(p, T): |
|
120 |
+ css = """ |
|
121 |
+ *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased;} |
|
122 |
+ html,body{width:1080px;height:1350px;overflow:hidden;font-family:__FONT__;color:__FG__;} |
|
123 |
+ .stage{position:relative;width:1080px;height:1350px;__STAGE__} |
|
124 |
+ __FRAME__ |
|
125 |
+ .grain{position:absolute;inset:0;opacity:__GOP__;mix-blend-mode:overlay; |
|
126 |
+ background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");} |
|
127 |
+ .pad{position:absolute;inset:0;padding:70px 72px 250px;display:flex;flex-direction:column;z-index:2;} |
|
128 |
+ .eyebrow{display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em; |
|
129 |
+ font-size:22px;text-transform:uppercase;color:__ACC__;} |
|
130 |
+ .eyebrow .dot{width:40px;height:5px;border-radius:3px;background:__ACC__;} |
|
131 |
+ .kabadge{position:absolute;top:60px;right:72px;background:__A__;color:#0d0d0d; |
|
132 |
+ font-weight:900;font-size:40px;padding:8px 20px;border-radius:16px;transform:rotate(-4deg);z-index:6;__BADGE__} |
|
133 |
+ .site{font-size:88px;font-weight:900;letter-spacing:-.02em;margin-top:26px;} |
|
134 |
+ .tag{font-size:31px;font-weight:600;color:__MUTED__;margin-top:6px;} |
|
135 |
+ .foot{position:absolute;left:72px;right:72px;bottom:66px;display:flex;justify-content:space-between; |
|
136 |
+ align-items:center;font-weight:800;z-index:6;} |
|
137 |
+ .foot .url{font-size:34px;color:__ACC__;letter-spacing:.01em;} |
|
138 |
+ .foot .brand{font-size:26px;color:__MUTED2__;letter-spacing:.12em;} |
|
139 |
+ .sig{position:absolute;left:72px;bottom:150px;font-size:20px;color:__SIG__;font-weight:600; |
|
140 |
+ display:flex;align-items:center;gap:8px;z-index:6;} |
|
141 |
+ .glass{border-radius:22px;__GLASS__} |
| 99 |
142 |
""" |
|
143 |
+ return (css.replace("__FONT__",FONT).replace("__FG__",T["fg"]).replace("__STAGE__",T["stage"]) |
|
144 |
+ .replace("__FRAME__",T["frame"]).replace("__GOP__",T["grain_op"]) |
|
145 |
+ .replace("__MUTED2__",T["muted2"]).replace("__MUTED__",T["muted"]).replace("__SIG__",T["sig"]) |
|
146 |
+ .replace("__GLASS__",T["glass"]).replace("__BADGE__",T["badge_extra"]) |
|
147 |
+ .replace("__ACC__",T["acc"]).replace("__A__",p["a"])) |
|
148 |
+ |
|
149 |
+def hdr(ins, p, kicker="STAT DU JOUR"): |
|
150 |
+ return ('<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · ' |
|
151 |
+ + esc(kicker)+' · '+today_str().upper()+'</div>' |
|
152 |
+ + '<div class="kabadge">KA</div>' |
|
153 |
+ + '<div class="site">'+esc(ins["label"])+'</div>' |
|
154 |
+ + (('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else '')) |
| 100 |
155 |
|
| 101 |
156 |
def footer(ins, p): |
| 102 |
157 |
url = "www." + ins["site"] + ".com" |
| 103 |
|
− return f""" |
| 104 |
|
− <div class="sig">✶ Rédigé et publié par l'Agent KA</div> |
| 105 |
|
− <div class="foot"><div class="url">{url}</div><div class="brand">GROUPE ·KA</div></div> |
| 106 |
|
− """ |
|
158 |
+ return ('<div class="sig">✶ Rédigé et publié par l\'Agent KA</div>' |
|
159 |
+ + '<div class="foot"><div class="url">'+url+'</div><div class="brand">GROUPE ·KA</div></div>') |
| 107 |
160 |
|
| 108 |
|
−def tiles_html(tiles, p): |
|
161 |
+def tiles_html(tiles, p, T): |
| 109 |
162 |
cells = "" |
| 110 |
163 |
for v, l in tiles: |
| 111 |
|
− cells += f"""<div class="glass tile"><div class="tv">{esc(v)}</div><div class="tl">{esc(l)}</div></div>""" |
| 112 |
|
− return f"""<div class="tiles">{cells}</div> |
| 113 |
|
− <style> |
| 114 |
|
− .tiles{{display:grid;grid-template-columns:repeat({len(tiles)},1fr);gap:18px;margin-top:auto;margin-bottom:0;}} |
| 115 |
|
− .tile{{padding:22px 24px;}} |
| 116 |
|
− .tv{{font-size:46px;font-weight:900;color:{p['a']};letter-spacing:-.01em;}} |
| 117 |
|
− .tl{{font-size:23px;font-weight:600;color:#ffffffa0;margin-top:6px;line-height:1.2;}} |
| 118 |
|
− </style>""" |
|
164 |
+ cells += '<div class="glass tile"><div class="tv" data-fit>'+esc(v)+'</div><div class="tl">'+esc(l)+'</div></div>' |
|
165 |
+ css = """ |
|
166 |
+ .tiles{display:grid;grid-template-columns:repeat(__N__,1fr);gap:18px;margin-top:auto;margin-bottom:0;} |
|
167 |
+ .tile{padding:22px 24px;} |
|
168 |
+ .tv{font-size:46px;font-weight:900;color:__ACC__;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;} |
|
169 |
+ .tl{font-size:23px;font-weight:600;color:__MUTED__;margin-top:6px;line-height:1.2;} |
|
170 |
+ """.replace("__N__",str(len(tiles))).replace("__ACC__",T["acc"]).replace("__MUTED__",T["muted"]) |
|
171 |
+ return '<div class="tiles">'+cells+'</div><style>'+css+'</style>' |
| 119 |
172 |
|
| 120 |
|
−def genre_hero(ins, p): |
|
173 |
+def genre_hero(ins, p, T): |
| 121 |
174 |
tiles = ins.get("tiles") or [] |
| 122 |
|
− return f""" |
| 123 |
|
− <div class="pad"> |
| 124 |
|
− {hdr(ins,p)} |
| 125 |
|
− <div class="hero"> |
| 126 |
|
− <div class="num">{esc(ins['headline'])}</div> |
| 127 |
|
− <div class="lab">{esc(ins['headline_label'])}</div> |
| 128 |
|
− </div> |
| 129 |
|
− {tiles_html(tiles,p) if tiles else '<div style="margin-top:auto"></div>'} |
| 130 |
|
− {footer(ins,p)} |
| 131 |
|
− </div> |
| 132 |
|
− <style> |
| 133 |
|
− .hero{{margin-top:70px;}} |
| 134 |
|
− .num{{font-size:230px;font-weight:900;line-height:.9;letter-spacing:-.03em; |
| 135 |
|
− background:linear-gradient(180deg,#fff 0%,{p['a']} 130%);-webkit-background-clip:text; |
| 136 |
|
− -webkit-text-fill-color:transparent;filter:drop-shadow(0 12px 40px {p['a']}44);}} |
| 137 |
|
− .lab{{font-size:44px;font-weight:700;color:#fff;margin-top:20px;max-width:820px;line-height:1.12;}} |
| 138 |
|
− </style>""" |
|
175 |
+ hl = str(ins["headline"]); L = len(hl) |
|
176 |
+ numsize = 150 if L >= 10 else (190 if L >= 8 else 230) |
|
177 |
+ css = """ |
|
178 |
+ .hero{margin-top:70px;} |
|
179 |
+ .num{font-size:__NS__px;font-weight:900;line-height:.9;letter-spacing:-.03em;display:block; |
|
180 |
+ max-width:100%;white-space:nowrap;overflow:hidden; |
|
181 |
+ __NUMGRAD__-webkit-background-clip:text;-webkit-text-fill-color:transparent;__NUMGLOW__} |
|
182 |
+ .lab{font-size:44px;font-weight:700;margin-top:20px;max-width:820px;line-height:1.12;} |
|
183 |
+ """.replace("__NS__",str(numsize)).replace("__NUMGRAD__",T["numgrad"]).replace("__NUMGLOW__",T["numglow"]) |
|
184 |
+ return ('<div class="pad">'+hdr(ins,p) |
|
185 |
+ + '<div class="hero"><div class="num" data-fit>'+esc(hl)+'</div>' |
|
186 |
+ + '<div class="lab">'+esc(ins["headline_label"])+'</div></div>' |
|
187 |
+ + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>') |
|
188 |
+ + footer(ins,p)+'</div><style>'+css+'</style>') |
| 139 |
189 |
|
| 140 |
|
−def genre_ranking(ins, p): |
|
190 |
+def genre_ranking(ins, p, T): |
| 141 |
191 |
bars = ins.get("bars") or [] |
|
192 |
+ # avec sous-textes, 6 rangées débordent sur le pied de page -> max 5 |
|
193 |
+ has_sub = any(len(b) > 3 and b[3] for b in bars) |
|
194 |
+ bars = bars[:5] if has_sub else bars[:6] |
| 142 |
195 |
maxv = max([b[1] for b in bars] or [1]) |
| 143 |
196 |
rows = "" |
| 144 |
197 |
for b in bars: |
| 145 |
198 |
label, val, vtxt = b[0], b[1], b[2] |
| 146 |
199 |
sub = b[3] if len(b) > 3 else "" |
| 147 |
200 |
w = max(8, int(100*val/maxv)) |
| 148 |
|
− rows += f""" |
| 149 |
|
− <div class="row"> |
| 150 |
|
− <div class="rl"><span class="rlab">{esc(label)}</span><span class="rval">{esc(vtxt)}</span></div> |
| 151 |
|
− <div class="track"><div class="fill" style="width:{w}%"></div></div> |
| 152 |
|
− {f'<div class="rsub">{esc(sub)}</div>' if sub else ''} |
| 153 |
|
− </div>""" |
| 154 |
|
− return f""" |
| 155 |
|
− <div class="pad"> |
| 156 |
|
− {hdr(ins,p)} |
| 157 |
|
− <div class="rank-title">{esc(ins['headline_label'])}</div> |
| 158 |
|
− <div class="rows">{rows}</div> |
| 159 |
|
− {footer(ins,p)} |
| 160 |
|
− </div> |
| 161 |
|
− <style> |
| 162 |
|
− .rank-title{{font-size:40px;font-weight:800;color:#fff;margin-top:50px;margin-bottom:8px;}} |
| 163 |
|
− .rows{{margin-top:20px;display:flex;flex-direction:column;gap:30px;}} |
| 164 |
|
− .rl{{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;}} |
| 165 |
|
− .rlab{{font-size:34px;font-weight:700;}} |
| 166 |
|
− .rval{{font-size:36px;font-weight:900;color:{p['a']};}} |
| 167 |
|
− .track{{height:20px;border-radius:12px;background:rgba(255,255,255,.09);overflow:hidden;}} |
| 168 |
|
− .fill{{height:100%;border-radius:12px;background:linear-gradient(90deg,{p['a']},{p['b']}); |
| 169 |
|
− box-shadow:0 4px 18px {p['a']}55;}} |
| 170 |
|
− .rsub{{font-size:22px;color:#ffffff8c;margin-top:8px;font-weight:600;}} |
| 171 |
|
− </style>""" |
|
201 |
+ rows += ('<div class="row"><div class="rl"><span class="rlab">'+esc(label)+'</span>' |
|
202 |
+ + '<span class="rval">'+esc(vtxt)+'</span></div>' |
|
203 |
+ + '<div class="track"><div class="fill" style="width:'+str(w)+'%"></div></div>' |
|
204 |
+ + (('<div class="rsub">'+esc(sub)+'</div>') if sub else '')+'</div>') |
|
205 |
+ css = """ |
|
206 |
+ .rank-title{font-size:40px;font-weight:800;margin-top:50px;margin-bottom:8px;} |
|
207 |
+ .rows{margin-top:20px;display:flex;flex-direction:column;gap:30px;} |
|
208 |
+ .rl{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;} |
|
209 |
+ .rlab{font-size:34px;font-weight:700;} |
|
210 |
+ .rval{font-size:36px;font-weight:900;color:__ACC__;} |
|
211 |
+ .track{height:20px;border-radius:12px;background:__TRACK__;overflow:hidden;} |
|
212 |
+ .fill{height:100%;border-radius:12px;background:linear-gradient(90deg,__ACC__,__B__); |
|
213 |
+ box-shadow:0 4px 18px __A__55;} |
|
214 |
+ .rsub{font-size:22px;color:__MUTED2__;margin-top:8px;font-weight:600;} |
|
215 |
+ """.replace("__ACC__",T["acc"]).replace("__TRACK__",T["track"]).replace("__B__",p["b"]) \ |
|
216 |
+ .replace("__A__",p["a"]).replace("__MUTED2__",T["muted2"]) |
|
217 |
+ return ('<div class="pad">'+hdr(ins,p) |
|
218 |
+ + '<div class="rank-title">'+esc(ins["headline_label"])+'</div>' |
|
219 |
+ + '<div class="rows">'+rows+'</div>'+footer(ins,p)+'</div><style>'+css+'</style>') |
| 172 |
220 |
|
| 173 |
|
−def genre_spotlight(ins, p): |
|
221 |
+def genre_spotlight(ins, p, T): |
| 174 |
222 |
prod = ins.get("product") or {} |
| 175 |
223 |
img = prod.get("image_data") or "" |
| 176 |
|
− chips = "".join(f'<span class="chip">{esc(c)}</span>' for c in (prod.get("meta") or [])) |
|
224 |
+ chips = "".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or [])) |
| 177 |
225 |
price = prod.get("price","") |
| 178 |
|
− return f""" |
| 179 |
|
− <div class="stage-photo" style="background-image:url('{img}')"></div> |
| 180 |
|
− <div class="scrim"></div> |
| 181 |
|
− <div class="pad"> |
| 182 |
|
− <div class="eyebrow"><span class="dot"></span>{esc(ins['label'].upper())} · À DÉCOUVRIR · {today_str().upper()}</div> |
| 183 |
|
− <div class="kabadge">KA</div> |
| 184 |
|
− <div class="spot"> |
| 185 |
|
− <div class="ptitle">{esc(prod.get('title',''))}</div> |
| 186 |
|
− <div class="psub">{esc(prod.get('subtitle',''))}</div> |
| 187 |
|
− <div class="chips">{chips}</div> |
| 188 |
|
− {f'<div class="price">{esc(price)}</div>' if price else ''} |
| 189 |
|
− <div class="tagline2">{esc(ins.get('tagline',''))}</div> |
| 190 |
|
− </div> |
| 191 |
|
− {footer(ins,p)} |
| 192 |
|
− </div> |
| 193 |
|
− <style> |
| 194 |
|
− .stage-photo{{position:absolute;top:0;left:0;right:0;height:760px;background-size:cover;background-position:center;}} |
| 195 |
|
− .scrim{{position:absolute;inset:0;background: |
| 196 |
|
− linear-gradient(180deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,0) 34%, {p['g2']}cc 62%, {p['g2']} 82%);}} |
| 197 |
|
− .spot{{margin-top:auto;}} |
| 198 |
|
− .ptitle{{font-size:62px;font-weight:900;line-height:1.02;letter-spacing:-.02em;max-width:900px; |
| 199 |
|
− text-shadow:0 4px 24px #000a;}} |
| 200 |
|
− .psub{{font-size:30px;font-weight:600;color:#ffffffc0;margin-top:12px;}} |
| 201 |
|
− .chips{{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}} |
| 202 |
|
− .chip{{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);border-radius:999px; |
| 203 |
|
− padding:10px 20px;font-size:24px;font-weight:700;}} |
| 204 |
|
− .price{{font-size:80px;font-weight:900;color:{p['a']};margin-top:24px;letter-spacing:-.02em; |
| 205 |
|
− filter:drop-shadow(0 8px 24px {p['a']}55);}} |
| 206 |
|
− .tagline2{{font-size:28px;font-weight:600;color:#ffffffa8;margin-top:18px;}} |
| 207 |
|
− </style>""" |
|
226 |
+ css = """ |
|
227 |
+ .stage-photo{position:absolute;top:0;left:0;right:0;height:760px;background-size:cover;background-position:center;} |
|
228 |
+ .scrim{position:absolute;inset:0;background: |
|
229 |
+ linear-gradient(180deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,0) 34%, __G2__cc 62%, __G2__ 82%);} |
|
230 |
+ .spot{margin-top:auto;} |
|
231 |
+ .ptitle{font-size:62px;font-weight:900;line-height:1.02;letter-spacing:-.02em;max-width:900px; |
|
232 |
+ text-shadow:0 4px 24px #000a;} |
|
233 |
+ .psub{font-size:30px;font-weight:600;color:#ffffffc0;margin-top:12px;} |
|
234 |
+ .chips{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;} |
|
235 |
+ .chip{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);border-radius:999px; |
|
236 |
+ padding:10px 20px;font-size:24px;font-weight:700;} |
|
237 |
+ .price{font-size:80px;font-weight:900;color:__A__;margin-top:24px;letter-spacing:-.02em; |
|
238 |
+ filter:drop-shadow(0 8px 24px __A__55);} |
|
239 |
+ .tagline2{font-size:28px;font-weight:600;color:#ffffffa8;margin-top:18px;} |
|
240 |
+ """.replace("__G2__",p["g2"]).replace("__A__",p["a"]) |
|
241 |
+ return ('<div class="stage-photo" style="background-image:url(\''+img+'\')"></div><div class="scrim"></div>' |
|
242 |
+ + '<div class="pad">' |
|
243 |
+ + '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · À DÉCOUVRIR · '+today_str().upper()+'</div>' |
|
244 |
+ + '<div class="kabadge">KA</div>' |
|
245 |
+ + '<div class="spot"><div class="ptitle">'+esc(prod.get("title",""))+'</div>' |
|
246 |
+ + '<div class="psub">'+esc(prod.get("subtitle",""))+'</div>' |
|
247 |
+ + '<div class="chips">'+chips+'</div>' |
|
248 |
+ + (('<div class="price">'+esc(price)+'</div>') if price else '') |
|
249 |
+ + '<div class="tagline2">'+esc(ins.get("tagline",""))+'</div></div>' |
|
250 |
+ + footer(ins,p)+'</div><style>'+css+'</style>') |
| 208 |
251 |
|
| 209 |
|
−def genre_list(ins, p): |
|
252 |
+def genre_list(ins, p, T): |
| 210 |
253 |
items = ins.get("list") or [] |
| 211 |
254 |
rows = "" |
| 212 |
255 |
for it in items[:5]: |
| 238 |
281 |
+ footer(ins, p) |
| 239 |
282 |
+ '</div><style>' + css + '</style>') |
| 240 |
283 |
|
|
284 |
+def genre_grid(ins, p, T): |
|
285 |
+ items = [it for it in (ins.get("list") or []) if it.get("image_data")][:4] |
|
286 |
+ cells = "" |
|
287 |
+ for it in items: |
|
288 |
+ cells += ('<div class="cell"><div class="cph" style="background-image:url(\''+it["image_data"]+'\')"></div>' |
|
289 |
+ + '<div class="cscrim"></div><div class="cbot"><div class="ct">'+esc(it.get("title",""))+'</div>' |
|
290 |
+ + (('<div class="cp">'+esc(it["price"])+'</div>') if it.get("price") else '') |
|
291 |
+ + '</div></div>') |
|
292 |
+ heading = esc(ins.get("list_title","") or ins.get("headline_label","") or "À la une") |
|
293 |
+ css = """ |
|
294 |
+ .gtitle{font-size:42px;font-weight:800;color:#fff;margin-top:10px;margin-bottom:22px;max-width:920px;line-height:1.1} |
|
295 |
+ .gwrap{display:grid;grid-template-columns:1fr 1fr;gap:18px} |
|
296 |
+ .cell{position:relative;height:350px;border-radius:22px;overflow:hidden;border:1px solid rgba(255,255,255,.16)} |
|
297 |
+ .cph{position:absolute;inset:0;background-size:cover;background-position:center} |
|
298 |
+ .cscrim{position:absolute;inset:0;background:linear-gradient(180deg,transparent 42%,#000d 100%)} |
|
299 |
+ .cbot{position:absolute;left:18px;right:18px;bottom:14px} |
|
300 |
+ .ct{font-size:26px;font-weight:800;line-height:1.15;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-shadow:0 2px 10px #000} |
|
301 |
+ .cp{font-size:28px;font-weight:900;color:__A__;margin-top:4px;text-shadow:0 2px 10px #000} |
|
302 |
+ """.replace("__A__", p["a"]) |
|
303 |
+ return ('<div class="pad">' |
|
304 |
+ + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · EN IMAGES · ' + today_str().upper() + '</div>' |
|
305 |
+ + '<div class="kabadge">KA</div>' |
|
306 |
+ + '<div class="site" style="font-size:68px;margin-top:14px">' + esc(ins["label"]) + '</div>' |
|
307 |
+ + '<div class="gtitle">' + heading + '</div>' |
|
308 |
+ + '<div class="gwrap">' + cells + '</div>' |
|
309 |
+ + footer(ins, p) |
|
310 |
+ + '</div><style>' + css + '</style>') |
|
311 |
+ |
|
312 |
+NUMRE = re.compile(r'(\d[\d\u00a0\u202f ,\.]*\d|\d)(\s?(%|\$|G\$|M\$|milliards?|millions?))?') |
|
313 |
+ |
|
314 |
+def genre_quote(ins, p, T): |
|
315 |
+ fact = str(ins.get("fact") or (str(ins["headline"])+" "+ins.get("headline_label",""))) |
|
316 |
+ txt = NUMRE.sub(lambda m: '<em>'+m.group(0)+'</em>', esc(fact)) |
|
317 |
+ L = len(fact) |
|
318 |
+ fs = 64 if L < 170 else (56 if L < 240 else 48) |
|
319 |
+ tiles = (ins.get("tiles") or [])[:3] |
|
320 |
+ css = """ |
|
321 |
+ .qmark{font-size:230px;line-height:.55;color:__ACC__;font-weight:900;margin-top:70px;height:118px; |
|
322 |
+ font-family:Georgia,serif;opacity:.9} |
|
323 |
+ .qtext{font-size:__FS__px;font-weight:800;line-height:1.3;letter-spacing:-.01em;max-width:930px;} |
|
324 |
+ .qtext em{font-style:normal;color:__ACC__;} |
|
325 |
+ """.replace("__ACC__",T["acc"]).replace("__FS__",str(fs)) |
|
326 |
+ return ('<div class="pad">'+hdr(ins,p,"LE SAVIEZ-VOUS ?") |
|
327 |
+ + '<div class="qmark">“</div>' |
|
328 |
+ + '<div class="qtext">'+txt+'</div>' |
|
329 |
+ + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>') |
|
330 |
+ + footer(ins,p)+'</div><style>'+css+'</style>') |
|
331 |
+ |
|
332 |
+def genre_donut(ins, p, T): |
|
333 |
+ pct = parse_pct(ins["headline"]) or 0.0 |
|
334 |
+ C = 2*3.14159*210 |
|
335 |
+ filled = C*min(100.0, pct)/100.0 |
|
336 |
+ svg = ('<svg width="540" height="540" viewBox="0 0 540 540">' |
|
337 |
+ + '<circle cx="270" cy="270" r="210" fill="none" stroke="'+T["track"]+'" stroke-width="60"/>' |
|
338 |
+ + '<circle cx="270" cy="270" r="210" fill="none" stroke="url(#gd)" stroke-width="60" stroke-linecap="round" ' |
|
339 |
+ + 'stroke-dasharray="'+str(round(filled,1))+' '+str(round(C,1))+'" transform="rotate(-90 270 270)"/>' |
|
340 |
+ + '<defs><linearGradient id="gd" x1="0" y1="0" x2="1" y2="1">' |
|
341 |
+ + '<stop offset="0" stop-color="'+T["acc"]+'"/><stop offset="1" stop-color="'+p["b"]+'"/></linearGradient></defs></svg>') |
|
342 |
+ tiles = (ins.get("tiles") or [])[:3] |
|
343 |
+ css = """ |
|
344 |
+ .drow{display:flex;align-items:center;gap:44px;margin-top:56px} |
|
345 |
+ .dwrap{position:relative;width:540px;height:540px;flex:0 0 540px} |
|
346 |
+ .dperc{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; |
|
347 |
+ font-size:104px;font-weight:900;color:__ACC__;letter-spacing:-.02em} |
|
348 |
+ .dlab{flex:1;font-size:41px;font-weight:700;line-height:1.18} |
|
349 |
+ """.replace("__ACC__",T["acc"]) |
|
350 |
+ return ('<div class="pad">'+hdr(ins,p) |
|
351 |
+ + '<div class="drow"><div class="dwrap">'+svg+'<div class="dperc">'+esc(ins["headline"])+'</div></div>' |
|
352 |
+ + '<div class="dlab">'+esc(ins["headline_label"])+'</div></div>' |
|
353 |
+ + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>') |
|
354 |
+ + footer(ins,p)+'</div><style>'+css+'</style>') |
|
355 |
+ |
| 241 |
356 |
GENRES = {"hero":genre_hero, "ranking":genre_ranking, "spotlight":genre_spotlight, |
| 242 |
|
− "list":genre_list, "deal":genre_hero, "pulse":genre_hero} |
|
357 |
+ "list":genre_list, "grid":genre_grid, "quote":genre_quote, "donut":genre_donut, |
|
358 |
+ "deal":genre_hero, "pulse":genre_hero} |
|
359 |
+ |
|
360 |
+FIT_JS = """<script> |
|
361 |
+document.querySelectorAll('[data-fit]').forEach(function(el){ |
|
362 |
+ var s=parseFloat(getComputedStyle(el).fontSize); |
|
363 |
+ while(el.scrollWidth>el.clientWidth && s>24){ s-=4; el.style.fontSize=s+'px'; } |
|
364 |
+}); |
|
365 |
+</script>""" |
| 243 |
366 |
|
| 244 |
367 |
def build_html(ins): |
| 245 |
368 |
p = pal(ins["site"]) |
| 246 |
369 |
genre = ins.get("genre","hero") |
| 247 |
370 |
if genre == "ranking" and not ins.get("bars"): genre = "hero" |
| 248 |
371 |
if genre == "spotlight" and not (ins.get("product") or {}).get("image_data"): genre = "hero" |
|
372 |
+ if genre == "grid": |
|
373 |
+ n_img = len([i for i in (ins.get("list") or []) if i.get("image_data")]) |
|
374 |
+ if n_img < 4: genre = "list" if n_img >= 2 else "hero" |
| 249 |
375 |
if genre == "list" and not ins.get("list"): genre = "hero" |
| 250 |
|
− body = GENRES.get(genre, genre_hero)(ins, p) |
| 251 |
|
− return f"""<!doctype html><html><head><meta charset="utf-8"><style>{base_css(p)}</style></head> |
| 252 |
|
− <body><div class="stage"><div class="grain"></div>{body}</div></body></html>""", genre |
|
376 |
+ if genre == "donut" and parse_pct(ins.get("headline")) is None: genre = "hero" |
|
377 |
+ if genre == "quote" and len(str(ins.get("fact") or "")) < 40: genre = "hero" |
|
378 |
+ # les genres photo restent sur le thème sombre (les photos y ressortent mieux) |
|
379 |
+ theme = "nuit" if genre in ("spotlight","list","grid") else theme_of(ins) |
|
380 |
+ T = tokens(p, theme) |
|
381 |
+ body = GENRES.get(genre, genre_hero)(ins, p, T) |
|
382 |
+ html = ("<!doctype html><html><head><meta charset=\"utf-8\"><style>"+base_css(p,T)+"</style></head>" |
|
383 |
+ + "<body><div class=\"stage\"><div class=\"grain\"></div>"+body+"</div>"+FIT_JS+"</body></html>") |
|
384 |
+ return html, genre, theme |
| 253 |
385 |
|
| 254 |
386 |
def render(ins, out_dir): |
| 255 |
387 |
os.makedirs(out_dir, exist_ok=True) |
| 256 |
|
− # télécharger la photo produit en data-uri (robuste, pas de hotlink) |
|
388 |
+ # télécharger les photos en data-uri (robuste, pas de hotlink) |
| 257 |
389 |
prod = ins.get("product") |
| 258 |
390 |
if prod and prod.get("image") and not prod.get("image_data"): |
| 259 |
391 |
du = data_uri(prod["image"]) |
| 262 |
394 |
if it.get("image") and not it.get("image_data"): |
| 263 |
395 |
du = data_uri(it["image"]) |
| 264 |
396 |
if du: it["image_data"] = du |
| 265 |
|
− html, genre = build_html(ins) |
| 266 |
|
− key = hashlib.md5((ins["site"]+ins.get("insight_id","")+genre).encode()).hexdigest()[:8] |
|
397 |
+ html, genre, theme = build_html(ins) |
|
398 |
+ key = hashlib.md5((ins["site"]+str(ins.get("insight_id",""))+genre+theme).encode()).hexdigest()[:8] |
| 267 |
399 |
html_path = os.path.join(out_dir, f"card-{ins['site']}-{key}.html") |
| 268 |
400 |
big = os.path.join(out_dir, f"_big-{key}.png") |
| 269 |
|
− out = os.path.join(out_dir, f"card-{ins['site']}-{ins.get('insight_id','x')}-{genre}.png") |
|
401 |
+ out = os.path.join(out_dir, f"card-{ins['site']}-{ins.get('insight_id','x')}-{genre}-{theme}.png") |
| 270 |
402 |
open(html_path,"w",encoding="utf-8").write(html) |
| 271 |
403 |
chrome = find_chrome() |
| 272 |
404 |
subprocess.run([chrome,"--headless","--disable-gpu","--hide-scrollbars", |
| 273 |
405 |
|