- Dropflow est un moteur de mise en page CSS conçu pour explorer les standards CSS fondamentaux comme inline, block, float, positioning, et plus tard les tables ; flexbox et grid ne sont pas visés
- Il peut être utilisé avec Node et node-canvas pour la génération de PDF et d’images côté backend, et dans le navigateur pour afficher du texte enrichi avec retour à la ligne dans un canvas
- La mise en page du texte prend en charge le texte bidirectionnel et RTL, le fallback de police au niveau des graphèmes, les signes diacritiques colorés, l’enregistrement de polices OpenType/TrueType, et le shaping basé sur HarfBuzz
- La prise en charge CSS couvre plus de 30 propriétés ;
float,clear,display: inline-block,position: relative,overflow,z-index,zoomfonctionnent, tandis quetable,absolute,fixed,transform, etc. sont prévus - Les meilleures performances sont obtenues avec l’API hyperscript et des objets de style réutilisables : sur un MacBook Pro 2019, The Little Prince, avec plus de 500 paragraphes, est converti de HTML en image en moins de 160 ms
Étendue de la mise en page CSS prise en charge par Dropflow
- Dropflow est un moteur de mise en page CSS créé pour explorer le périmètre des standards CSS fondamentaux
- Les cibles sont inline, block, float, positioning, et plus tard les tables
- flexbox et grid ne sont pas visés
- Il dispose d’une implémentation de mise en page de texte de haute qualité, capable d’afficher de nombreuses langues du monde
- Deux cas d’usage sont mis en avant
- Génération de PDF ou d’images côté backend avec Node et node-canvas
- Rendu de texte enrichi avec retour à la ligne dans un canvas côté navigateur
Fonctionnalités principales
- Plus de 30 propriétés CSS sont prises en charge, y compris des propriétés complexes comme
float - Les fonctionnalités liées au texte sont étendues
- Texte bidirectionnel et texte RTL
- Fallback de police au niveau des graphèmes
- Signes diacritiques colorés
- Gestion souhaitable des retours à la ligne, par exemple en reportant le padding initial à la ligne suivante
- Shaping optimisé
- En plus de HTML/CSS, l’entrée peut aussi passer par l’API hyperscript
h(), qui reçoit les styles sous forme d’objets - Les buffers OpenType/TrueType peuvent être enregistrés, et cet enregistrement est obligatoire
<img>prend en charge JPEG, BMP, PNG et GIF, mais le support peut varier selon le backend- Les styles hérités et calculés ne sont pas recalculés deux fois
- Le projet est entièrement typé, abondamment testé, et vise une exécution rapide
État de la prise en charge CSS
- En inline formatting, les éléments pris en charge incluent
color,direction,font-family,font-size,font-stretch,font-style,font-weight,line-height,text-align,vertical-align,white-space,word-break,overflow-wrap,word-wrap,word-spacing, etc. - En inline formatting,
font-variant,letter-spacing,tab-size,text-decoration,unicode-bidisont prévus - En block formatting,
clearetfloatfonctionnenthorizontal-tb,vertical-lr,vertical-rldewriting-modesont partiellement implémentés- C’est implémenté pour le BFC, mais pas encore pour l’IFC
- Dans boxes and positioning, plusieurs propriétés de boîte et de positionnement fonctionnent
background-clip,background-color,border-color,border-style,border-widthtop,right,bottom,leftbox-sizingdisplay: block,inline,inline-block,flow-root,noneheight,margin,padding,overflow,width,z-index,zoomposition: relative
- Dans boxes and positioning,
display: table,min/max-height,min/max-width,position: absolute,position: fixed,transformsont prévus
Flux d’utilisation de base
- Dropflow fonctionne, comme un navigateur, à partir d’un DOM avec styles hérités et calculés
- Le flux habituel est le suivant
- Enregistrer les polices avec
FontFaceavant la mise en page - Créer les objets de style avec
flow.style() - Créer le DOM avec
flow.h() - Préparer l’arbre à rendre avec
flow.dom() - Effectuer la mise en page et la peinture sur tout le canvas avec
renderToCanvas()
- Enregistrer les polices avec
- Une API HTML est aussi fournie, mais elle n’est recommandée que si les performances ne sont pas importantes ou à des fins d’apprentissage
- Le parsing ajoute du temps supplémentaire et augmente fortement la taille du bundle
- Il est indiqué que le parsing HTML est rapide grâce à @fb55
- Pour le moment, seul l’attribut HTML
styleest pris en charge ;classne fonctionne pas encore
Caractéristiques de performance
- Les performances sont un objectif prioritaire, juste après la correction
- Des chiffres de performance sont fournis sur des exemples
- Conversion de 8 paragraphes, avec plusieurs spans inline et différentes polices, de HTML en image : 9 ms sur un MacBook Pro 2019, 13 ms sur un MacBook Pro 2012
- Conversion de The Little Prince, avec plus de 500 paragraphes, de HTML en image : moins de 160 ms sur un MacBook Pro 2019, moins de 250 ms sur un MacBook Pro 2012
- Génération d’un mot de 10 caractères et exécution de la seule mise en page : moins de 25 µs sur un MacBook Pro 2019, moins de 50 µs sur un MacBook Pro 2012
- Les meilleures performances sont obtenues en construisant directement le DOM avec l’API hyperscript et en évitant l’étape classique de parsing HTML/CSS
- La réutilisation des objets de style apporte encore plus d’avantages
- Le reflow à différentes largeurs est plus rapide que la reconstruction de l’arbre de mise en page
Composition de l’API
- Les étapes de base sont au nombre de deux
- Enregistrement des polices
- Création du DOM via l’API Hyperscript ou l’API Parse
- Pour un usage simple, le DOM peut être rendu directement dans un canvas
renderToCanvas(el, canvas)utilise la largeur et la hauteur du canvas comme taille du viewport pour rendre toute la mise en page
- Avec l’API de plus bas niveau, il est possible de conserver la mise en page
- Chargement des ressources dépendantes
- Création du layout du DOM
- Reflow du layout
- Paint vers une cible comme un canvas HTML5
- Cette approche peut servir à faire un reflow dans d’autres dimensions, à ne pas peindre les zones non visibles, ou à obtenir les dimensions intrinsèques
Gestion des polices
- La première étape d’un programme Dropflow consiste à enregistrer les polices qui seront sélectionnées par les propriétés CSS de police
- Dropflow ne recherche pas les polices système ; il faut donc au minimum créer et ajouter une fois un
FontFace - L’API d’enregistrement des polices implémente un sous-ensemble de la CSS Font Loading API et ajoute une méthode non standard
loadSync - Côté serveur, les URL
file:///peuvent être chargées de manière synchrone viareadFileSync - Les
ArrayBuffersont chargés immédiatement dans le constructeur, comme dans le navigateur registerNotoFontsenregistre toutes les familles Noto Sans- Les polices sont distribuées par FontSource et hébergées par jsDelivr
- Il s’agit d’un import volumineux, car il y a plus de 200 polices Noto Sans et les chaînes
unicodeRangedes polices CJK sont grandes - Pour un usage navigateur en production, il est indiqué qu’il vaut mieux enregistrer les polices individuellement
- En alphabet latin, les polices italique sont enregistrées ; pour toutes les écritures, les styles normal 400 et bold 700 sont enregistrés
- Le chinois, le coréen et le japonais partagent des points de code Unicode, mais les caractères peuvent être rendus différemment ; quand c’est possible, il vaut donc mieux utiliser des polices spécifiques à chaque langue
Layout, reflow et paint
layout(el)crée un arbre de boîtes, un arbre de fragmentation, et les glyphes qui composent le layout- L’arbre de boîtes correspond globalement à l’arbre DOM, mais peut contenir plus de boîtes à cause du contenu texte anonyme, ou moins à cause de
display: none
- L’arbre de boîtes correspond globalement à l’arbre DOM, mais peut contenir plus de boîtes à cause du contenu texte anonyme, ou moins à cause de
reflow(layout, width = 640, height = 480)positionne les boîtes, découpe le texte en lignes et le met dans un état prêt à être peint- margin collapsing des block boxes
- Passage du texte à HarfBuzz
- Itérations de fallback de police
- Reshaping selon les retours à la ligne et les points de coupure
- Placement des floats et gestion de
clear - Placement des spans de texte shapés et des backgrounds selon
directionet le sens du texte - Calcul des dimensions intrinsèques de
float,inline-block,absolute - Traitement de
positionaprès le flux normal
- Les cibles de paint prises en charge actuellement sont Canvas et SVG
paintToCanvaspeint dans un canvas navigateur, node-canvas, ou un contexte similaire conforme au standardpaintToSvgproduit une chaîne SVG et inclut des règles@font-facequi référencent les URL passées àFontFacepaintToSvgElementsn’ajoute ni<svg>ni règles@font-face, pour un usage de dessin dans un SVG existantpaintToHtmlgénère une liste plate d’éléments en position absolue ; son usage n’est pas recommandé, mais cela peut être utile pendant le développement
API DOM et hooks d’environnement
- Le
HTMLElementracine obtenu via les API Hyperscript et Parse fournit des méthodes permettant de trouver des éléments par nom de balise,idouclass, comme la famillequerySelectordu navigateurquery(selector)renvoie unHTMLElementounullqueryAll(selector)renvoie unHTMLElement[]
HTMLElementpeut avoir une render box associée- En général une seule, mais il peut y en avoir plusieurs si contenu inline et block sont mélangés
BlockContainerest créé pour les éléments en position absolue, flottants, inline-block et de niveau blockReplacedBoxest créé pour les images
- Dropflow est conçu pour pouvoir s’adapter à divers environnements
- Dans le navigateur, les polices et images sont chargées avec
fetch, et les buffers de polices sont enregistrés dansdocument.fonts - Dans Nodejs, les polices peuvent être chargées de manière synchrone via
fs.readFileSync - S’il existe un backend canvas et
node-canvas,registerFontdenode-canvasest appelé node-canvasne prend pas en charge les buffers de police ; il faut donc utiliser des URLfile://
- Dans le navigateur, les polices et images sont chargées avec
- Pour utiliser
@napi-rs/canvasouskia-canvas, quelques lignes de code sont nécessaires pour relierflow.environment.registerFontà l’API d’enregistrement de police correspondante - L’environnement comporte 6 hooks
wasmLocatorregisterFontresolveUrlresolveUrlSynccreateDecodedImagedestroyDecodedImage
Shaping de texte basé sur HarfBuzz
- La mise en page des glyphes est assurée par HarfBuzz, compilé en WebAssembly
- L’objectif est d’obtenir une précision difficile à atteindre avec une approche qui positionne les spans de texte via l’API
measureText - Exemple : dans Google Sheets, si l’on colore différemment uniquement le
"V"de"AV", le kerning disparaît et les lettres s’éloignent l’une de l’autre- Cela vient du fait que deux appels à
measureTextetfillTextsont effectués, un par caractère, ce qui fait perdre l’avance contextuelle des glyphes
- Cela vient du fait que deux appels à
- Dropflow utilise HarfBuzz sur des frontières de shaping plus grossières, et non aux points où la couleur change, afin de mieux prendre en charge les polices
- HarfBuzz compilé en WebAssembly peut atteindre des performances comparables à celles de
measureTextdeCanvasRenderingContext2D- Il n’est pas aussi rapide que
measureText, mais n’est pas non plus nettement plus lent - Dans les deux cas, il est indiqué qu’il ne s’agit pas du principal goulot d’étranglement de la pile de mise en page du texte
- Il n’est pas aussi rapide que
- Une mise en page de texte basée sur
measureTextnécessite un word cache pour être rapide, et il est indiqué que les applications GSuite utilisent cette approche- Un word cache ne permet pas de prendre en charge les polices ayant des effets qui traversent les espaces
- Pour prendre en charge de telles polices, il faut effectuer une recherche binaire sur les index de coupure du paragraphe, ce qui est indiqué comme bien plus lent que de passer le paragraphe entier à HarfBuzz
- Les signes diacritiques colorés sont impossibles avec
measureText
Projets dont Dropflow dépend
- Dropflow n’a pas de dépendances
package.json, mais réutilise le travail de plusieurs projets - Les dépendances JavaScript sont intégrées directement au projet, et modifiées à divers degrés pour garder le focus et éviter les problèmes de dependency-of-dependency
- Les principaux projets sont les suivants
- harfbuzz : shaping des polices et API de police essentielles
- Tehreer/SheenBidi : calcul des frontières bidi
- foliojs/linebreak : fourniture des index de coupure Unicode
- peggyjs/peggy : génération du parseur CSS
- fb55/htmlparser2 : parsing HTML
- google/emoji-segmenter : segmentation des emoji
- foliojs/grapheme-breaker : fourniture des frontières de graphèmes Unicode, fortement modifié pour Unicode 15
- foliojs/unicode-trie : utilisé pour des données Unicode rapides, fortement modifié pour supprimer les parties inutilisées
3 commentaires
Le titre original était donc « Show HN: Dropflow, a CSS layout engine for node or <canvas> ». Maintenant, il apparaît comme « GN⁺: Présentation HN : Dropflow, un moteur de mise en page CSS pour Node ou <canvas></canvas> ».
Comme il y a des balises dans le titre, la partie titre de la page de détail s’affiche mal, haha. Il faut les échapper...
Commentaires sur Hacker News
De nos jours, la manière standard de générer de jolis PDF côté backend consiste à lancer un navigateur headless et à convertir du HTML/CSS en PDF via les API du navigateur, mais faire tourner des instances de navigateur sur le serveur et les faire passer à l’échelle pour une forte charge a un coût assez élevé
C’est un outil qui change la donne, car on peut désormais concevoir et générer des PDF en HTML/CSS sans le surcoût d’un navigateur
Cela dit, les PDF que je produis ne sont clairement pas très beaux, donc c’est peut-être là toute la différence
Ils avaient tous besoin d’être conformes à l’ADA, donc difficile de passer à un moteur basé sur Canvas, puisque cela ferait disparaître l’accessibilité
Ça a l’air vraiment excellent. Dans mon précédent job, je développais une plateforme d’apprentissage en ligne immersive, et on formait des personnels du ministère de la Défense aux langues étrangères avec des Oculus Quest 2, en utilisant WebXR, Three.js, etc.
Unity3D, une fois, ça m’a suffi, et je n’avais aucune envie de repasser par la validation des app stores. On avait notre propre parc d’appareils, donc ça allait
L’un des plus gros défis consistait à créer un workflow pour le contenu pédagogique, et j’ai dû m’en charger presque seul. De vrais formateurs en langues créaient des PDF avec PowerPoint, puis, via un éditeur dédié que j’avais développé, chargeaient les PDF dans la base de données de contenu pour les déployer dans l’environnement d’entraînement ; ensuite, je les rendais avec PDFJS dans un élément Canvas pour les utiliser comme texture sur des rectangles 3D
Avec un outil comme celui-ci, il n’aurait pas été nécessaire de contourner le problème en passant par PowerPoint pour produire les supports. Cette méthode avait déjà fortement accéléré le workflow par rapport à une tentative antérieure où les gens fabriquaient des images dans Photoshop, mais si on avait pu créer un éditeur de panneaux directement dans l’app, on aurait évité la boucle « imaginer le rendu dans l’environnement → exporter en PDF → téléverser dans la base → voir le résultat réel »
Cela dit, comme il n’y avait de toute façon ni équipe business dev ni marketing qui sache vendre autre chose qu’un service plutôt qu’un produit, ça n’aurait probablement pas changé grand-chose
Je cherchais quelque chose comme ça pour mon projet https://htwins.net/scale2 et pour d’autres travaux utilisant SVG ou Canvas
Si vous galérez avec Flexbox, vous pouvez utiliser un outil qui simplifie le processus et évite d’avoir à jongler avec plein de propriétés pour créer des layouts responsive : https://flexboxcss.com
Excellent. Ce genre d’outil est très important, car il rend compréhensible cette boîte noire magique qu’est le moteur de rendu d’un navigateur
S’il existait une spécification complète et lisible par machine du rendu HTML et CSS, on pourrait générer des moteurs de rendu. Les comportements spécifiques à chaque navigateur pourraient alors n’être que des extensions au-dessus. Ce serait formidable que quelque chose comme https://github.com/tawesoft/html5spec devienne exploitable dans un vrai moteur
C’est assez proche de ce que je me demandais ces derniers temps. Je réfléchissais à la possibilité d’utiliser CSS et SVG comme couche d’abstraction au-dessus de bibliothèques graphiques et UI
C’est la première fois que j’entends parler de node-canvas, mais on dirait que ça fournit justement la partie dessin. Cet outil semble pouvoir prendre en charge la partie layout, qui est tout ce dont j’ai besoin dans une bibliothèque UI
Je me demande aussi à quel point l’implémentation de CSS a dû être difficile. J’ai entendu dire que c’était assez complexe
L’implémentation de CSS a été difficile, mais le plus gros obstacle était que les connaissances ne sont pas vraiment mises en avant ni faciles à trouver
La partie la plus difficile, c’est le layout du texte. Gérer les glyphes, parcourir en sens inverse pour le RTL, c’est un vrai casse-tête, et les retours à la ligne deviennent eux aussi très complexes. Le fait que les connaissances nécessaires ne soient pas rassemblées en un seul endroit rend la chose encore plus ardue
Après avoir terminé le layout en blocs au début, j’ai passé plusieurs années à n’y consacrer que quelques heures par semaine, le temps d’apprendre les détails du text shaping et de l’itemization, ce qu’il faut faire et ce qu’il faut éviter. J’en ai appris une grande partie en lisant le code source de Pango [1], et le reste en recoupant des recherches Google
Pour le reste, les spécifications du W3C couvrent presque tout. La norme CSS2 [2] est l’un des plus beaux documents que j’aie jamais lus. Elle est cohérente en interne, concise, et reflète des années de réflexion et d’essais-erreurs. CSS3 est excellent aussi, mais CSS2 reste la base de tout
[1] https://gitlab.gnome.org/GNOME/pango/
[2] https://www.w3.org/TR/CSS22/
C’est une grande contribution au monde. On dirait un cas typique du genre « quelqu’un devrait faire $X », que tout le monde pense mais que personne n’avait fait
Comme j’aime utiliser CSS pour le layout, aujourd’hui je m’appuie surtout sur Flexbox et Grid. Je comprends tout à fait que ce ne soit pas encore pris en charge, mais je me demande s’il est prévu de les supporter un jour. Et si oui, comment d’autres pourraient-ils aider ?
Vraiment impressionnant. Je pense que la plupart des gens ne réalisent pas à quel point il est difficile de convertir du HTML en PNG de manière programmatique
On se heurte à une multitude de petits problèmes à cause des différences entre Node et le navigateur, ou entre HTML et Canvas
Ça a l’air utile. Il est difficile d’imaginer l’ampleur du travail nécessaire pour d’abord comprendre CSS, puis construire par-dessus un moteur de layout