Ce script propose une approche générative et déterministe pour créer des images de couverture originales à partir d’une simple graine (seed). L’idée centrale est de produire des visuels abstraits, cohérents et reproductibles, sans dépendre de fichiers graphiques préexistants.
Le fonctionnement repose sur trois principes clés :
-
Une seed unique pour chaque image
La graine (chaîne de caractères ou valeur quelconque) est transformée en nombre via un hash SHA-256. Cela permet d’initialiser un générateur pseudo-aléatoire de façon stable : avec la même graine, l’image produite sera toujours identique. C’est idéal pour générer des covers cohérentes par article ou par identifiant. -
Une palette de couleurs prédéfinie
Chaque image commence par le choix aléatoire d’une palette composée d’un fond sombre et de couleurs d’accent. Cette contrainte garantit une harmonie visuelle, tout en laissant suffisamment de variété entre les rendus. -
Une composition en couches
L’image est construite progressivement :-
un fond uniforme,
-
de grandes formes floues et semi-transparentes pour donner de la profondeur,
-
une série de formes géométriques abstraites (triangles, polygones, vagues, capsules, étoiles, etc.),
-
puis un léger bruit visuel sous forme de points, pour enrichir la texture.
-
Chaque type de forme est dessiné avec des variations de taille, de position, d’opacité et de remplissage, ce qui donne un rendu organique tout en restant structuré.
Enfin, l’image est fusionnée et sauvegardée automatiquement dans un dossier dédié. Le résultat est une cover moderne, abstraite et unique, parfaitement adaptée à des articles de blog, tout en étant générée rapidement et de manière entièrement automatisée.
import hashlib
import math
import os
import random
from PIL import Image, ImageDraw
PALETTES = [
("#2f3437", ["#3b4044", "#596067", "#747b83"]), # slate
("#2e2b2f", ["#3a353c", "#5a545e", "#77717d"]), # charcoal
("#2c313a", ["#3b4250", "#5d6778", "#7a8699"]), # steel
("#1f2937", ["#3b82f6", "#60a5fa", "#93c5fd"]), # blue
("#2a1f33", ["#8b5cf6", "#a78bfa", "#c4b5fd"]), # violet
("#1f3328", ["#10b981", "#34d399", "#6ee7b7"]), # emerald
("#332417", ["#f97316", "#fb923c", "#fdba74"]), # orange
("#33161d", ["#ef4444", "#f87171", "#fca5a5"]), # red
("#331a2a", ["#ec4899", "#f472b6", "#f9a8d4"]), # pink
("#102a2e", ["#06b6d4", "#22d3ee", "#67e8f9"]), # cyan
]
def normalize_seed(seed) -> int:
if not isinstance(seed, (str, bytes)):
seed = str(seed)
h = hashlib.sha256(seed.encode("utf-8")).hexdigest()
return int(h[:16], 16)
def hex_rgba(hex_color: str, alpha: int):
hex_color = hex_color.lstrip("#")
return (
int(hex_color[0:2], 16),
int(hex_color[2:4], 16),
int(hex_color[4:6], 16),
alpha,
)
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def rand_point(rnd, w, h, margin=0):
return (rnd.randint(-margin, w + margin), rnd.randint(-margin, h + margin))
def draw_triangle(d, rnd, w, h, col, filled=True):
p1 = rand_point(rnd, w, h, margin=80)
p2 = rand_point(rnd, w, h, margin=80)
p3 = rand_point(rnd, w, h, margin=80)
if filled:
d.polygon([p1, p2, p3], fill=col)
else:
d.line([p1, p2, p3, p1], fill=col, width=rnd.randint(2, 4))
def draw_polygon(d, rnd, w, h, col, n=None, filled=False):
n = n or rnd.randint(4, 8)
cx, cy = rand_point(rnd, w, h, margin=100)
r = rnd.randint(25, 120)
start = rnd.random() * math.tau
pts = []
for i in range(n):
ang = start + i * (math.tau / n) + rnd.uniform(-0.15, 0.15)
rr = r * rnd.uniform(0.75, 1.15)
pts.append((int(cx + math.cos(ang) * rr), int(cy + math.sin(ang) * rr)))
if filled:
d.polygon(pts, fill=col)
else:
d.line(pts + [pts[0]], fill=col, width=rnd.randint(2, 4))
def draw_donut(d, rnd, w, h, col):
x = rnd.randint(0, w - 1)
y = rnd.randint(0, h - 1)
r = rnd.randint(20, 90)
width = rnd.randint(6, 16)
d.ellipse((x - r, y - r, x + r, y + r), outline=col, width=width)
def draw_plus(d, rnd, w, h, col):
x = rnd.randint(0, w - 1)
y = rnd.randint(0, h - 1)
size = rnd.randint(18, 70)
width = rnd.randint(3, 6)
d.line((x - size, y, x + size, y), fill=col, width=width)
d.line((x, y - size, x, y + size), fill=col, width=width)
def draw_zigzag(d, rnd, w, h, col):
y = rnd.randint(0, h - 1)
x0 = rnd.randint(-100, 200)
amp = rnd.randint(10, 40)
step = rnd.randint(35, 80)
width = rnd.randint(2, 4)
pts = []
x = x0
up = True
while x < w + 100:
pts.append((x, y + (amp if up else -amp)))
up = not up
x += step
d.line(pts, fill=col, width=width)
def draw_capsule(d, rnd, w, h, col):
cw = rnd.randint(80, 260)
ch = rnd.randint(22, 70)
x = rnd.randint(-50, w - 1)
y = rnd.randint(-50, h - 1)
radius = ch // 2
filled = rnd.random() < 0.5
if filled:
d.rounded_rectangle((x, y, x + cw, y + ch), radius=radius, fill=col)
else:
d.rounded_rectangle((x, y, x + cw, y + ch), radius=radius, outline=col, width=rnd.randint(3, 6))
def draw_star(d, rnd, w, h, col, points=5):
cx, cy = rand_point(rnd, w, h, margin=80)
r_outer = rnd.randint(25, 85)
r_inner = int(r_outer * rnd.uniform(0.4, 0.6))
rot = rnd.random() * math.tau
pts = []
for i in range(points * 2):
r = r_outer if i % 2 == 0 else r_inner
ang = rot + i * (math.tau / (points * 2))
pts.append((int(cx + math.cos(ang) * r), int(cy + math.sin(ang) * r)))
if rnd.random() < 0.5:
d.polygon(pts, fill=col)
else:
d.line(pts + [pts[0]], fill=col, width=rnd.randint(2, 4))
def draw_wave(d, rnd, w, h, col):
baseline = rnd.randint(0, h - 1)
amp = rnd.randint(10, 40)
period = rnd.randint(120, 260)
width = rnd.randint(2, 4)
pts = []
for x in range(-50, w + 50, 10):
y = baseline + int(math.sin((x / period) * math.tau) * amp)
pts.append((x, y))
d.line(pts, fill=col, width=width)
def generate_cover(seed: str = None, size=(1600, 900)):
if seed is None:
seed = os.urandom(16).hex()
rnd = random.Random(normalize_seed(seed))
bg, accents = rnd.choice(PALETTES)
w, h = size
base = Image.new("RGB", size, bg).convert("RGBA")
overlay = Image.new("RGBA", size, (0, 0, 0, 0))
d = ImageDraw.Draw(overlay)
# --- big soft blobs (background depth) ---
for _ in range(rnd.randint(3, 6)):
cx = rnd.randint(-200, w + 200)
cy = rnd.randint(-200, h + 200)
r = rnd.randint(260, 520)
col = hex_rgba(rnd.choice(accents), rnd.randint(22, 45))
d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=col)
# --- mixed shape pass ---
shape_fns = [
lambda col: draw_triangle(d, rnd, w, h, col, filled=(rnd.random() < 0.5)),
lambda col: draw_polygon(d, rnd, w, h, col, n=rnd.randint(3, 8), filled=(rnd.random() < 0.35)),
lambda col: draw_donut(d, rnd, w, h, col),
lambda col: draw_plus(d, rnd, w, h, col),
lambda col: draw_zigzag(d, rnd, w, h, col),
lambda col: draw_capsule(d, rnd, w, h, col),
lambda col: draw_star(d, rnd, w, h, col, points=rnd.choice([5, 6])),
lambda col: draw_wave(d, rnd, w, h, col),
]
for _ in range(rnd.randint(10, 20)):
col = hex_rgba(rnd.choice(accents), rnd.randint(35, 95))
rnd.choice(shape_fns)(col)
# --- subtle texture dots ---
for _ in range(rnd.randint(350, 750)):
x = rnd.randrange(w)
y = rnd.randrange(h)
r = rnd.randint(1, 2)
col = hex_rgba(rnd.choice(accents), rnd.randint(16, 35))
d.ellipse((x - r, y - r, x + r, y + r), fill=col)
img_dir = "app/static/covers"
os.makedirs(img_dir, exist_ok=True)
img_path = os.path.join(img_dir, f"cover_{seed}.png")
img = Image.alpha_composite(base, overlay).convert("RGB")
img.save(img_path, format="PNG", optimize=True)
return img_path