created variable title svg & png export module

This commit is contained in:
2026-09-01 15:11:05 +02:00
parent 882164231f
commit 7b8d66edb7
14 changed files with 845 additions and 2 deletions
@@ -0,0 +1,185 @@
# Le Shed SVG Export
Exporte en SVG (vectoriel) et PNG (haute définition) la typographie
générative lettre par lettre du thème `leshed` : le logo du site et tout
titre marqué `.variable-title` — tel qu'affiché à l'instant précis du clic
(tirage de graisses/italiques en cours, retours à ligne, mise en majuscule
CSS…), pas une version figée/officielle.
## Contexte
Le thème `leshed` découpe ces textes en spans par caractère (Splitting.js)
et tire aléatoirement, à chaque affichage, une graisse (`font-variation-settings:
'wght'`, police variable Epilogue) et un état italique par lettre — voir
`styleWordChars()` dans `assets/js/main.js` du thème. Rien n'est jamais
figé côté thème ; ce module permet de capturer et exporter un tirage
précis pour un usage print.
## Ce que ce module ne fait pas
- Il ne modifie rien dans le thème `leshed`. Il consomme uniquement un
contrat DOM déjà exposé par le thème : la classe `.variable-title` sur
un élément, et des spans `.char` à l'intérieur, stylés inline en
`font-variation-settings` / `font-style`. Si le thème actif n'expose
jamais cette structure, le module reste installable mais n'a aucun
effet visible.
- Il n'exporte pas de texte libre/arbitraire — seulement ce que le thème a
déjà marqué `.variable-title`. Aujourd'hui, ça se limite au logo du site
et aux titres de node "projet" (voir le sélecteur dans `initTitles()` du
thème) : pas encore les pages de taxonomie ni les autres types de
contenu, faute de marquage côté thème.
- Il ne vendorise aucune police. Voir "Résolution dynamique de la police"
ci-dessous.
- Il ne vendorise pas non plus son moteur de rendu de police (harfbuzzjs) :
installé via Composer, pas commité dans les sources du module. Voir
"Installation" ci-dessous.
## Architecture
Toute la conversion texte → tracés vectoriels se fait **côté client**,
dans le navigateur. Le module Drupal ne fait que :
1. Déclarer la permission `export leshed typography`.
2. Décider, dans `hook_page_attachments()` (`leshed_svg_export.module`),
si la librairie JS est attachée à la page — uniquement si l'utilisateur
courant a la permission. Un utilisateur non autorisé ne charge donc
strictement aucun asset (wasm, police, JS) de ce module.
3. Fournir ses propres fichiers JS/CSS et l'URL vers le moteur `harfbuzzjs`
installé par ailleurs via Composer (voir "Installation").
## Installation
Le moteur `harfbuzzjs` (WebAssembly, MIT) n'est **pas commité** dans ce
module — ça paraîtrait bizarre de vendoriser un moteur générique dans les
sources d'un module métier, et ça découplerait sa mise à jour du reste du
projet. Il est déclaré comme dépendance Composer, exactement comme les
modules contrib :
- `composer.json` déclare le dépôt `https://asset-packagist.org` (miroir
Composer du registre npm) et requiert `npm-asset/harfbuzzjs`, ainsi que
`oomphinc/composer-installers-extender` (nécessaire pour que
`composer/installers` sache installer un paquet de type `npm-asset`
package marqué "abandonné" sur Packagist mais toujours fonctionnel ;
aucun remplaçant n'est proposé à ce jour, à surveiller).
- `extra.installer-types`/`installer-paths` mappent `type:npm-asset` vers
`web/libraries/{$name}`, comme le fait déjà `type:drupal-library`.
- Résultat : `composer install`/`composer update` installe `harfbuzzjs`
dans `web/libraries/harfbuzzjs/` — **aucune étape d'installation
supplémentaire** par rapport au reste du projet.
- `leshed_svg_export.module` expose l'URL correspondante
(`web/libraries/harfbuzzjs/dist/index.mjs`) via
`drupalSettings.leshedSvgExport.harfbuzzUrl`.
**Prérequis serveur (nginx)** : le fichier `dist/index.mjs` du paquet doit
être servi avec un type MIME JavaScript. Beaucoup de configurations nginx
(dont celle de ce projet à l'origine) n'ont pas de mapping pour l'extension
`.mjs` et répondent `application/octet-stream`, que les navigateurs
refusent de charger comme module ES (Firefox notamment). Ajouter dans
`Docker/nginx/default.conf` :
```nginx
location ~ \.mjs$ {
default_type application/javascript;
}
```
### `js/svg-export.js`
Comportement Drupal (`Drupal.behaviors.leshedSvgExport`) :
- Surveille le DOM via `MutationObserver` pour repérer tout élément
`.variable-title` (existant ou ajouté/marqué plus tard — pas d'hypothèse
d'ordre de chargement avec le script du thème).
- Pour chaque titre trouvé, crée un petit badge avec deux boutons ("SVG",
"PNG") et l'ajoute comme **enfant DOM du titre lui-même**
(`titleEl.appendChild(widget)`), positionné en `position: absolute`
(`top: 0; left: 0` — superposé au coin haut-gauche du titre, jamais au
reste). Le titre reçoit aussi la classe `leshed-svg-export-target` (marqueur
propre au module, indépendant de `.variable-title` — utilisé par le CSS
ci-dessous, et disponible pour cibler "un titre équipé du bouton export"
sans dépendre du nom de classe du thème). Il reçoit `position: relative`
en JS uniquement s'il ne l'a pas déjà (visuellement neutre, ne bouge/ne
redimensionne rien) pour servir de contexte de positionnement. Étant en
`position: absolute`, le badge est retiré du flux normal : il ne peut
donc jamais influencer la mise en page du front, et le navigateur le
garde attaché au titre automatiquement au scroll/resize — aucune
synchronisation JS de position n'est nécessaire.
- Visibilité **entièrement en CSS**
(`.leshed-svg-export-target:hover > .leshed-svg-export`, `:focus-within`,
voir `css/svg-export.css`), sans aucun JS de détection de
survol, et **sans jamais recouvrir le reste du titre** d'un calque
invisible — un titre peut être un vrai lien ailleurs (le logo, un
teaser) et doit rester entièrement cliquable.
**Limite connue, assumée** : ce hover ne peut pas se déclencher sur un
titre que le thème a lui-même rendu non interactif
(`pointer-events: none` — ex. le h2 décoratif surdimensionné d'une page
"projet" en vue complète). Un élément à `pointer-events: none` ne reçoit
jamais d'événement de survol, quoi qu'on écrive en CSS ; le contourner
demanderait soit de modifier le CSS du thème, soit de recouvrir le titre
d'un calque — les deux sont exclus. Sur tout titre sans cette règle
(l'immense majorité), le survol fonctionne normalement.
- Au clic : capture la largeur du conteneur du titre, puis pour chaque
`.char` dans l'ordre du DOM — position (`getBoundingClientRect()`),
style calculé (`font-family`, `font-style`, tous les axes de
`font-variation-settings`, `text-transform`) — et convertit `.char.textContent`
selon le `text-transform` réellement appliqué (uppercase / lowercase /
capitalize, ce dernier basé sur le premier `.char` de chaque `.word`)
avant de demander le tracé du bon caractère.
- Assemble le SVG (fond transparent, remplissage noir uniquement — décision
volontaire, pas configurable) et déclenche le téléchargement (`Blob` +
`<a download>`).
- Pour le PNG : rasterise le SVG assemblé via un `<canvas>` à ×4 la taille
affichée (`PNG_SCALE`), puis `canvas.toBlob('image/png')`.
### `js/harfbuzz-export.js`
Résolution dynamique de la police et extraction des tracés, via
[harfbuzzjs](https://github.com/harfbuzz/harfbuzzjs) (HarfBuzz compilé en
WebAssembly, MIT — installé via Composer, voir "Installation" ci-dessus).
`loadHarfbuzz(harfbuzzUrl)` importe directement l'URL fournie par le module
PHP (`web/libraries/harfbuzzjs/dist/index.mjs`).
**Aucune police n'est embarquée dans ce module.** Pour chaque caractère :
1. Lit son style calculé (`font-family`, `font-style`, les axes de
`font-variation-settings`).
2. Cherche, dans `document.styleSheets` (CSSOM), la règle `@font-face`
dont `font-family`/`font-style` correspondent, et en extrait l'URL du
descripteur `src`.
3. `fetch()` cette URL (peu importe le conteneur réel — ttf/otf/woff2 —
HarfBuzz travaille sur les octets bruts), met en cache par URL.
4. Instancie la police à la variation exacte lue (`font.setVariations()`),
shape le caractère, extrait son tracé (`font.glyphToPath()`).
Conséquence : si le thème change de police demain (nouvelle famille,
nouveaux axes variables, retour à une police statique), l'export suit
automatiquement — rien à modifier dans ce module.
## Permission
`export leshed typography` — à assigner aux rôles voulus (aucun rôle par
défaut). Contrôle uniquement l'attachement de la librairie ; ne dépend
d'aucune configuration de thème.
## Style du widget
Volontairement **non harmonisé avec le thème front** : le widget imite le
thème d'administration Gin en mode sombre (`css/svg-export.css`), avec les
valeurs de couleur copiées directement des tokens Gin (`--gin-bg-app`,
accent "blue" en mode sombre, etc. — voir
`web/themes/contrib/gin/dist/css/theme/{variables,accent}.css`), en dur,
puisque le CSS de Gin n'est pas chargé sur les pages front.
## Limites connues
- Sur un titre que le thème a rendu non interactif (`pointer-events: none`
— ex. le h2 décoratif de la page "projet" en vue complète), le bouton
n'apparaît jamais au survol souris (il reste accessible au clavier via
Tab/`:focus-within`). Voir la note dans `js/svg-export.js`.
- Les césures logicielles insérées par le thème (`hyphenateWord()`, U+00AD)
ne sont pas des `.char` et ne sont donc jamais exportées.
- Positionnement du texte basé sur `getBoundingClientRect()` +
métriques de police (ascender/unitsPerEm) plutôt que sur une mesure
exacte de ligne de base — fidèle en pratique mais pas garanti au pixel
près sur toutes les combinaisons police/navigateur.