跳到內容

使用 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 一併安裝。

Terminal window
composer require nextpdf/artisan

安裝 worker 使用者可執行的 Chrome 或 Chromium 版本。在 Debian 或 Ubuntu 上,請使用發行版套件。

Terminal window
apt-get install -y chromium

確認該執行檔能以 worker 使用者身分無頭執行。

Terminal window
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 頁的匯入器裁掉。

// 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): ChromeRenderResult
ChromeHtmlRenderer::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): self

ChromeRendererConfig 是唯一的組態介面,而且不可變;若要變更某個值,請建構一個新實例。ChromeRenderResult::getPdfData() 會回傳 PDF 位元組。完整的選項參考,以及固定的 Chrome 啟動旗標,都在「另請參閱」連結的 Artisan 組態頁。

將組態掛載到文件上,繪製可信的 HTML,然後存檔。

render-quickstart.php
<?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 頁面,請以點為單位傳入寬度與高度。

explicit A4 page size
$document->writeHtmlChrome($html, width: 595.28, height: 841.89);

在正式環境中,請為每個 worker 建構一個 renderer,注入一個 PSR-3 logger,分別捕捉兩種不同的例外型別,並在關閉時明確釋放 Chrome 程序。

ReportRenderer.php
<?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 影響。