跳到內容

HTML 算繪管線

writeHtml() 以單次向前遍歷執行:tokenize、resolve(解析)@page 與樣式、進行版面配置,最後繪製 PDF 運算子。各階段之間不保留任何元素樹。

Terminal window
composer require nextpdf/core:^3

HTML 算繪管線透過單次向前遍歷,將 HTML+CSS 轉換成 PDF 內容串流(content stream)運算子。它不會建立持久保留的文件樹。以下階段順序對應 HtmlParser::parse()main 上的實作。

階段 1 — 清理與正規化。 HtmlParser::parse() 會拒絕超過 10 MB 的輸入、移除控制字元,並正規化換行:CRLF 與單獨的 CR 都會變成 LF;這與來源採用的 HTML 換行正規化方式一致。接著它會重設每個實例欄位,因此不會有任何狀態從先前的呼叫殘留下來。

階段 2 — 擷取 @page 與樣式區塊。 parser(剖析器)會先擷取 <style> 區塊,再套用所發現的 @page 規則來重新設定頁面幾何。這會在處理任何 token 之前完成,因為頁面尺寸會影響後續每一個版面配置決策。

階段 3 — Tokenize。 HtmlTokenizer::cleanHtml() 會正規化空白,同時保留 <pre> 內容。接著 tokenize() 會產生一份扁平的 list<HtmlToken>。這是 token 清單,而不是節點圖。只含空白的文字 token 會立即被捨棄。HtmlChildScanner::scan() 會在這份扁平清單上建立 Index(索引)對映(子節點數、標籤數、是否為空),讓結構性選擇器即使沒有樹也能運作。

階段 4 — 選用的 :has() 預掃描。 當啟用 css.has 實驗性功能時,CssResolver::resolveHasSelectors() 會在 token 清單上執行一次有界的預掃描,以解析這個關係型選擇器。這是單遍歷規則中一個有文件記載且有界的例外。

階段 5 — 處理 token(樣式、版面、繪製)。 HtmlParser::processTokens() 會走訪 token 清單一次。它會為每個元素解析串接(cascade)(第 1 層套用器會寫入 HtmlStyleState)、計算幾何(第 3 層版面),並輸出 PDF 運算子(第 4 層繪製)。樣式繼承使用一個 push-and-pop 的 HtmlStyleState 堆疊。游標(xy、邊界、串流偏移量)會透過 HtmlBlockCursor 快照在各處理常式之間傳遞。

階段 6 — 回傳結果。 parse() 會回傳一個不可變的 HtmlRenderResult,攜帶輸出的內容串流、結束時的游標位置,以及使用到的字型鍵。呼叫端(writeHtml())會把游標交回給頁面座標 Framework(框架)。

層契約說明階段 5 內部執行的四層分離。串流限制說明不保留樹的特性及其上限。

符號位置階段
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> 區塊的樣式化報表。管線會先擷取並套用樣式區塊,然後才處理任何 token。

<?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 會在 token 之前讀取。 即使 @page 規則放在內容之後,仍會生效,因為樣式擷取先於 tokenize。頁面幾何會在階段 5 之前就固定下來。
  • <pre> 的空白會被保留。 cleanHtml() 會保護 <pre> 內容;其他位置的空白會被摺疊。
  • :has() 受功能旗標控管。 若未啟用 css.has 實驗性功能,階段 4 就不會執行,:has() 選擇器也不會比對成功。
  • 單一串流緩衝區。 管線會寫入單一字串緩衝區。已寫入的內容絕不會被移動,也不會重新配置版面。
  • 上限會在遍歷途中生效。 元素數與巢狀深度超過上限時,會在階段 5 期間擲出例外,而非在此之前。文件可能在中途失敗。

管線走訪的複雜度為 O(token 數)。表格欄寬計算會新增一次有界、以表格為單位的逐列掃描(階段 5,TableParser)。啟用時,:has() 預掃描會新增一次有界的 token 清單遍歷(階段 4)。就樣式堆疊而言,記憶體用量是 O(巢狀深度) 而非 O(元素數)——詳見 串流限制。HTML 算繪管線的效能基準測試以 5% 的 gate 防止退化(已合併的成果,PR #564)。每頁的 performance_budgetwall_ms: 1500peak_mb: 64)是其運作上限。

階段 1 是第一道安全邊界:10 MB 輸入上限、控制字元移除,以及換行正規化都在 tokenize 之前執行。接著 DefaultHtmlSecurityPolicy 會在階段 5 期間,管控允許的標籤、屬性、CSS 屬性與 URL scheme。請參閱 HTML 模組安全模型一節。

換行正規化遵循 HTML 標準的換行處理(CRLF 與單獨的 CR 都會變成 LF)。 逐屬性的 CSS 符合性記載於 CSS 支援對照表,而串接行為則記載於 css-resolver一節。 本頁不重述逐屬性的支援情形。

企業版能力。 Premium 在同一條管線上擴大 CSS 涵蓋範圍。這六階段的順序在各版本之間都不會改變。請參閱 CSS 支援對照表一節。