- Logo SVG (source texte) ajouté dans docs/assets/alpinux-logo.svg - .gitignore : exclut *.png, *.ico, docs/assets/images/ (binaires → static.alpinux.org) - overrides/partials/logo.html : logo depuis https://static.alpinux.org/logo/ - overrides/main.html : favicons depuis static.alpinux.org via {% block extrahead %} - mkdocs.yml : logo → SVG, ajout pymdownx.emoji (icônes Material) - home/index.html : page d'accueil alpinux.org (logo + favicon depuis static.alpinux.org, carte dynamic.alpinux.org) - scripts/build-assets.py : génère PNG/favicon depuis le SVG source - scripts/static.alpinux.org.vhost.conf : template vhost Apache Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
5 KiB
Python
155 lines
5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Génère les assets binaires (logo PNG + favicons) depuis la source SVG.
|
||
Dépendances : Pillow, chromium
|
||
|
||
Usage :
|
||
python3 scripts/build-assets.py [--out /chemin/de/sortie]
|
||
|
||
Les fichiers générés sont ensuite uploadés sur static.alpinux.org/logo/
|
||
"""
|
||
|
||
import argparse
|
||
import subprocess
|
||
import tempfile
|
||
import os
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
REPO = Path(__file__).resolve().parent.parent
|
||
SVG = REPO / "docs/assets/alpinux-logo.svg"
|
||
FONT_R = "/usr/share/fonts/truetype/msttcorefonts/arial.ttf"
|
||
FONT_B = "/usr/share/fonts/truetype/msttcorefonts/arialbd.ttf"
|
||
COLOR = (15, 78, 143) # #0f4e8f
|
||
|
||
|
||
def render_shapes(svg_path: Path, width: int, height: int) -> Image.Image:
|
||
"""Render SVG shapes via chromium headless (no text)."""
|
||
html = f"""<!DOCTYPE html><html>
|
||
<head><meta charset="utf-8">
|
||
<style>*{{margin:0;padding:0}}html,body{{width:{width}px;height:{height}px;overflow:hidden;background:transparent}}</style>
|
||
</head>
|
||
<body><img src="file://{svg_path}" width="{width}" height="{height}"></body>
|
||
</html>"""
|
||
with tempfile.NamedTemporaryFile(suffix=".html", mode="w", delete=False) as f:
|
||
f.write(html)
|
||
tmp_html = f.name
|
||
out_png = tmp_html.replace(".html", ".png")
|
||
subprocess.run([
|
||
"chromium", "--headless", "--disable-gpu", "--no-sandbox",
|
||
f"--screenshot={out_png}", f"--window-size={width},{height}",
|
||
f"file://{tmp_html}"
|
||
], capture_output=True)
|
||
img = Image.open(out_png).convert("RGBA")
|
||
os.unlink(tmp_html)
|
||
os.unlink(out_png)
|
||
return img
|
||
|
||
|
||
def add_text(canvas: Image.Image) -> Image.Image:
|
||
"""Composite Alpinux text with correct weights onto the canvas."""
|
||
draw = ImageDraw.Draw(canvas)
|
||
size = 30
|
||
f_reg = ImageFont.truetype(FONT_R, size)
|
||
f_bld = ImageFont.truetype(FONT_B, size)
|
||
parts = [("A", False), ("l", True), ("p", False), ("inux", True)]
|
||
|
||
widths = []
|
||
for char, bold in parts:
|
||
f = f_bld if bold else f_reg
|
||
bb = f.getbbox(char)
|
||
widths.append(bb[2] - bb[0])
|
||
|
||
total_w = sum(widths)
|
||
x = (canvas.width - total_w) // 2
|
||
y = 164 + (36 - size) // 2 + 1
|
||
|
||
for (char, bold), w in zip(parts, widths):
|
||
f = f_bld if bold else f_reg
|
||
bb = f.getbbox(char)
|
||
draw.text((x - bb[0], y - bb[1]), char, font=f, fill=COLOR)
|
||
x += w
|
||
|
||
return canvas
|
||
|
||
|
||
def build_logo(out_dir: Path):
|
||
"""Build 200×200 logo PNG."""
|
||
shapes = render_shapes(SVG, 200, 164)
|
||
canvas = Image.new("RGBA", (200, 200), (255, 255, 255, 255))
|
||
canvas.paste(shapes, (0, 0))
|
||
canvas = add_text(canvas)
|
||
path = out_dir / "alpinux-logo.png"
|
||
canvas.convert("RGB").save(path)
|
||
print(f" {path} (200×200)")
|
||
|
||
# 512px high-res version
|
||
shapes512 = render_shapes(SVG, 512, 421)
|
||
canvas512 = Image.new("RGBA", (512, 512), (255, 255, 255, 255))
|
||
canvas512.paste(shapes512, (0, 0))
|
||
# Scale text proportionally
|
||
draw = ImageDraw.Draw(canvas512)
|
||
size = 77
|
||
f_reg = ImageFont.truetype(FONT_R, size)
|
||
f_bld = ImageFont.truetype(FONT_B, size)
|
||
parts = [("A", False), ("l", True), ("p", False), ("inux", True)]
|
||
widths = [f_bld.getbbox(c)[2]-f_bld.getbbox(c)[0] if b else f_reg.getbbox(c)[2]-f_reg.getbbox(c)[0] for c,b in parts]
|
||
total_w = sum(widths)
|
||
x = (512 - total_w) // 2
|
||
y = 421 + (91 - size) // 2 + 2
|
||
for (char, bold), w in zip(parts, widths):
|
||
f = f_bld if bold else f_reg
|
||
bb = f.getbbox(char)
|
||
draw.text((x - bb[0], y - bb[1]), char, font=f, fill=COLOR)
|
||
x += w
|
||
path512 = out_dir / "alpinux-logo-512.png"
|
||
canvas512.convert("RGB").save(path512)
|
||
print(f" {path512} (512×512)")
|
||
|
||
|
||
def build_favicons(out_dir: Path):
|
||
"""Build favicon PNG set + .ico from the icon portion of the SVG."""
|
||
icon_src = render_shapes(SVG, 200, 164).crop((0, 0, 164, 164))
|
||
|
||
sizes = {
|
||
"favicon-16.png": 16,
|
||
"favicon-32.png": 32,
|
||
"favicon.png": 48,
|
||
"favicon-96.png": 96,
|
||
"favicon-192.png": 192,
|
||
}
|
||
imgs = {}
|
||
for name, sz in sizes.items():
|
||
img = icon_src.resize((sz, sz), Image.LANCZOS)
|
||
bg = Image.new("RGBA", (sz, sz), (255, 255, 255, 255))
|
||
bg.paste(img, (0, 0))
|
||
p = out_dir / name
|
||
bg.save(p)
|
||
imgs[sz] = bg
|
||
print(f" {p} ({sz}×{sz})")
|
||
|
||
ico = out_dir / "favicon.ico"
|
||
imgs[16].save(ico, format="ICO", sizes=[(16,16),(32,32),(48,48)],
|
||
append_images=[imgs[32], imgs[48]])
|
||
print(f" {ico} (multi-size: 16+32+48)")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--out", default="/tmp/alpinux-static-assets",
|
||
help="Répertoire de sortie (défaut: /tmp/alpinux-static-assets)")
|
||
args = parser.parse_args()
|
||
|
||
out_dir = Path(args.out)
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
print("Génération des assets Alpinux...")
|
||
build_logo(out_dir)
|
||
build_favicons(out_dir)
|
||
print(f"\nFichiers dans : {out_dir}")
|
||
print("À uploader sur : static.alpinux.org/logo/")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|