Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts). Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
101 lines
4.3 KiB
Python
101 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Génère le jeu complet d'assets de marque Arboretum depuis le logo source.
|
|
|
|
Source : un PNG « néon sur fond sombre » (arbre-circuit + texte « Arboretum »).
|
|
On extrait l'alpha par luminance (méthode propre pour ce type d'artwork glow-on-black) :
|
|
chaque pixel reçoit une transparence proportionnelle à sa luminosité, ce qui rend le
|
|
fond sombre totalement transparent, garde le cœur des traits opaque et conserve le halo
|
|
en semi-transparent, donc lisible sur n'importe quel fond.
|
|
|
|
Sorties :
|
|
brand/arboretum-logo.png logo complet transparent (haute déf) : Gitea / README
|
|
brand/arboretum-mark.png marque carrée transparente (arbre seul) : avatar Gitea
|
|
brand/arboretum-logo-on-dark.png logo complet sur fond #09090b (fallback fond clair)
|
|
packages/web/public/logo.png logo complet transparent, optimisé pour l'UI
|
|
packages/web/public/icon-192.png icône PWA maskable (arbre, fond #09090b)
|
|
packages/web/public/icon-512.png icône PWA maskable (arbre, fond #09090b)
|
|
packages/web/public/apple-touch-icon.png icône iOS 180 (arbre, fond #09090b)
|
|
packages/web/public/favicon.ico favicon transparent (arbre, 16/32/48)
|
|
|
|
Usage : python3 brand/build-assets.py <source.png>
|
|
"""
|
|
import sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
SRC = sys.argv[1] if len(sys.argv) > 1 else "brand/arboretum-logo-source.png"
|
|
BG = (9, 9, 11) # #09090b : couleur de fond du dashboard (manifest background_color/theme_color)
|
|
|
|
# Découpe verticale arbre / texte (mesurée sur la source)
|
|
TREE_Y = (130, 930) # arbre + curseur >_
|
|
FULL_Y = (130, 1060) # arbre + texte
|
|
|
|
|
|
def extract_rgba(path):
|
|
"""Charge la source et calcule l'alpha par luminance (floor + knee)."""
|
|
rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.float32)
|
|
maxc = rgb.max(axis=2)
|
|
# floor > canal max du fond (~14) pour annuler totalement le fond ; knee = seuil d'opacité.
|
|
floor, knee = 20.0, 185.0 # < floor : transparent ; >= knee : opaque
|
|
alpha = np.clip((maxc - floor) / (knee - floor), 0.0, 1.0) * 255.0
|
|
alpha[alpha < 8] = 0.0 # gate anti-bruit : un vrai fond transparent et une bbox serrée
|
|
out = np.dstack([rgb, alpha]).astype(np.uint8)
|
|
return Image.fromarray(out, "RGBA")
|
|
|
|
|
|
def crop_band(rgba, y0, y1):
|
|
"""Recadre une bande verticale puis serre sur le contenu non transparent."""
|
|
band = rgba.crop((0, y0, rgba.width, y1))
|
|
bbox = band.getbbox() # bbox sur l'alpha
|
|
return band.crop(bbox)
|
|
|
|
|
|
def square(content, pad_frac, bg=None):
|
|
"""Centre `content` dans un carré ; bg=None → transparent, sinon fond plein."""
|
|
side = round(max(content.size) / (1 - 2 * pad_frac))
|
|
fill = (0, 0, 0, 0) if bg is None else (*bg, 255)
|
|
canvas = Image.new("RGBA", (side, side), fill)
|
|
canvas.paste(content, ((side - content.width) // 2, (side - content.height) // 2), content)
|
|
return canvas
|
|
|
|
|
|
def save(img, path, size=None):
|
|
if size:
|
|
img = img.resize((size, size), Image.LANCZOS)
|
|
img.save(path)
|
|
print(f" {path} {img.size[0]}x{img.size[1]}")
|
|
|
|
|
|
def main():
|
|
rgba = extract_rgba(SRC)
|
|
full = crop_band(rgba, *FULL_Y) # logo complet transparent serré
|
|
tree = crop_band(rgba, *TREE_Y) # arbre seul transparent serré
|
|
|
|
print("brand/")
|
|
save(full, "brand/arboretum-logo.png")
|
|
save(square(tree, 0.08), "brand/arboretum-mark.png")
|
|
# logo complet sur fond sombre (pour surfaces claires où la transparence gêne)
|
|
on_dark = Image.new("RGBA", (full.width + 160, full.height + 160), (*BG, 255))
|
|
on_dark.paste(full, (80, 80), full)
|
|
save(on_dark, "brand/arboretum-logo-on-dark.png")
|
|
|
|
print("packages/web/public/")
|
|
# logo UI : largeur max 600 px, transparent
|
|
ui = full.copy()
|
|
ui.thumbnail((600, 600), Image.LANCZOS)
|
|
ui.save("packages/web/public/logo.png")
|
|
print(f" packages/web/public/logo.png {ui.size[0]}x{ui.size[1]}")
|
|
# icônes maskable : arbre dans la zone sûre (contenu ~80 %), fond #09090b
|
|
mask = square(tree, 0.12, bg=BG)
|
|
save(mask, "packages/web/public/icon-192.png", 192)
|
|
save(mask, "packages/web/public/icon-512.png", 512)
|
|
save(mask, "packages/web/public/apple-touch-icon.png", 180)
|
|
# favicon : arbre transparent, multi-tailles
|
|
fav = square(tree, 0.04)
|
|
fav.save("packages/web/public/favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)])
|
|
print(" packages/web/public/favicon.ico 16/32/48")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|