From 1b7bb31684e308b24ac64737a6638c11f2cd6e09 Mon Sep 17 00:00:00 2001 From: Philippe Vienne Date: Sun, 27 Sep 2026 12:43:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(logos):=20texte=20des=20logos=20converti?= =?UTF-8?q?=20en=20trac=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chargé par , un SVG n'a pas accès aux polices de la page : le texte des logos (« OTSPI » et le sous-titre, en Ubuntu Sans) était rendu avec la police installée sur le poste, Liberation Sans ou celle du système à défaut, et changeait de largeur d'un navigateur à l'autre. Les sources restent en texte dans scripts/logos/ ; scripts/build_logos.py génère les logos de assets/ en convertissant chaque en tracés, positionnés par HarfBuzz segment par segment comme dans Chrome. Rendu comparé au pixel près sur un poste disposant de la police : seuls les bords des lettres diffèrent (lissage). Les logos portent un . Co-authored-by: Claude <noreply@anthropic.com> --- README.md | 5 + assets/logo-horizontal-transparent-dark.svg | 12 +- assets/logo-horizontal.svg | 12 +- assets/logo-vertical-dark.svg | 12 +- assets/logo-vertical.svg | 12 +- scripts/build_logos.py | 142 ++++++++++++++++++ .../logo-horizontal-transparent-dark.svg | 39 +++++ scripts/logos/logo-horizontal.svg | 39 +++++ scripts/logos/logo-vertical-dark.svg | 38 +++++ scripts/logos/logo-vertical.svg | 37 +++++ 10 files changed, 320 insertions(+), 28 deletions(-) create mode 100755 scripts/build_logos.py create mode 100644 scripts/logos/logo-horizontal-transparent-dark.svg create mode 100644 scripts/logos/logo-horizontal.svg create mode 100644 scripts/logos/logo-vertical-dark.svg create mode 100644 scripts/logos/logo-vertical.svg diff --git a/README.md b/README.md index 95e759c..af7f7cc 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Il s'adresse aux partenaires potentiels : hébergeurs, financeurs, laboratoires, | `scripts/signataires.py` | Génération de la liste publique des signataires (non déployé) | | `scripts/build_og.py` | Génération des images de partage de l'accueil (non déployé) | | `scripts/verifier_site.py` | Contrôle du menu commun et des versions des feuilles de style et scripts (non déployé) | +| `scripts/build_logos.py`, `scripts/logos/` | Génération des logos de `assets/` (texte converti en tracés) à partir de leurs sources (non déployé) | | `style.css` | Feuille de style | | `script.js` | Menu de navigation mobile | | `assets/signataires.js` | Liste des signataires à jour en direct sur les pages du manifeste | @@ -87,6 +88,10 @@ Le retrait d'une signature dans l'application supprime la signature à la source `python3 scripts/build_og.py` régénère les quatre images de partage de `assets/og/` (`og-fr.png`, `og-en.png` pour l'accueil, `og-manifeste.png`, `og-manifesto.png` pour le manifeste ; 1200 × 630 px, utilisées par les balises Open Graph et Twitter ; Chrome ou Chromium requis). Les textes de l'accueil doivent rester alignés sur le hero de la page d'accueil, ceux du manifeste sur la page du manifeste. Les réseaux sociaux mettent les aperçus en cache : après un changement, forcer le rafraîchissement depuis leurs outils de débogage. +## Logos + +Les logos `logo-horizontal*.svg` et `logo-vertical*.svg` de `assets/` sont générés : ne pas les modifier directement. Leurs sources, dans `scripts/logos/`, écrivent « OTSPI » et le sous-titre en texte SVG (Ubuntu Sans). Chargé par `<img>`, un SVG n'a pas accès aux polices de la page : chaque navigateur prenait la police installée sur le poste, et le texte changeait de largeur d'un système à l'autre. `python3 scripts/build_logos.py` convertit ce texte en tracés, positionnés par HarfBuzz comme le ferait un navigateur disposant de la police (prérequis : `pip install fonttools uharfbuzz` et la police Ubuntu Sans variable, paquet `fonts-ubuntu`, ou l'option `--police`). + ## Plaquette d'une page `python3 scripts/build_plaquette.py` régénère `assets/docs/otspi-plaquette-fr.pdf` et `otspi-plaquette-en.pdf` (Chrome ou Chromium requis ; sources dans `scripts/plaquette/`). Les chiffres sont ceux de la page d'accueil : les mettre à jour ensemble. diff --git a/assets/logo-horizontal-transparent-dark.svg b/assets/logo-horizontal-transparent-dark.svg index ed032e5..d8c2aaa 100644 --- a/assets/logo-horizontal-transparent-dark.svg +++ b/assets/logo-horizontal-transparent-dark.svg @@ -1,4 +1,5 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 120" width="540" height="120"> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 120" width="540" height="120" role="img" aria-label="OTSPI — Open Trusted Service Provider Initiative"> + <title>OTSPI — Open Trusted Service Provider Initiative @@ -29,11 +30,8 @@ stroke-linecap="round" stroke-linejoin="round" /> - - OTSPI - - - OPEN TRUSTED SERVICE PROVIDER INITIATIVE - + + + diff --git a/assets/logo-horizontal.svg b/assets/logo-horizontal.svg index e3c7b86..7c6908d 100644 --- a/assets/logo-horizontal.svg +++ b/assets/logo-horizontal.svg @@ -1,4 +1,5 @@ - + + OTSPI — Open Trusted Service Provider Initiative @@ -29,11 +30,8 @@ stroke-linecap="round" stroke-linejoin="round" /> - - OTSPI - - - OPEN TRUSTED SERVICE PROVIDER INITIATIVE - + + + diff --git a/assets/logo-vertical-dark.svg b/assets/logo-vertical-dark.svg index 93009c3..1b83566 100644 --- a/assets/logo-vertical-dark.svg +++ b/assets/logo-vertical-dark.svg @@ -1,4 +1,5 @@ - + + OTSPI — Open Trusted Service Provider Initiative @@ -29,10 +30,7 @@ stroke-width="14.40" stroke-linecap="round" stroke-linejoin="round" /> - - OTSPI - - - OPEN TRUSTED SERVICE PROVIDER INITIATIVE - + + + diff --git a/assets/logo-vertical.svg b/assets/logo-vertical.svg index 8a5448d..b0c9579 100644 --- a/assets/logo-vertical.svg +++ b/assets/logo-vertical.svg @@ -1,4 +1,5 @@ - + + OTSPI — Open Trusted Service Provider Initiative @@ -28,10 +29,7 @@ stroke-width="14.40" stroke-linecap="round" stroke-linejoin="round" /> - - OTSPI - - - OPEN TRUSTED SERVICE PROVIDER INITIATIVE - + + + diff --git a/scripts/build_logos.py b/scripts/build_logos.py new file mode 100755 index 0000000..a207200 --- /dev/null +++ b/scripts/build_logos.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: EUPL-1.2 +"""Génère les logos de assets/ à partir de leurs sources de scripts/logos/ (non déployé). + +Les sources écrivent « OTSPI » et le sous-titre en texte SVG, en Ubuntu Sans. Chargé par , +un SVG n'a pas accès aux polices de la page : chaque navigateur prenait la police installée +localement (Ubuntu Sans, sinon Liberation Sans, sinon celle du système) et le texte changeait de +largeur d'un poste à l'autre. Ce script remplace chaque par les tracés des glyphes, +positionnés par HarfBuzz (crénage compris) comme le ferait un navigateur disposant de la police. + +Prérequis : fontTools et uharfbuzz (pip install fonttools uharfbuzz), police Ubuntu Sans +variable (paquet fonts-ubuntu, ou https://github.com/canonical/Ubuntu-Sans-fonts). + + python3 scripts/build_logos.py [--police chemin/UbuntuSans[wdth,wght].ttf] +""" +import argparse +import html +import io +import re +import sys +from pathlib import Path + +import uharfbuzz as hb +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont + +ROOT = Path(__file__).resolve().parent.parent +SOURCES = ROOT / "scripts" / "logos" +POLICE = Path("/usr/share/fonts/truetype/ubuntu/UbuntuSans[wdth,wght].ttf") +TITRE = "OTSPI — Open Trusted Service Provider Initiative" + +TEXTE = re.compile(r"(?P[ \t]*)[^>]*)>(?P.*?)", re.S) +TSPAN = re.compile(r"]*)>(.*?)", re.S) +ATTR = re.compile(r'([\w:-]+)="([^"]*)"') + + +class Police: + def __init__(self, chemin): + self.donnees = Path(chemin).read_bytes() + self.instances = {} + + def instance(self, graisse): + if graisse not in self.instances: + ttf = instantiateVariableFont(TTFont(io.BytesIO(self.donnees)), {"wght": graisse, "wdth": 100}) + tampon = io.BytesIO() + ttf.save(tampon) + face = hb.Face(tampon.getvalue()) + self.instances[graisse] = (ttf, hb.Font(face), face.upem) + return self.instances[graisse] + + +def segments(corps, remplissage): + """Texte et couleur de chaque segment, espaces réduits comme le fait SVG par défaut.""" + morceaux, pos = [], 0 + for m in TSPAN.finditer(corps): + if m.start() > pos: + morceaux.append((corps[pos:m.start()], remplissage)) + morceaux.append((m.group(2), dict(ATTR.findall(m.group(1))).get("fill", remplissage))) + pos = m.end() + morceaux.append((corps[pos:], remplissage)) + texte = "".join(t for t, _ in morceaux) + debut = len(texte) - len(texte.lstrip()) + fin = len(texte.rstrip()) + resultat, index = [], 0 + for t, couleur in morceaux: + garde = t[max(0, debut - index): max(0, fin - index)] + index += len(t) + garde = html.unescape(re.sub(r"\s+", " ", garde)) + if garde: + resultat.append((garde, couleur)) + return resultat + + +def en_traces(attrs, corps, police): + a = dict(ATTR.findall(attrs)) + taille = float(a["font-size"]) + espacement = float(a.get("letter-spacing", 0)) + ttf, font, upem = police.instance(int(a.get("font-weight", 400))) + echelle = taille / upem + + # Chaque est mis en forme à part, comme le font les navigateurs : pas de crénage + # entre deux segments (le « O » bleu et le « T » d'OTSPI). + glyphes_places = [] + for texte, couleur in segments(corps, a.get("fill", "#000000")): + tampon = hb.Buffer() + tampon.add_str(texte) + tampon.guess_segment_properties() + hb.shape(font, tampon, {"kern": True, "liga": True}) + glyphes_places += [(info.codepoint, pos, couleur) for info, pos in zip(tampon.glyph_infos, tampon.glyph_positions)] + + # letter-spacing s'ajoute après chaque caractère, dernier compris, comme dans les navigateurs. + largeur = sum(pos.x_advance * echelle + espacement for _, pos, _ in glyphes_places) + x = float(a.get("x", 0)) + if a.get("text-anchor") == "middle": + x -= largeur / 2 + elif a.get("text-anchor") == "end": + x -= largeur + y = float(a.get("y", 0)) + + glyphes = ttf.getGlyphSet() + noms = ttf.getGlyphOrder() + parCouleur = {} + for glyphe, pos, couleur in glyphes_places: + stylo = SVGPathPen(glyphes, ntos=lambda v: f"{v:.2f}".rstrip("0").rstrip(".")) + gx, gy = x + pos.x_offset * echelle, y - pos.y_offset * echelle + glyphes[noms[glyphe]].draw(TransformPen(stylo, (echelle, 0, 0, -echelle, gx, gy))) + if stylo.getCommands(): + parCouleur.setdefault(couleur, []).append(stylo.getCommands()) + x += pos.x_advance * echelle + espacement + return [(couleur, "".join(d)) for couleur, d in parCouleur.items()] + + +def convertir(source, police): + svg = source.read_text(encoding="utf-8") + + def remplacer(m): + indent = m.group("indent") + return "\n".join(f'{indent}' for c, d in en_traces(m.group("attrs"), m.group("corps"), police)) + + svg = TEXTE.sub(remplacer, svg) + # Le texte ayant disparu, le nom est porté par un titre (lu si le SVG est ouvert seul). + svg = re.sub(r"(]*?)>", rf'\1 role="img" aria-label="{TITRE}">\n {TITRE}', svg, count=1) + return svg + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--police", default=POLICE, help="police Ubuntu Sans variable") + args = parser.parse_args() + if not Path(args.police).is_file(): + sys.exit(f"Police introuvable : {args.police} (option --police)") + police = Police(args.police) + for source in sorted(SOURCES.glob("*.svg")): + cible = ROOT / "assets" / source.name + cible.write_text(convertir(source, police), encoding="utf-8") + print(f"{cible.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/logos/logo-horizontal-transparent-dark.svg b/scripts/logos/logo-horizontal-transparent-dark.svg new file mode 100644 index 0000000..ed032e5 --- /dev/null +++ b/scripts/logos/logo-horizontal-transparent-dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + OTSPI + + + OPEN TRUSTED SERVICE PROVIDER INITIATIVE + + + diff --git a/scripts/logos/logo-horizontal.svg b/scripts/logos/logo-horizontal.svg new file mode 100644 index 0000000..e3c7b86 --- /dev/null +++ b/scripts/logos/logo-horizontal.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + OTSPI + + + OPEN TRUSTED SERVICE PROVIDER INITIATIVE + + + diff --git a/scripts/logos/logo-vertical-dark.svg b/scripts/logos/logo-vertical-dark.svg new file mode 100644 index 0000000..93009c3 --- /dev/null +++ b/scripts/logos/logo-vertical-dark.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + OTSPI + + + OPEN TRUSTED SERVICE PROVIDER INITIATIVE + + diff --git a/scripts/logos/logo-vertical.svg b/scripts/logos/logo-vertical.svg new file mode 100644 index 0000000..8a5448d --- /dev/null +++ b/scripts/logos/logo-vertical.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + OTSPI + + + OPEN TRUSTED SERVICE PROVIDER INITIATIVE + +