HN-Vorstellung: Dropflow, eine CSS-Layout-Engine für Node oder `<canvas>`
(github.com/chearon)- Dropflow ist eine CSS-Layout-Engine, die entwickelt wurde, um grundlegende CSS-Standards wie inline, block, float, positioning und künftig table zu erforschen; flexbox und grid gehören nicht zum Zielbereich
- Mit Node und node-canvas kann sie im Backend für die Erzeugung von PDFs und Bildern verwendet werden, und im Browser kann sie Rich-Text mit Zeilenumbrüchen auf
canvasrendern - Das Textlayout unterstützt bidirektionalen und RTL-Text, Font-Fallback auf Graphem-Ebene, farbige diakritische Zeichen, die Registrierung von OpenType-/TrueType-Schriften sowie HarfBuzz-basiertes Shaping
- Der Umfang der CSS-Unterstützung umfasst mehr als 30 Eigenschaften; unter anderem funktionieren
float,clear,display: inline-block,position: relative,overflow,z-indexundzoom, währendtable,absolute,fixed,transformusw. noch geplant sind - Die höchste Performance wird mit der hyperscript API und wiederverwendbaren Style-Objekten erreicht, die HTML/CSS-Parsing überspringen; auf einem MacBook Pro von 2019 wird Der kleine Prinz mit mehr als 500 Absätzen in weniger als 160 ms von HTML in ein Bild umgewandelt
Welchen CSS-Layout-Bereich Dropflow abdeckt
- Dropflow ist eine CSS-Layout-Engine, die geschaffen wurde, um den Bereich grundlegender CSS-Standards zu erforschen
- Zielbereich sind inline, block, float, positioning und künftig tables
- flexbox und grid gehören nicht dazu
- Es verfügt über eine hochwertige Implementierung für Textlayout und kann viele Sprachen der Welt darstellen
- Als Einsatzgebiete werden zwei Szenarien genannt
- Erzeugung von PDFs oder Bildern im Backend mit Node und node-canvas
- Rendering von umbrechendem Rich-Text auf
canvasim Browser
Hauptfunktionen
- Es unterstützt mehr als 30 CSS-Eigenschaften, darunter auch komplexe Eigenschaften wie
float - Der Funktionsumfang rund um Text ist breit
- Bidirektionaler Text und RTL-Text
- Font-Fallback auf Graphem-Ebene
- Farbige diakritische Zeichen
- Korrekte Zeilenumbruchbehandlung, etwa wenn ein führendes Padding in die nächste Zeile übernommen wird
- Optimiertes Shaping
- Als Eingabe werden nicht nur HTML/CSS unterstützt, sondern auch die hyperscript-
h()-API, bei der Styles als Objekte übergeben werden - OpenType-/TrueType-Buffer können registriert werden, und die Registrierung ist erforderlich
<img>unterstützt JPEG, BMP, PNG und GIF, wobei die Unterstützung je nach Backend variieren kann- Vererbte und kaskadierte Styles werden nicht doppelt berechnet
- Ziel sind vollständige Typisierung, umfangreiche Tests und hohe Geschwindigkeit
Status der CSS-Unterstützung
- In der Inline-Formatierung funktionieren unter anderem
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-wrapundword-spacing - In der Inline-Formatierung sind
font-variant,letter-spacing,tab-size,text-decorationundunicode-bidinoch geplant - In der Block-Formatierung funktionieren
clearundfloat- Bei
writing-modesindhorizontal-tb,vertical-lrundvertical-rlteilweise implementiert - Im BFC ist es implementiert, im IFC jedoch noch nicht
- Bei
- Bei Boxes and positioning funktionieren verschiedene Box- und Positionierungs-Eigenschaften
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
- Bei Boxes and positioning sind
display: table,min/max-height,min/max-width,position: absolute,position: fixedundtransformnoch geplant
Grundlegender Nutzungsablauf
- Dropflow arbeitet wie ein Browser auf Basis eines DOM mit vererbten und berechneten Styles
- Der typische Ablauf ist wie folgt
- Vor dem Layout Schriften mit
FontFaceregistrieren - Mit
flow.style()Style-Objekte erzeugen - Mit
flow.h()ein DOM erstellen - Mit
flow.dom()den zu rendernden Baum vorbereiten - Mit
renderToCanvas()Layout und Paint auf dem gesamten Canvas ausführen
- Vor dem Layout Schriften mit
- Es gibt auch eine HTML-API, sie wird aber nur empfohlen, wenn Performance nicht entscheidend ist oder zu Lernzwecken
- Parsing kostet zusätzliche Zeit und vergrößert das Bundle deutlich
- Es wird erwähnt, dass das HTML-Parsing dank @fb55 schnell ist
- Aktuell wird nur das HTML-Attribut
styleunterstützt;classfunktioniert noch nicht
Performance-Eigenschaften
- Performance ist ein oberstes Ziel und nach Korrektheit am wichtigsten
- Es werden beispielhafte Leistungswerte genannt
- Die Umwandlung von 8 Absätzen mit mehreren inline spans und unterschiedlichen Schriften von HTML in ein Bild dauert auf einem MacBook Pro von 2019 9 ms, auf einem MacBook Pro von 2012 13 ms
- Die Umwandlung von Der kleine Prinz mit mehr als 500 Absätzen von HTML in ein Bild dauert auf einem MacBook Pro von 2019 unter 160 ms, auf einem MacBook Pro von 2012 unter 250 ms
- Das Erzeugen eines 10-Zeichen-Worts und die reine Layout-Berechnung dauern auf einem MacBook Pro von 2019 unter 25 µs, auf einem MacBook Pro von 2012 unter 50 µs
- Die schnellste Performance wird erreicht, wenn mit der hyperscript API das DOM direkt erstellt und der übliche HTML/CSS-Parsing-Schritt übersprungen wird
- Noch größere Vorteile ergeben sich, wenn Style-Objekte wiederverwendet werden
- Reflow bei anderer Breite ist schneller, als den Layout-Baum neu zu erstellen
API-Aufbau
- Die Grundschritte sind zwei
- Schriftregistrierung
- DOM-Erstellung über die Hyperscript API oder die Parse API
- Bei einfacher Nutzung kann das DOM direkt auf ein Canvas gerendert werden
renderToCanvas(el, canvas)rendert das vollständige Layout, wobeiwidthundheightdes Canvas als Viewport-Größe verwendet werden
- Mit einer API auf niedrigerer Ebene kann das Layout erhalten bleiben
- Abhängige Ressourcen laden
- Layout des DOM erzeugen
- Layout-Reflow
- Auf ein Ziel wie HTML5 canvas painten
- Das kann genutzt werden, um mit anderen Größen zu reflowen, unsichtbares Layout nicht zu painten oder Intrinsics zu ermitteln
Schriftverarbeitung
- Der erste Schritt in einem Dropflow-Programm ist die Registrierung der Schriften, die über CSS-Schrifteigenschaften ausgewählt werden sollen
- Da Dropflow keine Systemschriften durchsucht, muss mindestens einmal
FontFaceerstellt und hinzugefügt werden - Die API zur Schriftregistrierung implementiert eine Teilmenge der CSS Font Loading API und ergänzt die nicht standardisierte Methode
loadSync - Serverseitig können
file:///-URLs synchron überreadFileSyncgeladen werden ArrayBufferwird wie im Browser sofort im Konstruktor geladenregisterNotoFontsregistriert alle Noto-Sans-Schriftfamilien- Die Schriften werden von FontSource verteilt und von jsDelivr gehostet
- Da es mehr als 200 Noto-Sans-Schriften gibt und die
unicodeRange-Strings der CJK-Schriften groß sind, ist dies ein großer Import - Für den produktiven Einsatz im Browser sei es besser, einzelne Schriften zu registrieren
- Für Latin wird eine italic-Schrift registriert, für alle Schriftsysteme normal 400 und bold 700
- Chinesisch, Koreanisch und Japanisch teilen sich gemeinsame Unicode-Codepoints, Zeichen können aber unterschiedlich gerendert werden; wenn möglich, sind sprachspezifische Schriften besser
Layout, Reflow und Paint
layout(el)erzeugt ein box tree, einen Fragmentierungsbaum und Glyphen umfassendes Layout- Der box tree entspricht grob dem DOM tree, kann wegen anonymem Textinhalt aber mehr Boxes enthalten oder wegen
display: noneweniger
- Der box tree entspricht grob dem DOM tree, kann wegen anonymem Textinhalt aber mehr Boxes enthalten oder wegen
reflow(layout, width = 640, height = 480)positioniert Boxes, bricht Text in Zeilen um und macht ihn paintbar- Margin-Collapsing von Block-Boxes
- Übergabe von Text an HarfBuzz
- Iteration über Font-Fallbacks
- Reshaping je nach Zeilenumbruch und Breakpoints
- Platzierung von Floats und Behandlung von
clear - Platzierung von geformten Textspans und Hintergründen gemäß
directionund Textrichtung - Berechnung der Intrinsics von
float,inline-blockundabsolute-Inhalten - Verarbeitung von
positionnach dem normalen Flow
- Als Paint-Ziele werden derzeit Canvas und SVG unterstützt
paintToCanvaspaintet auf Browser-Canvas, node-canvas und ähnliche standardkonforme ContextspaintToSvgerzeugt einen SVG-String und enthält@font-face-Regeln, die auf die anFontFaceübergebenen URLs verweisenpaintToSvgElementsfügt kein<svg>und keine@font-face-Regeln hinzu, damit es zum Zeichnen innerhalb eines bestehenden SVG verwendet werden kannpaintToHtmlerzeugt eine flache Liste absolut positionierter Elemente; empfohlen wird es nicht, für die Entwicklung kann es aber nützlich sein
DOM-API und Umgebungs-Hooks
- Das Root-
HTMLElementaus der Hyperscript- und Parse-API bietet Methoden zum Finden von Elementen über Tag-Name,idundclass, ähnlich wie diequerySelector-Familie im Browserquery(selector)gibt einHTMLElementodernullzurückqueryAll(selector)gibtHTMLElement[]zurück
HTMLElementkann verknüpfte Render-Boxes haben- Üblicherweise eine, bei gemischtem inline- und block-Inhalt aber mehrere
BlockContainerwird für absolute positioned elements, floated elements, inline-blocks und block-level elements erzeugtReplacedBoxwird für Bilder erzeugt
- Dropflow ist so entworfen, dass es an verschiedene Umgebungen angepasst werden kann
- Im Browser werden Schriften und Bilder per
fetchgeladen, und Font-Buffer werden indocument.fontsregistriert - In Nodejs können Schriften synchron per
fs.readFileSyncgeladen werden - Wenn ein Canvas-Backend und
node-canvasvorhanden sind, wirdregisterFontvonnode-canvasaufgerufen - Da
node-canvaskeine Font-Buffer unterstützt, müssenfile://-URLs verwendet werden
- Im Browser werden Schriften und Bilder per
- Für
@napi-rs/canvasoderskia-canvasbraucht man einige Zeilen Code, umflow.environment.registerFontan die jeweilige API zur Schriftregistrierung anzubinden - Die Umgebung besitzt 6 Hooks
wasmLocatorregisterFontresolveUrlresolveUrlSynccreateDecodedImagedestroyDecodedImage
HarfBuzz-basiertes Text-Shaping
- Das Glyph-Layout wird von HarfBuzz ausgeführt, das zu WebAssembly kompiliert wurde
- Ziel ist eine Genauigkeit, die mit einem Ansatz über die
measureText-API für die Positionierung von Textspans schwer zu erreichen ist - Als Beispiel wird genannt, dass in Google Sheets beim Einfärben nur des
"V"in"AV"das Kerning verschwindet und die Buchstaben weiter auseinanderstehen als ursprünglich- Der Grund ist, dass pro Buchstabe zwei Aufrufe von
measureTextundfillTexterfolgen und dadurch kontextabhängige Glyph-Advances verloren gehen
- Der Grund ist, dass pro Buchstabe zwei Aufrufe von
- Dropflow unterstützt Schriften präziser, indem HarfBuzz an gröberen Shaping-Grenzen statt an Farbwechselpunkten eingesetzt wird
- Das zu WebAssembly kompilierte HarfBuzz kann Leistungswerte erreichen, die denen von
CanvasRenderingContext2DmitmeasureTextähneln- Es ist laut Beschreibung nicht so schnell wie
measureText, aber auch nicht deutlich langsamer - Beide seien nicht der dominierende Engpass im Textlayout-Stack
- Es ist laut Beschreibung nicht so schnell wie
- Textlayout auf Basis von
measureTextbraucht einen Word-Cache, um schnell zu sein; es wird erwähnt, dass GSuite-Apps diesen Ansatz verwenden- Ein Word-Cache kann keine Schriften unterstützen, die Effekte über Leerzeichen hinweg haben
- Um solche Schriften zu unterstützen, wäre an den Umbruchindizes eines Absatzes eine Binärsuche nötig, was viel langsamer sei, als den ganzen Absatz an HarfBuzz zu übergeben
- Farbige diakritische Zeichen sind mit
measureTextnicht möglich
Abhängige Projekte
- Dropflow hat keine
package.json-Abhängigkeiten, nutzt aber die Arbeit mehrerer Projekte - JavaScript-Abhängigkeiten sind ins Projekt eingecheckt und wurden in unterschiedlichem Ausmaß angepasst, um den Fokus beizubehalten und dependency-of-dependency-Probleme zu vermeiden
- Zu den wichtigsten Projekten gehören
- harfbuzz: Font-Shaping und essenzielle Font-APIs
- Tehreer/SheenBidi: Berechnung bidi-Grenzen
- foliojs/linebreak: Bereitstellung von Unicode-Break-Indizes
- peggyjs/peggy: Erzeugung des CSS-Parsers
- fb55/htmlparser2: HTML-Parsing
- google/emoji-segmenter: Emoji-Segmentierung
- foliojs/grapheme-breaker: Bereitstellung von Unicode-Graphemgrenzen, stark für Unicode 15 modifiziert
- foliojs/unicode-trie: Verwendung für schnelle Unicode-Daten, stark modifiziert, um ungenutzte Teile zu entfernen
3 Kommentare
Der ursprüngliche Titel war also „Show HN: Dropflow, a CSS layout engine for node or <canvas>“. Jetzt steht er als „GN⁺: HN vorgestellt: CSS-Layout-Engine für Node oder <canvas>, Dropflow</canvas>“ drin.
Im Titel sind Tags enthalten, daher wird der Titelbereich auf der Detailseite beschädigt … haha. Man sollte escapen …
Hacker-News-Kommentare
Heutzutage besteht der Standardweg, im Backend schöne PDFs zu erzeugen, darin, einen Headless-Browser zu starten und HTML/CSS per Browser-API in PDF umzuwandeln. Die Kosten dafür, Browser-Instanzen auf dem Server laufen zu lassen und für hohe Last zu skalieren, sind jedoch ziemlich hoch.
Das hier ist ein Tool, das die Spielregeln verändert: Man kann PDFs nun mit HTML/CSS entwerfen und erzeugen, ohne den Browser-Overhead in Kauf nehmen zu müssen.
Die PDFs, die ich erzeuge, sind allerdings definitiv nicht besonders hübsch, also könnte genau das der Unterschied sein.
Alle mussten ADA-konform sein, daher ist ein Wechsel zu einem Canvas-Renderer schwierig, weil dabei die Barrierefreiheit verloren geht.
Sieht wirklich gut aus. In meinem vorherigen Job habe ich eine immersive Online-Lernplattform entwickelt, mit der Personal des Verteidigungsministeriums über die Oculus Quest 2 in Fremdsprachen geschult wurde, unter Einsatz von WebXR, Three.js und mehr.
Unity3D reichte mir einmal, und ich wollte auch nicht durch App-Store-Prüfungen gehen. Da wir einen eigenen Gerätepark hatten, war das in Ordnung.
Eine der größten Herausforderungen war der Aufbau eines Workflows für Unterrichtsinhalte, und ich musste das fast alleine stemmen. Echte Sprachlehrkräfte erstellten PDFs in PowerPoint, und mit einem von mir gebauten Spezialeditor wurden die PDFs in die Content-Datenbank hochgeladen, in die Trainingsumgebung ausgerollt und dann mit PDFJS auf ein Canvas-Element gerendert, das ich als Textur für 3D-Rechtecke verwendet habe.
Mit einem solchen Tool hätten die Leute ihre Materialien nicht über PowerPoint erstellen müssen. Der PowerPoint-Ansatz war gegenüber einem früheren Versuch, bei dem Bilder in Photoshop erstellt wurden, bereits eine deutliche Verbesserung der Workflow-Geschwindigkeit. Noch besser wäre es aber gewesen, einen Schild-Editor direkt in der App zu haben, um die Schleife „raten, wie es in der Umgebung aussehen wird → als PDF exportieren → in die DB hochladen → tatsächliches Aussehen prüfen“ zu vermeiden.
Letztlich hätte es aber wohl keinen großen Unterschied gemacht, weil wir ohnehin kein Business-Development- oder Marketing-Team hatten, das etwas anderes als den Verkauf von Services verstand.
Ich habe nach so etwas für mein Projekt https://htwins.net/scale2 und andere Arbeiten gesucht, die SVG oder Canvas verwenden.
Wenn man mit Flexbox kämpft, kann man ein Tool verwenden, das den Prozess vereinfacht, damit man beim Erstellen responsiver Layouts nicht auf so viele Eigenschaften achten muss: https://flexboxcss.com
Hervorragend. Solche Tools sind enorm wichtig, weil sie die magische Blackbox einer Browser-Rendering-Engine nachvollziehbar machen.
Wenn man eine vollständige maschinenlesbare Spezifikation für HTML- und CSS-Rendering erstellen könnte, ließe sich daraus ein Renderer erzeugen. Browser-spezifisches Verhalten könnte dann als Erweiterung darüberliegen. Es wäre großartig, wenn so etwas wie https://github.com/tawesoft/html5spec in eine Form käme, die in echten Engines verwendet werden kann.
Das kommt dem ziemlich nahe, worüber ich in letzter Zeit nachgedacht habe. Ich habe mich gefragt, ob man CSS und SVG als Abstraktionsschicht über Grafik- und UI-Bibliotheken verwenden könnte.
Von node-canvas höre ich zum ersten Mal, aber es scheint den Zeichnungsteil davon abzudecken. Dieses Tool könnte den Layout-Teil übernehmen, also genau den Teil, den ich aus einer UI-Bibliothek brauche.
Mich würde auch interessieren, wie schwierig die CSS-Implementierung war. Ich habe gehört, dass sie ziemlich komplex ist.
Die Implementierung von CSS war schwierig, aber die größte Hürde war, dass das nötige Wissen nicht gut sichtbar dokumentiert ist.
Der schwierigste Teil ist Text-Layout. Sich mit Glyphen zu befassen und für RTL rückwärts zu iterieren, ist schon kompliziert genug, und Zeilenumbrüche machen alles noch viel komplexer. Noch schwerer wird es dadurch, dass das erforderliche Wissen nicht an einer Stelle gesammelt ist.
Nachdem das Block-Layout anfangs fertig war, habe ich über mehrere Jahre hinweg nur ein paar Stunden pro Woche daran gearbeitet und musste die Details von Text Shaping und Itemization lernen, also was man tun und was man vermeiden sollte. Vieles habe ich durch das Lesen des Pango-[1]-Quellcodes gelernt, den Rest durch zusammenstückelte Google-Suchen.
Davon abgesehen decken die W3C-Spezifikationen fast alles ab. Der CSS2-Standard [2] war eines der schönsten Dokumente, die ich je gelesen habe. Er ist intern konsistent und prägnant und spiegelt jahrelanges Nachdenken sowie Versuch und Irrtum wider. CSS3 ist ebenfalls großartig, aber CSS2 ist die Grundlage von allem.
[1] https://gitlab.gnome.org/GNOME/pango/
[2] https://www.w3.org/TR/CSS22/
Das ist ein großer Beitrag für die Welt. Es wirkt wie ein klassischer Fall von „Jemand sollte $X tun“, den alle denken, aber niemand umsetzt.
Als jemand, der CSS für Layouts gern verwendet, verlasse ich mich heute meist auf Flexbox und Grid. Dass das noch nicht unterstützt wird, ist völlig verständlich, aber ich frage mich, ob geplant ist, das irgendwann zu unterstützen. Falls ja: Wie könnten andere dabei helfen?
Wirklich beeindruckend. Die meisten Leute wissen vermutlich nicht, wie schwierig es ist, HTML programmatisch in PNG umzuwandeln.
Wegen der Unterschiede zwischen Node und Browser oder zwischen HTML und Canvas stößt man auf unzählige kleine Probleme.
Sieht nützlich aus. Es ist schwer vorstellbar, wie viel Arbeit nötig ist, um zuerst CSS zu verstehen und darauf dann eine Layout-Engine aufzubauen.