コンテンツにスキップ

HTML レンダリングパイプライン

writeHtml() は単一の前方パスで処理します。トークン化、@page とスタイルの resolve(解決)、レイアウト、PDF オペレーターの描画を順に実行します。要素ツリーはステージ間で保持されません。

Terminal window
composer require nextpdf/core:^3

HTML レンダリングパイプラインは、単一の前方パスで HTML+CSS を PDF コンテンツストリームのオペレーターに変換します。保持されるドキュメントツリーは構築しません。以下のステージシーケンスは、HtmlParser::parse()main 上に実装されている内容を反映しています。

ステージ 1 — サニタイズと正規化。 HtmlParser::parse() は 10 MB を超える入力を拒否し、制御文字を除去し、改行を正規化します。ソース HTML の改行正規化に従い、CRLF と単独の CR はどちらも LF になります。その後、すべてのインスタンスフィールドをリセットするため、前回の呼び出しから状態が引き継がれることはありません。

ステージ 2 — @page とスタイルブロックの抽出。 パーサーはまず <style> ブロックを抽出し、続いて検出した @page ルールを適用してページジオメトリを再構成します。ページサイズはそれ以降のすべてのレイアウト判断に影響するため、この処理はトークンが処理される前に行われます。

ステージ 3 — トークン化。 HtmlTokenizer::cleanHtml()<pre> のコンテンツを保持しつつ空白を正規化します。続いて tokenize() がフラットな list<HtmlToken> を生成します。これはノードグラフではなく、トークンリストです。空白のみのテキストトークンは直ちに破棄されます。HtmlChildScanner::scan() はフラットなリストに対してインデックスマップ(子の数、タグの数、空かどうか)を構築するため、構造セレクターはツリーを必要としません。

ステージ 4 — オプションの :has() プリスキャン。 css.has 実験的機能が有効な場合、CssResolver::resolveHasSelectors() がトークンリストに対して境界付きのプリスキャンを 1 回実行し、関係セレクターを解決します。これは単一パスルールにおける、文書化された境界付きの例外です。

ステージ 5 — トークンの処理(スタイル、レイアウト、描画)。 HtmlParser::processTokens() はトークンリストを 1 回走査します。各要素についてカスケードを解決し(レイヤー 1 のアプリケーターが HtmlStyleState に書き込み)、ジオメトリを計算し(レイヤー 3 レイアウト)、PDF オペレーターを出力します(レイヤー 4 描画)。スタイルの継承には、プッシュとポップを行う HtmlStyleState スタックを使用します。カーソル(xy、マージン、ストリームオフセット)は、HtmlBlockCursor のスナップショットを通じてハンドラー間で受け渡されます。

ステージ 6 — 結果の返却。 parse() は、出力されたコンテンツストリーム、終端カーソル位置、および使用されたフォントキーを保持する不変の HtmlRenderResult を返します。呼び出し元(writeHtml())は、カーソルをページ座標フレームに戻します。

この レイヤーコントラクト のページでは、ステージ 5 の内部で実行される 4 レイヤー分離について説明しています。ストリーミング制約 のページでは、ツリーを保持しない特性とその上限について説明しています。

シンボル場所ステージ
Document::writeHtml(string $html): staticsrc/Core/Concerns/HasTextOutput.php公開エントリ
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpすべてのステージを統括
HtmlTokenizer::cleanHtml() / tokenize()src/Html/HtmlTokenizer.phpステージ 3
HtmlChildScanner::scan()src/Html/HtmlChildScanner.phpステージ 3 のインデックスマップ
CssResolver::resolveHasSelectors()src/Html/CssResolver.phpステージ 4(ゲート制御)
HtmlRenderResult (stream, endX, endY, usedFontKeys)src/Html/HtmlRenderResult.phpステージ 6

このコードは examples/08-html-basic.php から引用しています。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('HTML Basic');
$doc->addPage();
$doc->writeHtml('<h1 style="color:#1E3A8A;">HTML Rendering</h1><p>One pass.</p>');
$doc->save(__DIR__ . '/output/08-html-basic.pdf');

埋め込まれた <style> ブロックを含むスタイル付きレポートをレンダリングします。パイプラインは、トークンを処理する前にスタイルブロックを抽出して適用します。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\HtmlParsingException;
function renderInvoice(string $bodyHtml, string $out): void
{
$doc = Document::createStandalone();
$doc->setTitle('Invoice');
$doc->addPage();
$html = '<style>@page { margin: 20mm; } '
. 'h1 { color: #1E3A8A; } '
. 'table { width: 100%; }</style>'
. $bodyHtml;
try {
$doc->writeHtml($html);
} catch (HtmlParsingException $e) {
// Sanitize/cap failures surface here. Do not retry.
throw $e;
}
$doc->save($out);
}
  • @page はトークンより先に読み取られます。 スタイル抽出はトークン化に先行するため、コンテンツの後に配置された @page ルールも適用されます。ページジオメトリはステージ 5 より前に確定します。
  • <pre> の空白は保持されます。 cleanHtml()<pre> のコンテンツを保護します。それ以外の場所の空白は折りたたまれます。
  • :has() はゲート制御されます。 css.has 実験的機能がない場合、ステージ 4 は実行されず、:has() セレクターはマッチしません。
  • 単一のストリームバッファー。 パイプラインは単一の文字列バッファーに書き込みます。すでに書き込まれたコンテンツが移動されることはありません。再レイアウトはありません。
  • 上限はパスの途中で適用されます。 要素とネストの上限は、それ以前ではなくステージ 5 中にスローされます。ドキュメントは途中で失敗する可能性があります。

パイプラインの走査は O(トークン数) です。テーブルの列サイズ調整では、テーブルごとに境界付きの行スキャンが追加されます(ステージ 5、TableParser)。:has() プリスキャンは、有効な場合に境界付きのトークンリストパスを 1 回追加します(ステージ 4)。スタイルスタックのメモリは O(要素数) ではなく O(ネスト深度) です — ストリーミング制約 を参照してください。HTML レンダリングパイプラインのパフォーマンスベンチマークは、5% のゲートでリグレッションを防ぎます(マージ済みの作業、PR #564)。ページごとの performance_budgetwall_ms: 1500peak_mb: 64)が運用上の上限です。

ステージ 1 は最初のセキュリティ境界です。10 MB の入力上限、制御文字の除去、改行の正規化は、トークン化の前に実行されます。続いて DefaultHtmlSecurityPolicy が、ステージ 5 で許可されるタグ、属性、CSS プロパティ、URL スキームをゲート制御します。HTML モジュールのセキュリティモデル を参照してください。

改行の正規化は、HTML 標準の改行処理(CRLF と単独の CR が LF になる)に従います。プロパティごとの CSS 準拠状況は CSS サポートマトリックス に、カスケード動作は css-resolver に文書化されています。このページでは、プロパティごとのサポート状況を再掲しません。

Enterprise の機能。 Premium は、この同じパイプライン上で CSS のカバレッジを拡大します。6 ステージのシーケンスはエディション間で変わりません。CSS サポートマトリックス を参照してください。