feat(extension): générer les assets visuels Chrome Web Store (icon, tiles, screenshots, marquee)
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m51s
CI / Deploy production (on server) (push) Successful in 23s

- extension/scripts/generate-store-assets.py : générateur Python/PIL avec
  la palette x.ai premium (BG #0B0A09, TEXT #F4F1EA, OCHRE #D4A373).
- extension/store-assets/ :
  * icon-128.png (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 avec sélecteur de carnet
  * screenshot-2.png (1280×800) — confirmation IA avec résumé + tags
  * screenshot-3.png (1280×800) — vue RTL/persan (font DejaVu pour glyphes)

Tous les assets respectent les dimensions Chrome Web Store et sont prêts
pour la soumission.
This commit is contained in:
Antigravity
2026-07-18 09:16:46 +00:00
parent a7c15e9fc3
commit 6eb46fd1b7
8 changed files with 516 additions and 0 deletions

View File

@@ -0,0 +1,516 @@
#!/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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB