#!/usr/bin/env python3 """ Génère les assets visuels du Chrome Web Store pour Memento Web Clipper. Crée : - icon-128.png (icône Store 128×128) - promo-tile-small.png (440×280) - promo-tile-large.png (920×680) - marquee.png (1400×560) - screenshot-1.png (1280×800) — sidebar idle - screenshot-2.png (1280×800) — confirmation IA - screenshot-3.png (1280×800) — vue RTL/persan Usage : python3 generate-store-assets.py """ import os import sys from PIL import Image, ImageDraw, ImageFont, ImageFilter # ═══════════════════════════════════════════════════════════════════════ # Palette & polices (alignées sur la landing x.ai premium) # ═══════════════════════════════════════════════════════════════════════ BG = (11, 10, 9) # #0B0A09 — fond principal TEXT = (244, 241, 234) # #F4F1EA — texte principal TEXT_DIM = (200, 195, 184) # ~55% opacité TEXT_FAINT = (130, 125, 115) # ~25% opacité OCHRE = (212, 163, 115) # #D4A373 — accent OCHRE_SOFT = (212, 163, 115, 80) CARD = (28, 26, 24) # fond carte élevée CARD_BORDER = (50, 47, 43) # bordure carte WHITE_06 = (255, 255, 255, 16) WHITE_03 = (255, 255, 255, 8) FONT_SERIF_BOLD = "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf" FONT_SERIF_REG = "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf" FONT_SANS_BOLD = "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf" FONT_SANS_REG = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf" FONT_DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" # couvre latin + arabe/persan (Vazirmatn fallback) OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "store-assets") os.makedirs(OUT_DIR, exist_ok=True) # ═══════════════════════════════════════════════════════════════════════ # Utilitaires # ═══════════════════════════════════════════════════════════════════════ def load_font(path, size): try: return ImageFont.truetype(path, size) except Exception: return ImageFont.load_default() def text_w(draw, txt, font): bbox = draw.textbbox((0, 0), txt, font=font) return bbox[2] - bbox[0] def text_h(draw, txt, font): bbox = draw.textbbox((0, 0), txt, font=font) return bbox[3] - bbox[1] def rounded_rect(draw, xy, radius, fill=None, outline=None, width=1): draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width) def radial_glow(size, center, radius, color, alpha=80): """Crée un calque de glow radial.""" w, h = size glow = Image.new("RGBA", size, (0, 0, 0, 0)) cx, cy = center for r in range(radius, 0, -2): a = int(alpha * (1 - r / radius) ** 2) draw = ImageDraw.Draw(glow) draw.ellipse( [cx - r, cy - r, cx + r, cy + r], fill=color + (a,), ) return glow def base_canvas(w, h, bg=BG): """Crée un fond avec léger grain + glow ochre en haut-droite.""" img = Image.new("RGB", (w, h), bg) overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0)) # Glow ochre discret g = radial_glow((w, h), (int(w * 0.85), int(h * 0.15)), max(w, h) // 2, OCHRE, alpha=40) overlay = Image.alpha_composite(overlay, g) img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB") return img def draw_logo_m(draw, cx, cy, size, color=TEXT): """Dessine le logo 'M' dans un carré arrondi (style landing).""" box = size x0, y0 = cx - box // 2, cy - box // 2 x1, y1 = cx + box // 2, cy + box // 2 radius = int(box * 0.22) rounded_rect(draw, (x0, y0, x1, y1), radius, fill=color) # M text f = load_font(FONT_SERIF_BOLD, int(size * 0.55)) t = "M" w = text_w(draw, t, f) h = text_h(draw, t, f) draw.text( (cx - w // 2, cy - h // 2 - int(size * 0.05)), t, font=f, fill=BG, ) def draw_brand(draw, x, y, logo_size=44, gap=12, color=TEXT): """Logo + wordmark 'Memento' alignés.""" draw_logo_m(draw, x + logo_size // 2, y + logo_size // 2, logo_size, color=color) f = load_font(FONT_SERIF_REG, int(logo_size * 0.55)) draw.text((x + logo_size + gap, y + logo_size // 2 - text_h(draw, "Memento", f) // 2 - 2), "Memento", font=f, fill=color) def draw_subtle_border(draw, xy, r=10, alpha=16): """Bordure très subtile (style landing).""" draw.rounded_rectangle(xy, radius=r, outline=(255, 255, 255, alpha), width=1) # ═══════════════════════════════════════════════════════════════════════ # Générateurs # ═══════════════════════════════════════════════════════════════════════ def gen_icon_128(): """Icône Store 128×128 — fond carré, gros M centré.""" img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Fond arrondi rounded_rect(draw, (0, 0, 127, 127), 28, fill=TEXT) # M en serif bold f = load_font(FONT_SERIF_BOLD, 80) t = "M" w = text_w(draw, t, f) h = text_h(draw, t, f) draw.text(((128 - w) // 2, (128 - h) // 2 - 10), t, font=f, fill=BG) # Petite tache ochre en haut à droite draw.ellipse((95, 12, 116, 33), fill=OCHRE) return img.convert("RGB") def gen_promo_tile(w, h, filename): """Petite ou grande promo tile.""" img = base_canvas(w, h) draw = ImageDraw.Draw(img) # Marges pad = max(28, int(w * 0.07)) inner_w = w - 2 * pad # Logo + wordmark en haut draw_brand(draw, pad, pad, logo_size=int(w * 0.11)) # Tagline en dessous f_eyebrow = load_font(FONT_SANS_BOLD, int(w * 0.028)) f_headline = load_font(FONT_SERIF_BOLD, int(w * 0.075)) f_sub = load_font(FONT_SANS_REG, int(w * 0.030)) y = pad + int(w * 0.11) + int(w * 0.04) eyebrow = "WEB CLIPPER" draw.text((pad, y), eyebrow, font=f_eyebrow, fill=OCHRE) y += text_h(draw, eyebrow, f_eyebrow) + int(w * 0.02) headline = "Save anything\nto your Second Brain." # Wrap simple sur \n for line in headline.split("\n"): draw.text((pad, y), line, font=f_headline, fill=TEXT) y += text_h(draw, line, f_headline) + int(w * 0.008) y += int(w * 0.015) sub = "AI-powered summary · 15 languages · RTL ready" draw.text((pad, y), sub, font=f_sub, fill=TEXT_DIM) # Decorative dots/brand mark bas droite cx = w - pad - int(w * 0.04) cy = h - pad - int(w * 0.04) r = int(w * 0.035) for i, alpha in enumerate([180, 120, 60]): x0 = cx - r + i * int(w * 0.018) y0 = cy - r // 2 draw.ellipse((x0, y0, x0 + int(w * 0.012), y0 + int(w * 0.012)), fill=OCHRE) img.save(os.path.join(OUT_DIR, filename), "PNG", optimize=True) def gen_marquee(): """Marquee 1400×560 — bandeau hero très large.""" w, h = 1400, 560 img = base_canvas(w, h) draw = ImageDraw.Draw(img) pad_x = 80 # Logo + wordmark draw_brand(draw, pad_x, 60, logo_size=72) # Headline très large f_eyebrow = load_font(FONT_SANS_BOLD, 18) f_h1 = load_font(FONT_SERIF_BOLD, 92) f_h2 = load_font(FONT_SERIF_REG, 56) f_sub = load_font(FONT_SANS_REG, 22) y = 200 draw.text((pad_x, y), "WEB CLIPPER", font=f_eyebrow, fill=OCHRE) y += 36 draw.text((pad_x, y), "Your Second Brain,", font=f_h1, fill=TEXT) y += 100 draw.text((pad_x, y), "one click away.", font=f_h2, fill=TEXT_DIM) y += 80 draw.text((pad_x, y), "Capture any web page or selection · AI summary · 15 languages", font=f_sub, fill=TEXT_DIM) # Decorative arc + dots côté droit cx = 1100 cy = h // 2 for r in [200, 140, 90]: draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(255, 255, 255, 22), width=1) draw.ellipse((cx - 50, cy - 50, cx + 50, cy + 50), fill=OCHRE) img.save(os.path.join(OUT_DIR, "marquee.png"), "PNG", optimize=True) def draw_browser_chrome(draw, x, y, w, h): """Dessine un cadre de browser factice (toolbar + onglet).""" # Fond fenêtre rounded_rect(draw, (x, y, x + w, y + h), 12, fill=CARD, outline=CARD_BORDER, width=1) # Toolbar tb_h = 44 draw.rectangle((x, y, x + w, y + tb_h), fill=(20, 18, 16)) # Trafic lights for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]): cx = x + 18 + i * 22 cy = y + tb_h // 2 draw.ellipse((cx - 6, cy - 6, cx + 6, cy + 6), fill=c) # URL bar bar_x = x + 100 bar_y = y + 12 bar_w = w - 220 bar_h = 22 rounded_rect(draw, (bar_x, bar_y, bar_x + bar_w, bar_y + bar_h), 11, fill=(40, 36, 32)) # Texte URL simulé f = load_font(FONT_SANS_REG, 11) draw.text((bar_x + 12, bar_y + 4), " memento-note.com/notebooks/research", font=f, fill=TEXT_DIM) def draw_side_panel(draw, x, y, w, h, state="idle"): """Panneau latéral mockup. state: idle | confirm | rtl.""" rounded_rect(draw, (x, y, x + w, y + h), 8, fill=(20, 18, 16), outline=CARD_BORDER, width=1) # Header (brand + settings) header_h = 56 draw.rectangle((x, y, x + w, y + header_h), fill=(14, 13, 12)) draw.line((x, y + header_h, x + w, y + header_h), fill=CARD_BORDER, width=1) # M logo mini logo_size = 26 draw_logo_m(draw, x + 20 + logo_size // 2, y + header_h // 2, logo_size) # "Memento" + "Web Clipper" f_brand = load_font(FONT_SERIF_BOLD, 14) f_sub_brand = load_font(FONT_SANS_REG, 9) bx = x + 20 + logo_size + 8 draw.text((bx, y + 14), "Memento", font=f_brand, fill=TEXT) draw.text((bx, y + 32), "Web Clipper", font=f_sub_brand, fill=TEXT_FAINT) # Content area cy = y + header_h + 16 cx = x + 16 cw = w - 32 if state == "confirm": draw_confirm_content(draw, cx, cy, cw) elif state == "rtl": draw_rtl_content(draw, cx, cy, cw) else: draw_idle_content(draw, cx, cy, cw) def draw_idle_content(draw, cx, cy, cw): """État idle : carnet + page card + sélection hint + boutons.""" # Label "Carnet de destination" f_label = load_font(FONT_SANS_BOLD, 9) draw.text((cx, cy), "CARNET DE DESTINATION", font=f_label, fill=TEXT_FAINT) cy += 18 # Select field f_select = load_font(FONT_SANS_REG, 12) rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1) draw.text((cx + 10, cy + 9), "📁 Research Notes", font=f_select, fill=TEXT) cy += 48 # Page card rounded_rect(draw, (cx, cy, cx + cw, cy + 56), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1) f_sub = load_font(FONT_SANS_REG, 8) draw.text((cx + 10, cy + 8), "ACTIVE PAGE", font=f_sub, fill=TEXT_FAINT) # Favicon placeholder + title + url draw.rectangle((cx + 10, cy + 22, cx + 26, cy + 38), fill=OCHRE) f_title = load_font(FONT_SANS_BOLD, 11) f_url = load_font(FONT_SANS_REG, 9) draw.text((cx + 34, cy + 22), "Building a Second Brain", font=f_title, fill=TEXT) draw.text((cx + 34, cy + 38), " memento-note.com/blog/...", font=f_url, fill=TEXT_DIM) cy += 72 # Selection hint rounded_rect(draw, (cx, cy, cx + cw, cy + 36), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1) f_hint = load_font(FONT_SANS_REG, 10) draw.text((cx + 10, cy + 12), "Astuce : surlignez du texte pour", font=f_hint, fill=TEXT_DIM) draw.text((cx + 10, cy + 24), "clipper une sélection précise.", font=f_hint, fill=TEXT_DIM) cy += 48 # Buttons btn_w = (cw - 8) // 2 # Primary "Clipper cette page" rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=OCHRE) f_btn = load_font(FONT_SANS_BOLD, 10) text = "Clipper cette page" w = text_w(draw, text, f_btn) draw.text((cx + (cw - w) // 2, cy + 10), text, font=f_btn, fill=BG) cy += 40 # Secondary "Enregistrer le lien" rounded_rect(draw, (cx, cy, cx + cw, cy + 26), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1) text = "Enregistrer le lien uniquement" w = text_w(draw, text, f_btn) draw.text((cx + (cw - w) // 2, cy + 8), text, font=f_btn, fill=TEXT_DIM) def draw_confirm_content(draw, cx, cy, cw): """État confirmation : titre éditable, résumé, tags, save.""" f_label = load_font(FONT_SANS_BOLD, 9) draw.text((cx, cy), "VÉRIFIER AVANT D'ENREGISTRER", font=f_label, fill=TEXT_FAINT) cy += 18 # Title field rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1) f_title = load_font(FONT_SANS_BOLD, 12) draw.text((cx + 10, cy + 9), "Building a Second Brain", font=f_title, fill=TEXT) cy += 42 # Reading time f_meta = load_font(FONT_SANS_REG, 9) draw.text((cx, cy), "≈ 4 minutes de lecture", font=f_meta, fill=TEXT_FAINT) cy += 22 # Summary f_summary = load_font(FONT_SANS_REG, 10) summary_lines = [ "Capturez des pages web et du texte", "surligné dans vos carnets Memento.", "L'IA extrait le résumé, les tags et", "les embeddings sémantiques.", ] for line in summary_lines: draw.text((cx, cy), line, font=f_summary, fill=TEXT) cy += 16 cy += 8 # Tags chips f_tag = load_font(FONT_SANS_BOLD, 9) tags = ["notes", "productivity", "ai"] x = cx for t in tags: tw = text_w(draw, t, f_tag) + 14 rounded_rect(draw, (x, cy, x + tw, cy + 18), 9, fill=(30, 27, 25), outline=CARD_BORDER, width=1) draw.text((x + 7, cy + 4), t, font=f_tag, fill=TEXT) x += tw + 6 cy += 30 # Save button (primary) btn_h = 36 rounded_rect(draw, (cx, cy, cx + cw, cy + btn_h), 6, fill=OCHRE) f_btn = load_font(FONT_SANS_BOLD, 12) text = "Enregistrer dans Memento" w = text_w(draw, text, f_btn) draw.text((cx + (cw - w) // 2, cy + 11), text, font=f_btn, fill=BG) cy += btn_h + 8 # Back link f_link = load_font(FONT_SANS_REG, 10) text = "← Retour" w = text_w(draw, text, f_link) draw.text((cx + (cw - w) // 2, cy), text, font=f_link, fill=TEXT_DIM) def draw_rtl_content(draw, cx, cy, cw): """État RTL : panneau en mode persan.""" # Label en persan (gauche visuelle, mais le text est RTL) f_label = load_font(FONT_DEJAVU, 9) draw.text((cx, cy), "دفتر مقصد", font=f_label, fill=TEXT_FAINT) cy += 22 # Select field f_select = load_font(FONT_DEJAVU, 12) rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1) draw.text((cx + 10, cy + 9), "یادداشت‌های پژوهشی", font=f_select, fill=TEXT) cy += 48 # Page card rounded_rect(draw, (cx, cy, cx + cw, cy + 56), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1) f_sub = load_font(FONT_DEJAVU, 8) draw.text((cx + 10, cy + 8), "صفحه فعال", font=f_sub, fill=TEXT_FAINT) draw.rectangle((cx + cw - 26, cy + 22, cx + cw - 10, cy + 38), fill=OCHRE) f_title = load_font(FONT_DEJAVU, 11) f_url = load_font(FONT_SANS_REG, 9) draw.text((cx + 10, cy + 22), "ساختن مغز دوم", font=f_title, fill=TEXT) draw.text((cx + 10, cy + 38), "bbc.com/persian/...", font=f_url, fill=TEXT_DIM) cy += 72 # Selection hint RTL rounded_rect(draw, (cx, cy, cx + cw, cy + 36), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1) f_hint = load_font(FONT_DEJAVU, 10) draw.text((cx + 10, cy + 12), "نکته: متن را انتخاب کنید", font=f_hint, fill=TEXT_DIM) cy += 48 # Buttons (right-aligned for RTL feel) btn_w = cw rounded_rect(draw, (cx, cy, cx + btn_w, cy + 32), 6, fill=OCHRE) f_btn = load_font(FONT_DEJAVU, 10) text = "این صفحه را ذخیره کنید" w = text_w(draw, text, f_btn) draw.text((cx + (btn_w - w) // 2, cy + 10), text, font=f_btn, fill=BG) cy += 40 rounded_rect(draw, (cx, cy, cx + btn_w, cy + 26), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1) text = "فقط ذخیره پیوند" w = text_w(draw, text, f_btn) draw.text((cx + (btn_w - w) // 2, cy + 8), text, font=f_btn, fill=TEXT_DIM) def gen_screenshot_panel_only(state, filename): """Screenshot 1280×800 d'un mock browser avec le side panel.""" w, h = 1280, 800 img = base_canvas(w, h) draw = ImageDraw.Draw(img) # Browser chrome bx, by = 40, 40 bw, bh = w - 80, h - 80 draw_browser_chrome(draw, bx, by, bw, bh) # Page content area (mock article) pg_x = bx + 12 pg_y = by + 56 pg_w = bw - 24 - 360 # laisse la place au panel # Article mock f_article_title = load_font(FONT_SERIF_BOLD, 28) f_article_meta = load_font(FONT_SANS_REG, 11) f_article_p = load_font(FONT_SANS_REG, 12) f_article_h2 = load_font(FONT_SERIF_BOLD, 16) # Title draw.text((pg_x + 40, pg_y + 40), "Building a Second Brain", font=f_article_title, fill=TEXT) # Meta draw.text((pg_x + 40, pg_y + 80), "by Tiago Forte · 8 min read · Memento Blog", font=f_article_meta, fill=TEXT_DIM) # Divider draw.line((pg_x + 40, pg_y + 105, pg_x + pg_w - 40, pg_y + 105), fill=CARD_BORDER, width=1) # H2 draw.text((pg_x + 40, pg_y + 130), "Why capture matters", font=f_article_h2, fill=TEXT) # Body paragraphs (lignes simulées) y = pg_y + 160 paragraphs = [ "Modern knowledge workers are overwhelmed by information.", "Notes are not the goal — connections are. A Second Brain", "is not about collecting more; it's about retrieving the right", "piece at the right moment. Capture, organize, distill, express.", "", "When you highlight a paragraph and clip it to Memento,", "the AI extracts the gist, suggests tags, and links it to", "related notes you have written months ago. Memory Echo", "surfaces connections you would have missed otherwise.", ] for p in paragraphs: draw.text((pg_x + 40, y), p, font=f_article_p, fill=TEXT_DIM) y += 22 # Side panel (à droite) panel_w = 340 panel_h = bh - 56 panel_x = bx + bw - panel_w - 12 panel_y = by + 56 draw_side_panel(draw, panel_x, panel_y, panel_w, panel_h, state=state) # Caption bas img.save(os.path.join(OUT_DIR, filename), "PNG", optimize=True) # ═══════════════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════════════ def main(): print(f"📁 Output : {os.path.abspath(OUT_DIR)}") print() print(" • icon-128.png") gen_icon_128().save(os.path.join(OUT_DIR, "icon-128.png"), "PNG", optimize=True) print(" • promo-tile-small.png (440×280)") gen_promo_tile(440, 280, "promo-tile-small.png") print(" • promo-tile-large.png (920×680)") gen_promo_tile(920, 680, "promo-tile-large.png") print(" • marquee.png (1400×560)") gen_marquee() print(" • screenshot-1.png — sidebar idle") gen_screenshot_panel_only("idle", "screenshot-1.png") print(" • screenshot-2.png — confirmation IA") gen_screenshot_panel_only("confirm", "screenshot-2.png") print(" • screenshot-3.png — RTL/persan") gen_screenshot_panel_only("rtl", "screenshot-3.png") print() print("✅ Assets générés.") if __name__ == "__main__": main()