# 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` + ``). - Pour le PNG : rasterise le SVG assemblé via un `` à ×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.