跳转到内容

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 支持对照表一节。