HN紹介: Nodeまたは<canvas>向けCSSレイアウトエンジン、Dropflow
(github.com/chearon)- Dropflowは基礎的なCSS標準である inline、block、float、positioning、将来的には table を探究するために作られたCSSレイアウトエンジンであり、flexbox と grid は対象外
- Node と node-canvas を使ってバックエンドでPDF・画像生成に利用でき、ブラウザでは canvas に改行を含むリッチテキストをレンダリングできる
- テキストレイアウトは双方向・RTL テキスト、書記素単位のフォントフォールバック、色付きのダイアクリティカルマーク、OpenType/TrueType フォント登録、HarfBuzz ベースの shaping をサポートする
- CSS の対応範囲は30以上のプロパティで、
float、clear、display: inline-block、position: relative、overflow、z-index、zoomなどが動作し、table、absolute、fixed、transformなどは計画段階 - 最高性能は HTML/CSS パースを省くhyperscript APIと再利用可能な style オブジェクトで得られ、2019 MacBook Pro 基準で500超の段落からなる The Little Prince を HTML から画像へ 160ms 未満で変換する
Dropflowが扱うCSSレイアウトの範囲
- DropflowはCSSレイアウトエンジンで、基礎的な CSS standards の範囲を探究するために作られている
- 対象は inline、block、float、positioning、将来的には tables
- flexbox と grid は対象外
- 高品質なテキストレイアウト実装を備えており、世界中のさまざまな言語を表示できる
- 用途は2つ示されている
- Node と node-canvas を使ってバックエンドで PDF や画像を生成
- ブラウザで canvas に改行されるリッチテキストをレンダリング
主な機能
- 30以上の CSS プロパティをサポートし、
floatのような複雑なプロパティも含む - テキスト関連機能が広い
- 双方向テキストと RTL テキスト
- 書記素単位のフォントフォールバック
- 色付きのダイアクリティカルマーク
- 望ましい改行処理。たとえば先頭の padding を次の行へ持ち越す
- 最適化された shaping
- 入力方式は HTML/CSS だけでなく、スタイルをオブジェクトで渡すhyperscript
h()APIにも対応 - OpenType/TrueType バッファは登録可能で、登録は必須
<img>は JPEG、BMP、PNG、GIF をサポートするが、バックエンドごとの対応は異なる場合がある- 継承・カスケードされたスタイルは二重計算しない
- 型定義が充実しており、テストも多く、高速動作を目標としている
CSS対応状況
- Inline formatting で動作する項目には
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などがある - Inline formatting で
font-variant、letter-spacing、tab-size、text-decoration、unicode-bidiは計画中の状態 - Block formatting では
clearとfloatが動作するwriting-modeのhorizontal-tb、vertical-lr、vertical-rlは部分実装状態- BFC では実装されているが IFC ではまだ未実装
- Boxes and positioning では複数のボックス・配置プロパティが動作する
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
- Boxes and positioning で
display: table、min/max-height、min/max-width、position: absolute、position: fixed、transformは計画中の状態
基本的な使用フロー
- Dropflowはブラウザと同じように継承済み・計算済みスタイルを持つDOMを基盤に動作する
- 一般的な流れは次のとおり
- レイアウト前に
FontFaceでフォントを登録 flow.style()でスタイルオブジェクトを生成flow.h()で DOM を生成flow.dom()でレンダリングするツリーを準備renderToCanvas()で canvas 全体にレイアウトとペイントを実行
- レイアウト前に
- HTML API も提供されるが、性能が重要でない場合や学習目的の場合にのみ推奨される
- パースは追加時間を生み、バンドルサイズも大きく増やす
- HTML パースは @fb55 のおかげで高速だと記されている
- 現在は
styleHTML 属性のみ対応し、classはまだ動作しない
性能特性
- 性能は最上位の目標であり、正確性の次に重要
- 例に基づく性能値が示されている
- 複数の inline span と異なるフォントを含む8段落を HTML から画像へ変換するのに、2019 MacBook Pro で9ms、2012 MacBook Pro で13ms
- 500超の段落からなる The Little Prince を HTML から画像へ変換するのに、2019 MacBook Pro で160ms 未満、2012 MacBook Pro で250ms 未満
- 10文字の単語を生成してレイアウトだけを行うのに、2019 MacBook Pro で25µs 未満、2012 MacBook Pro で50µs 未満
- 最速の性能はhyperscript APIで DOM を直接構築し、一般的な HTML/CSS パース段階を省くときに得られる
- style オブジェクトを再利用すると、さらに大きな利点が得られる
- 異なる幅で reflow する処理は、レイアウトツリーを作り直すより速い
API構成
- 基本段階は2つ
- フォント登録
- Hyperscript API または Parse API で DOM を生成
- 単純な利用では DOM をそのまま canvas にレンダリングできる
renderToCanvas(el, canvas)は canvas の width と height を viewport サイズとして使い、全体レイアウトをレンダリングする
- より低レベルの API を使うとレイアウトを保持できる
- 依存リソースの読み込み
- DOM の layout 生成
- layout の reflow
- HTML5 canvas のような対象への paint
- この方法は、別サイズで reflow したり、見えないレイアウトを paint しなかったり、intrinsics を取得したりするのに使える
フォント処理
- Dropflowプログラムの最初の段階は、CSS font プロパティで選択されるフォントを登録すること
- Dropflowはシステムフォントを検索しないため、少なくとも一度は
FontFaceを作成して追加する必要がある - フォント登録 API は CSS Font Loading API のサブセットを実装し、非標準の
loadSyncメソッドを追加している - サーバー側では
file:///URL をreadFileSyncで同期ロードできる ArrayBufferはブラウザと同様にコンストラクタ内で即時ロードされるregisterNotoFontsはすべての Noto Sans フォントファミリーを登録する- フォントは FontSource が配布し、jsDelivr がホストしている
- Noto Sans フォントは200を超え、CJK フォントの
unicodeRange文字列も大きいため、大きな import になる - ブラウザの本番利用では、個別フォントを登録するほうがよいとされている
- Latin には italic フォントが登録され、すべてのスクリプトには normal 400 と bold 700 が登録される
- 中国語・韓国語・日本語は共通の Unicode コードポイントを共有するが、文字のレンダリングは異なる場合があるため、可能なら言語別フォントを使うほうがよい
レイアウト、reflow、paint
layout(el)はbox tree、fragmentation tree、glyph を含む layout を作る- box tree は DOM tree とおおむね対応するが、匿名テキストコンテンツのために box が増えたり、
display: noneのために減ったりすることがある
- box tree は DOM tree とおおむね対応するが、匿名テキストコンテンツのために box が増えたり、
reflow(layout, width = 640, height = 480)は box を配置し、テキストを行分割して paint 可能な状態にする- block box の margin collapsing
- HarfBuzz へテキストを渡す
- フォントフォールバックの反復
- 改行と break point に応じた reshaping
- float 配置と
clear処理 directionとテキスト方向に応じた shaped text span と background の配置float、inline-block、absoluteコンテンツの intrinsics 計算- normal flow の後で
positionを処理
- Paint 対象は現在Canvas と SVGをサポート
paintToCanvasはブラウザ canvas、node-canvas、同様の標準準拠 context に paint するpaintToSvgは SVG 文字列を作り、FontFaceに渡した URL を参照する@font-face規則を含むpaintToSvgElementsは既存 SVG 内に描画する用途のため、<svg>や@font-face規則を追加しないpaintToHtmlは絶対配置要素のフラットリストを生成し、推奨はされないが開発中には有用な場合がある
DOM APIと環境フック
- Hyperscript と Parse API で得たルート
HTMLElementは、ブラウザのquerySelector系のように tag name、id、classで要素を探すメソッドを提供するquery(selector)は1つのHTMLElementまたはnullを返すqueryAll(selector)はHTMLElement[]を返す
HTMLElementは接続された render box を持つことがある- 通常は1つだが、inline と block コンテンツが混在する場合は複数になることがある
BlockContainerは absolute positioned element、floated element、inline-block、block-level element に対して生成されるReplacedBoxは画像に対して生成される
- Dropflowはさまざまな環境に適応できるよう設計されている
- ブラウザでは
fetchでフォントと画像を読み込み、フォントバッファをdocument.fontsに登録する - Nodejs では
fs.readFileSyncでフォントを同期ロードできる - canvas バックエンドと
node-canvasがあれば、node-canvasのregisterFontを呼び出す node-canvasは font buffer をサポートしないためfile://URL を使う必要がある
- ブラウザでは
@napi-rs/canvasやskia-canvasを使うには、flow.environment.registerFontを対応するフォント登録 API へ接続する数行のコードが必要- 環境には6つのフックがある
wasmLocatorregisterFontresolveUrlresolveUrlSynccreateDecodedImagedestroyDecodedImage
HarfBuzzベースのテキスト shaping
- Glyph layout は WebAssembly にコンパイルされた HarfBuzz が担当する
measureTextAPI でテキスト span の位置を決める方式では得にくい正確性を目標としている- 例として Google Sheets で
"AV"の"V"だけを別色で塗ると kerning が失われ、文字間隔が本来より広がる- 文字ごとに2回の
measureTextとfillText呼び出しが発生し、contextual glyph advance が失われるため
- 文字ごとに2回の
- Dropflowは色の切り替わり位置ではなく、より粗い shaping boundary で HarfBuzz を使い、フォントをより正確にサポートする
- WebAssembly にコンパイルされた HarfBuzz は
CanvasRenderingContext2DのmeasureTextと近い性能指標を出せるmeasureTextほど速くはないが、大きく遅いわけでもないとされる- どちらもテキストレイアウトスタックの支配的なボトルネックではないとされる
measureTextベースのテキストレイアウトは高速化のために word cache を必要とし、GSuite アプリがこの方式を使っているとされる- word cache は空白をまたいで効果を持つフォントをサポートできない
- そのようなフォントをサポートするには、段落の break index で二分探索が必要であり、段落全体を HarfBuzz に渡すよりはるかに遅いとされる
- 色付きのダイアクリティカルマークは
measureTextでは不可能
依存プロジェクト
- Dropflowには
package.json依存関係はないが、複数プロジェクトの成果を活用している - JavaScript 依存コードはプロジェクトにチェックインされており、集中維持と dependency-of-dependency 問題を避けるため、さまざまな程度で修正されている
- 主なプロジェクトは次のとおり
- harfbuzz: font shaping と必須の font API を提供
- Tehreer/SheenBidi: bidi boundary 計算
- foliojs/linebreak: Unicode break index を提供
- peggyjs/peggy: CSS parser 生成
- fb55/htmlparser2: HTML parsing
- google/emoji-segmenter: emoji segmenting
- foliojs/grapheme-breaker: Unicode grapheme boundary を提供し、Unicode 15 向けに大幅修正
- foliojs/unicode-trie: 高速な Unicode data に使用され、未使用部分除去のため大幅修正
3件のコメント
元のタイトルは「Show HN: Dropflow, a CSS layout engine for node or <canvas>」だったのですね。現在は「GN⁺: HN紹介: Nodeまたは<canvas>向けCSSレイアウトエンジン、Dropflow</canvas>」として入っていますね。
タイトルにタグがあるので、詳細ページのタイトル部分が崩れますね..笑 エスケープを..
Hacker News のコメント
最近バックエンドで見栄えのよい PDF を作る基本的な方法は、ヘッドレスブラウザを立ち上げ、ブラウザ API で HTML/CSS を PDF に変換することだが、サーバー上でブラウザインスタンスを動かし、大きなワークロードに合わせてスケールさせるコストはかなり大きい
これは状況を変えるツールで、これからはブラウザのオーバーヘッドなしに HTML/CSS で PDF をデザインして生成できる
ただし自分が生成する PDF は確かに見栄えがよいとは言えないので、そこが違いなのかもしれない
どれも ADA 準拠が必要だったので、Canvas レンダラーに切り替えるのは難しい。そうするとアクセシビリティが失われるから
本当に良さそう。前職では没入型オンライン学習プラットフォームを開発していて、Oculus Quest 2 で国防総省職員の外国語教育を行い、WebXR、Three.js などを使っていた
Unity3D は一度で十分だったし、アプリストア審査も通したくなかった。自社のデバイス群があったので問題なかった
最大の難題の一つは教材コンテンツのワークフローを作ることで、ほぼ一人でやらなければならなかった。実際の語学講師たちが PowerPoint で PDF を作り、自分が作った専用エディタで PDF をコンテンツデータベースにアップロードして訓練環境に配置した後、PDFJS で Canvas 要素にレンダリングして 3D の四角形のテクスチャとして使っていた
こういうツールがあれば、人々が PowerPoint で回り道して教材を作る必要はなかったはず。PowerPoint 方式は、Photoshop で画像を作らせていた以前の試みよりワークフロー速度を大きく改善したが、アプリ内に標識エディタを作れたなら、「環境内でどう見えるか推測 → PDF にエクスポート → DB にアップロード → 実際の見た目を確認」という反復をなくせて、もっと良かったはず
どうせプロダクトではなくサービス販売しか知らない事業開発チームやマーケティングチームはいなかったので、大した意味はなかっただろうけど
自分のプロジェクト https://htwins.net/scale2 や、SVG または Canvas を使う他の作業でこういうものを探していた
Flexbox で苦労しているなら、レスポンシブレイアウトを作るときに複数のプロパティに気を配らなくて済むよう、プロセスを単純化してくれるツールを使える: https://flexboxcss.com
素晴らしい。こういうツールは、ブラウザレンダリングエンジンという魔法の箱を理解可能なものにしてくれるので、とても重要
HTML と CSS レンダリングについて完全に機械可読な仕様を作れれば、レンダラーを生成できるはず。ブラウザごとの特異な挙動は、その上の拡張として置けばよい。https://github.com/tawesoft/html5spec のようなものが実際のエンジンで使われる形になるといい
最近気になっていたことにかなり近い。CSS と SVG を、グラフィックスおよび UI ライブラリ上の抽象化レイヤーとして使えるか考えていた
node-canvas は初めて聞いたが、そのうち描画部分を埋めてくれるもののようだ。このツールは、UI ライブラリで自分に必要なすべてであるレイアウト部分を担えそうに見える
CSS 実装がどれほど難しかったのかも気になる。かなり複雑だと聞いている
CSS 実装は難しかったが、最大の障壁は知識があまり表に出ていないことだった
最も難しい部分はテキストレイアウト。グリフを扱い、RTL のために逆方向に走査するのは頭が痛いし、改行も本当に複雑になる。必要な知識が一か所にまとまっていないので、さらに難解
初期にブロックレイアウトを終えた後、数年間は週に数時間だけ作業しながら、テキストシェーピングと項目化の細部、やるべきことと避けるべきことを学ばなければならなかった。多くは Pango [1] のソースコードを読みながら学び、残りは Google 検索でつなぎ合わせた
それ以外は W3C 仕様がほぼすべてを扱っている。CSS2 標準 [2] は、これまで読んだ中で最も美しい文書の一つだった。内部的に一貫していて簡潔で、長年の熟考と試行錯誤が反映された結果だ。CSS3 も素晴らしいが、CSS2 がすべての基盤
[1] https://gitlab.gnome.org/GNOME/pango/
[2] https://www.w3.org/TR/CSS22/
世の中への大きな貢献。「誰かが $X をやるべきなのに」とみんなが思っているが、誰もやっていなかった典型例に見える
レイアウトに CSS を使うのが好きな立場として、最近は主に Flexbox と Grid に頼っている。まだ対応していないのは十分理解できるが、いつか対応する計画があるのか気になる。あるなら、他の人はどう手伝えるだろうか?
本当にすごい。HTML をプログラムで PNG に変換するのがどれほど難しいか、ほとんどの人はよく分かっていないと思う
Node とブラウザの違い、または HTML と Canvas の違いのために、無数の小さな問題にぶつかることになる
役に立ちそう。まず CSS を理解し、その上にレイアウトエンジンを作るのにどれほど多くの作業が必要か、想像しにくい