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-titlesur un élément, et des spans.charà l'intérieur, stylés inline enfont-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 dansinitTitles()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 :
- Déclarer la permission
export leshed typography. - 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. - Fournir ses propres fichiers JS/CSS et l'URL vers le moteur
harfbuzzjsinstallé 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.jsondéclare le dépôthttps://asset-packagist.org(miroir Composer du registre npm) et requiertnpm-asset/harfbuzzjs, ainsi queoomphinc/composer-installers-extender(nécessaire pour quecomposer/installerssache installer un paquet de typenpm-asset— package marqué "abandonné" sur Packagist mais toujours fonctionnel ; aucun remplaçant n'est proposé à ce jour, à surveiller).extra.installer-types/installer-pathsmappenttype:npm-assetversweb/libraries/{$name}, comme le fait déjàtype:drupal-library.- Résultat :
composer install/composer updateinstalleharfbuzzjsdansweb/libraries/harfbuzzjs/— aucune étape d'installation supplémentaire par rapport au reste du projet. leshed_svg_export.moduleexpose l'URL correspondante (web/libraries/harfbuzzjs/dist/index.mjs) viadrupalSettings.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
MutationObserverpour 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é enposition: absolute(top: 0; left: 0— superposé au coin haut-gauche du titre, jamais au reste). Le titre reçoit aussi la classeleshed-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çoitposition: relativeen JS uniquement s'il ne l'a pas déjà (visuellement neutre, ne bouge/ne redimensionne rien) pour servir de contexte de positionnement. Étant enposition: 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, voircss/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: nonene 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 : pour chaque
.chardans l'ordre du DOM — position (getBoundingClientRect()), style calculé (font-family,font-style, tous les axes defont-variation-settings,text-transform) — et convertit.char.textContentselon letext-transformréellement appliqué (uppercase / lowercase / capitalize, ce dernier basé sur le premier.charde chaque.word) avant de demander le tracé du bon caractère. Les dimensions du canevas sont mesurées à partir de l'étendue réelle de ces.char(min/max de leursgetBoundingClientRect()), pas depuistitleEl.getBoundingClientRect(): certains titres du thème ont unmax-heightplus petit que leur contenu une fois retourné à la ligne, avecoverflow: visible(rien n'est coupé visuellement à l'écran) — dans ce cas, la boîte du titre lui-même ne reflète que la hauteur contrainte, pas l'étendue réelle du texte qui déborde ; s'y fier tronquait les titres sur plusieurs lignes. -
L'échelle de rendu tient compte de la transformation CSS du titre (
getScaleFactor()) : le thème anime certains titres entransform: scale()au scroll (initPageFull()dansmain.js), etgetComputedStyle().fontSizene reflète jamais cette transformation, contrairement àgetBoundingClientRect()— utiliserfont-sizeseul rendait les glyphes trop grands par rapport à leur espacement mesuré, un écart qui s'accumulait lettre après lettre et débordait du canevas, en bas et à droite (fin de ligne) surtout. Le facteur d'échelle est lu directement depuis la matricetransformcalculée du titre (DOMMatrix), pas déduit en comparant la largeur mesurée d'un.charà son avance de glyphe HarfBuzz — une première version faisait ça, mais le thème pose aussiletter-spacing: -0.1emsur chaque lettre, inclus dans la largeur mesurée, ce qui faussait le calcul sur tous les caractères (pas seulement sous transformation active), jusqu'à rendre certaines lettres invisibles. -
Assemble le SVG (fond transparent, remplissage noir uniquement — décision volontaire, pas configurable) et déclenche le téléchargement (
Blob+<a download>). Une marge égale à 30 % de la plus grande taille de police effective (issue du point précédent, pas defont-sizebrut) du titre (ITALIC_OVERFLOW_MARGIN_RATIO) est ajoutée tout autour du canevas : les lettres italiques débordent de leur boîte d'avance nominale (surtout en haut des hampes), etgetBoundingClientRect()du titre ne tient pas compte de ce débordement — sans cette marge, un cadrage au plus juste coupe parfois ces lettres. -
Pour le PNG : rasterise le SVG assemblé via un
<canvas>à ×4 la taille affichée (PNG_SCALE), puiscanvas.toBlob('image/png'). Cette échelle est réduite (jamais augmentée) pour qu'aucune dimension du canevas ne dépasseMAX_CANVAS_DIMENSION(3000px, largement suffisant pour un usage print réel — environ 25cm à 300 DPI). Nécessaire pour un titre long sur plusieurs lignes en police énorme (12em) : ×4 la taille CSS affichée n'a de sens que pour un texte court comme le logo — pour un long titre, la taille "à l'écran" est déjà immense, donc ×4 produisait des fichiers démesurés (et pouvait dépasser la taille de<canvas>que le navigateur autorise).
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 :
- Lit son style calculé (
font-family,font-style, les axes defont-variation-settings). - Cherche, dans
document.styleSheets(CSSOM), la règle@font-facedontfont-family/font-stylecorrespondent, et en extrait l'URL du descripteursrc. fetch()cette URL (peu importe le conteneur réel — ttf/otf/woff2 — HarfBuzz travaille sur les octets bruts), met en cache par URL.- 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 dansjs/svg-export.js. - Les césures logicielles insérées par le thème (
hyphenateWord(), U+00AD) ne sont pas des.charet 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.