使用 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,因此不存在需要對外開放或驗證的網路端點(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 安全性與維運 — 網路隔離模型、沙箱邊界,以及各種失敗模式。