使用 Artisan Chrome renderer(渲染器)将 HTML 渲染为 PDF
Artisan 桥接会通过 headless Chrome 进程渲染 HTML,然后将结果以向量 Form XObject 的形式导入 NextPDF 文档。文字保持可选取、可搜索,不会被栅格化。你需要在文档上挂载一个 ChromeRendererConfig,并在文档上调用 writeHtmlChrome()(或直接使用 ChromeHtmlRenderer);版面由 Chrome 处理。本指南涵盖渲染调用、网络隔离策略、页面大小与内容高度模型,以及 worker 中长驻 renderer 的生命周期。
先确认以下先决条件:
- 已安装 NextPDF core 和
nextpdf/artisan。 - 已安装 Chrome 或 Chromium 可执行文件,且 worker 用户能够以无头(headless)方式运行它。开始前请先用
chromium --headless --dump-dom about:blank验证。可执行文件的预配,以及容器沙箱(sandbox)的取舍决策,都在“另请参阅”链接的 Chrome renderer 设置页说明。
这是一篇 how-to。它假设你能在应用程序旁运行一个 Chrome 进程。如需可直接运行的示例,请阅读 Artisan 快速入门。
把桥接包和 core 一起安装。
composer require nextpdf/artisan安装一份 worker 用户可运行的 Chrome 或 Chromium 版本。在 Debian 或 Ubuntu 上,使用发行版软件包。
apt-get install -y chromium确认该可执行文件能以 worker 用户身份无头运行。
chromium --headless --dump-dom about:blank退出代码为 0 且 DOM 为空,表示可执行文件及其共享库都已就绪。非零退出代码,与桥接以 ChromeRenderException 呈现的是同一类失败。请先在这里修复它。
概念总览
标题为“概念总览”的章节writeHtmlChrome() 是 NextPDF core Document 上的方法。它会验证输入、resolve(解析)出 Artisan renderer、通过 Chrome DevTools Protocol(CDP)把 HTML 发送给 Chrome、解析返回的 PDF,并在当前游标位置将第 0 页嵌入为 Form XObject。Chrome 以 PHP worker 的子进程身份运行。桥接通过 CDP 驱动 Chrome,而不是经由调试端口连接到另一个独立运行的 Chrome,因此没有需要对外开放或验证的网络端点(endpoint)。
桥接以“默认拒绝”的网络姿态进行渲染。每一次渲染都包裹在一段 Content-Security-Policy 中,拒绝所有资源来源(default-src 'none'),仅允许内嵌图像(img-src data:)。桥接还会在 CDP 传输层使用 Network.setBlockedURLs(['*']) 封锁每一个子资源 URL。因此,你的 HTML 中的远程图像、样式表、字体、脚本或 iframe 都不会加载。请把每个资产内嵌成 data: URI。这是桥接在渲染可能不可信 HTML 时,对服务器端请求伪造(SSRF)风险的应对,并且无论配置如何都成立。
页面大小模型有两种模式。当你同时提供宽度与高度(以 PDF 点为单位)时,Chrome 会精确打印到该纸张大小。当高度被省略或为 null 时,桥接会在 Chrome 中测量渲染后的内容高度、换算成点,并加上一小段重排安全缓冲(约 14.4 点),避免 printToPDF 溢出到第二页后被只取第 0 页的导入器裁掉。
API 接口
标题为“API 接口”的章节// On a NextPDF core Document (the HasTextOutput concern):writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static
// The standalone renderer:new ChromeHtmlRenderer(ChromeRendererConfig $config, ?LoggerInterface $logger = null)ChromeHtmlRenderer::render(string $html, float $widthPt, float $heightPt = 0.0): ChromeRenderResultChromeHtmlRenderer::close(): void
// The configuration value object (final readonly):new ChromeRendererConfig( ?string $chromeBinaryPath = null, int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5_000_000, bool $noSandbox = false,)ChromeRendererConfig::fromArray(array $config): selfChromeRendererConfig 是唯一的配置接口,而且不可变,所以要变更某个值时请构建一个新实例。ChromeRenderResult::getPdfData() 会返回 PDF 字节。完整的选项参考,以及固定的 Chrome 启动标志,都在“另请参阅”链接的 Artisan 配置页。
代码示例 — 快速开始
标题为“代码示例 — 快速开始”的章节把配置挂载到文档上,渲染可信 HTML,然后保存。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Artisan\ChromeRendererConfig;use NextPDF\Core\Document;
$config = new ChromeRendererConfig( chromeBinaryPath: '/usr/bin/chromium',);
$document = Document::createStandalone();$document->setChromeRendererConfig($config);$document->addPage();
$document->writeHtmlChrome(' <div style="display: flex; gap: 20px; font-family: sans-serif;"> <div style="flex: 1; background: #f0f0f0; padding: 24px;"> <h2>Revenue</h2> <p style="font-size: 2em; color: #2563eb;">$124,500</p> </div> <div style="flex: 1; background: #f0f0f0; padding: 24px;"> <h2>Orders</h2> <p style="font-size: 2em; color: #16a34a;">1,847</p> </div> </div>');
$document->save('/tmp/report.pdf');Chrome 会处理 flex 版面;由于该页以向量 Form XObject 而非栅格图像形式嵌入,输出中的数字仍保持可选取。若要匹配固定的 A4 页面,请以点为单位传入宽度与高度。
$document->writeHtmlChrome($html, width: 595.28, height: 841.89);代码示例 — 生产环境
标题为“代码示例 — 生产环境”的章节在生产环境中,请为每个 worker 构建一个 renderer、注入一个 PSR-3 logger、分别捕获两种不同的异常类型,并在关闭时以确定性方式释放 Chrome 进程。
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;use NextPDF\Artisan\ChromeRendererConfig;use NextPDF\Artisan\Exception\ChromeNotAvailableException;use NextPDF\Artisan\Exception\ChromeRenderException;use Psr\Log\LoggerInterface;
final class ReportRenderer{ private ChromeHtmlRenderer $renderer;
public function __construct(LoggerInterface $logger) { $config = ChromeRendererConfig::fromArray([ 'chrome_binary' => getenv('CHROME_BINARY') ?: null, 'render_timeout' => 45, 'max_html_size' => 2_000_000, 'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'), ]);
$this->renderer = new ChromeHtmlRenderer($config, $logger); }
public function render(string $html, float $widthPt, float $heightPt = 0.0): string { try { return $this->renderer->render($html, $widthPt, $heightPt)->getPdfData(); } catch (ChromeNotAvailableException $exception) { // Deployment fault: the Chrome runtime is missing. Page on-call. throw $exception; } catch (ChromeRenderException $exception) { // Render-time fault: timeout, crash, or empty output. Retryable once. throw $exception; } }
public function shutdown(): void { $this->renderer->close(); }}renderer 只构建一次并重复使用。底层浏览器池会让一个 Chrome 进程保持存活,并每 100 次渲染重启一次,以限制内存增长。这两个 catch 分支把部署错误(runtime 缺失)与渲染期错误(可重试)分开,且两个 catch 块都不是空的。请在 worker 关闭时调用 shutdown() 释放 Chrome 进程,而不是等待析构函数。
从 Framework(框架)的配置数组构建配置,以获取 snake-case 键,并在生产环境中固定 chromeBinaryPath,确保使用确定的可执行文件。
边界情况与陷阱
标题为“边界情况与陷阱”的章节- 空 HTML 是 no-op。
writeHtmlChrome('')会原封不动返回文档。 - 尚无页面。 若文档还没有任何页面,
writeHtmlChrome()会在渲染前先加上一页。 - 远程资产不会加载 — 这是刻意设计。
<img src="https://...">会渲染成空白。请把每个资产内嵌成data:URI。这是网络隔离姿态,不是缺陷。 - 只导入第 0 页。 自动适配高度会加上重排缓冲,使输出保持为单页。若指定明确高度,则不会加上缓冲,输出会精确符合所请求的纸张大小,因此请把高度设成刚好能容纳你的内容。
- 桥接缺失。 若未安装
nextpdf/artisan,core 会抛出版面异常,而不是致命错误。若缺少chrome-php/chrome库,桥接会抛出ChromeNotAvailableException,并附上安装命令。 defaultCss与</style>。 作为样式逃逸防御,任何</style>序列在defaultCss中都会在注入前被移除。如果你会用模板生成 CSS,请把这一点纳入规划。
第一次渲染需要付出 Chrome 启动和版面计算成本。后续渲染会重用存活中的 Chrome 进程,因此很少再次付出启动成本。请为每个 worker 构建一个 renderer 并重复使用,不要为每个请求单独构建。每到第 100 次渲染、也就是桥接为限制内存而重启 Chrome 进程时,要预期会有一次延迟尖峰。请把这一点纳入延迟目标,而不是把它当成事故处理。在任何可被不可信输入触及的路径上,请把 renderTimeout 与上游请求预算搭配使用。
安全性注意事项
标题为“安全性注意事项”的章节- 网络隔离是主要的控制措施。 桥接完全不允许任何对外的子资源抓取 — CSP
default-src 'none'再加上 CDP 传输层对每个 URL 的封锁。它不实现域名允许列表,因为没有必要。请把资产内嵌成data:URI。 - 输入会在连到 Chrome 之前先经过边界限制。 桥接会拒绝超过
maxHtmlSize(默认 5 MB)的 HTML、过大的 base64 data URI(一项解压缩炸弹防护),以及任何<meta http-equiv="refresh">标签(它可能触发一次指向内部端点的导航)。除非已知工作负载需要更多,否则请把maxHtmlSize维持在默认值。调高它会扩大资源耗尽攻击面。 - Chrome 沙箱是另一道独立的控制措施。 设置
noSandbox: true会以--no-sandbox启动 Chrome,这会移除 Chrome 的进程隔离 — 是实质的围堵能力降低,而非装饰性的标志。在容器之外,请把它维持为false。当容器沙箱无法初始化时,请以非 root 用户在受限的容器中运行 Chrome,并把该部署视为对输入有更高信任度要求的场景。 - 日志只携带元数据。 请注入一个 PSR-3 logger。桥接会记录字节长度、尺寸与生命周期事件,绝不记录 HTML、PDF 字节或抽取出的文字。
- 绝不要对外开放 Chrome 的远程调试端口。 桥接并不使用它,而开启的 CDP 端口是未经验证的控制通道。
完整的威胁模型 — SSRF 防御、明确陈述的沙箱边界,以及失败模式目录 — 都在“另请参阅”链接的 Artisan 安全性与运维页,该页固定引用了相关的 OWASP、CWE 与 NIST 条款。
符合性
标题为“符合性”的章节本指南本身不提出任何规范性的标准声明。桥接的网络、隔离与资源耗尽控制,已在上游的 Artisan 安全性与运维页映射到 OWASP ASVS、CWE Top 25(SSRF/不受控的资源消耗),以及 NIST SP 800-53 SC-7。本 cookbook 页面只重述用法,并把这些规范性引用留给该页。桥接不执行任何密码学运算 — 签名与加密属于 core 或商业版的范畴,不受 Artisan 影响。
另请参阅
标题为“另请参阅”的章节- 用 Cloudflare 在边缘渲染 — 在边缘渲染 HTML,并具备本地后备。
- Artisan 快速入门 — 最精简的首次渲染。
- Chrome renderer 设置 — 预配可执行文件、容器沙箱的取舍决策,以及健康探测。
- Artisan 安全性与运维 — 网络隔离模型、沙箱边界,以及各种失败模式。