spb/uqo-imm1003
Public
JavaScript 68%
CSS 32%
1"""brand.py — identité UQO : logo, favicons, icônes tactiles, image de partage (Open Graph) nettes."""2from __future__ import annotations34import base645import os6import subprocess78from PIL import Image, ImageDraw, ImageFont910HERE = os.path.dirname(os.path.abspath(__file__))11SITE = os.path.abspath(os.path.join(HERE, ".."))12BRAND = os.path.join(SITE, "assets", "brand")13UQO_TEAL = (15, 97, 128) # #0F6180 — bleu UQO du logo14NAVY = (9, 26, 51)15GOLD = (204, 164, 36)16INK = (31, 36, 48)17GRIS = (96, 102, 112)181920def font(weight: str, size: int) -> ImageFont.FreeTypeFont:21 name = {"bold": "FiraSans-Bold.otf", "semibold": "FiraSans-SemiBold.otf", "medium": "FiraSans-Medium.otf", "regular": "FiraSans-Regular.otf"}[weight]22 try:23 path = subprocess.run(["kpsewhich", name], stdout=subprocess.PIPE, text=True, timeout=20).stdout.strip()24 if path:25 return ImageFont.truetype(path, size)26 except Exception: # noqa: BLE00127 pass28 for cand in ["/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf"]:29 if os.path.exists(cand):30 return ImageFont.truetype(cand, size, index=1 if weight in ("bold", "semibold") and cand.endswith(".ttc") else 0)31 return ImageFont.load_default()323334def logo(color: tuple | None = None) -> Image.Image:35 """Logo UQO (mot-symbole) recoloré ; conserve l'alpha."""36 im = Image.open(os.path.join(BRAND, "uqo-logo-wiki.png")).convert("RGBA")37 if color is None:38 return im39 r, g, b, a = im.split()40 solid = Image.new("RGBA", im.size, color + (255,))41 solid.putalpha(a)42 return solid434445def rounded_tile(size: int, color: tuple, radius_ratio: float = .22) -> Image.Image:46 im = Image.new("RGBA", (size, size), (0, 0, 0, 0))47 d = ImageDraw.Draw(im)48 d.rounded_rectangle((0, 0, size - 1, size - 1), radius=int(size * radius_ratio), fill=color + (255,))49 return im505152def make_icons(out_dir: str):53 """favicon-32/64/192/512, apple-touch-icon (180), favicon.svg (vectoriel + logo intégré)."""54 os.makedirs(out_dir, exist_ok=True)55 white = logo((255, 255, 255))56 big = 102457 tile = rounded_tile(big, UQO_TEAL)58 lw = int(big * .78)59 lg = white.resize((lw, int(white.height * lw / white.width)), Image.LANCZOS)60 tile.alpha_composite(lg, ((big - lg.width) // 2, (big - lg.height) // 2 - int(big * .02)))61 # barre or discrète en bas62 d = ImageDraw.Draw(tile)63 d.rounded_rectangle((int(big * .3), int(big * .8), int(big * .7), int(big * .8) + int(big * .035)), radius=20, fill=GOLD + (255,))64 for s, name in ((32, "favicon-32.png"), (64, "favicon-64.png"), (180, "apple-touch-icon.png"), (192, "icon-192.png"), (512, "icon-512.png")):65 tile.resize((s, s), Image.LANCZOS).save(os.path.join(out_dir, name), optimize=True)66 tile.resize((256, 256), Image.LANCZOS).save(os.path.join(out_dir, "favicon.ico"), sizes=[(16, 16), (32, 32), (48, 48), (64, 64)])67 # SVG : tuile vectorielle + logo PNG haute résolution intégré (net sur tous les écrans)68 buf = os.path.join(out_dir, "_logo_white.png")69 white.save(buf, optimize=True)70 b64 = base64.b64encode(open(buf, "rb").read()).decode()71 os.remove(buf)72 h = white.height / white.width73 svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="22" fill="#0F6180"/>'74 f'<image href="data:image/png;base64,{b64}" x="11" y="{50 - 78 * h / 2 - 2:.1f}" width="78" height="{78 * h:.1f}"/>'75 f'<rect x="30" y="80" width="40" height="3.5" rx="1.75" fill="#CCA424"/></svg>')76 open(os.path.join(out_dir, "favicon.svg"), "w", encoding="utf-8").write(svg)77 # logos pour la page (teal et blanc), 2x78 logo().save(os.path.join(out_dir, "uqo-logo.png"), optimize=True)79 white.save(os.path.join(out_dir, "uqo-logo-white.png"), optimize=True)808182def make_og(course: dict, stats: dict, out_path: str):83 """Image de partage 1200×630 (rendue en 2400×1260 puis réduite : texte net)."""84 S = 285 W, H = 1200 * S, 630 * S86 im = Image.new("RGB", (W, H), (255, 255, 255))87 d = ImageDraw.Draw(im)88 # bandeau gauche UQO et fond doux89 d.rectangle((0, 0, 16 * S, H), fill=UQO_TEAL)90 d.rectangle((16 * S, 0, 22 * S, H), fill=(15, 97, 128, 60))91 for i in range(60):92 y = int(H * (1 - i / 60))93 shade = 255 - int(6 * (i / 60))94 d.line((22 * S, y, W, y), fill=(shade - 8, shade - 3, shade))95 # cercles décoratifs96 for (cx, cy, r, col) in ((W - 120 * S, 40 * S, 200 * S, (15, 97, 128)), (W - 40 * S, 200 * S, 300 * S, (42, 134, 176)), (W - 300 * S, 330 * S, 70 * S, GOLD)):97 d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=col + (0,), width=2 * S)98 # logo UQO99 lg = logo(UQO_TEAL)100 lw = 300 * S101 lg = lg.resize((lw, int(lg.height * lw / lg.width)), Image.LANCZOS)102 im.paste(lg, (70 * S, 56 * S), lg)103 d.text((70 * S + lw + 30 * S, 62 * S), "Université du Québec\nen Outaouais", font=font("medium", 30 * S), fill=GRIS, spacing=6 * S)104 # surtitre105 d.text((70 * S, 236 * S), f"NOTES DE COURS INTERACTIVES · {course['term'].upper()}", font=font("bold", 24 * S), fill=GOLD)106 d.rounded_rectangle((70 * S, 272 * S, 130 * S, 277 * S), radius=3 * S, fill=GOLD)107 # code et titre108 d.text((70 * S, 292 * S), course["code"], font=font("bold", 96 * S), fill=UQO_TEAL)109 title_font = font("semibold", 50 * S)110 title = course["title"]111 # retour à la ligne simple112 words, lines, cur = title.split(), [], ""113 for w in words:114 t = (cur + " " + w).strip()115 if d.textlength(t, font=title_font) > 1000 * S:116 lines.append(cur)117 cur = w118 else:119 cur = t120 lines.append(cur)121 y = 410 * S122 for ln in lines[:2]:123 d.text((70 * S, y), ln, font=title_font, fill=INK)124 y += 60 * S125 # stats126 sf = font("regular", 26 * S)127 stat = f"14 séances · {stats.get('def', 0)} définitions · {stats.get('form', 0)} formules · {stats.get('exo', 0)} exercices corrigés · {stats.get('quiz', 0)} questions de quiz"128 d.text((70 * S, 556 * S), stat, font=sf, fill=GRIS)129 d.text((70 * S, 592 * S), f"Simon-Pierre Boucher · {course['domain']}", font=font("medium", 24 * S), fill=UQO_TEAL)130 im.resize((1200, 630), Image.LANCZOS).save(out_path, optimize=True)131132133if __name__ == "__main__":134 make_icons("/tmp/uqo-test/brand")135 print("ok")136