Files
drupal-leshed/web/modules/custom/leshed_svg_export/README.md
T

9.5 KiB
Raw Blame History

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 :

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 (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.