コンテンツにスキップ

Artisan Chrome レンダラーで HTML を PDF に変換する

Artisan ブリッジは、ヘッドレス Chrome プロセスで HTML をレンダリングし、その結果をベクター Form XObject として NextPDF ドキュメントにインポートします。テキストはラスタライズされず、選択および検索が可能な状態で保持されます。ChromeRendererConfig をアタッチし、ドキュメントで writeHtmlChrome() を呼び出すと(または ChromeHtmlRenderer を直接使うと)、Chrome がレイアウトを処理します。このガイドでは、レンダリングの呼び出し方、ネットワーク分離ポリシー、ページサイズとコンテンツ高さのモデル、ワーカー向けの長寿命レンダラーのライフサイクルを説明します。

前提条件は次のとおりです。

  • NextPDF core と nextpdf/artisan がインストールされていること。
  • Chrome または Chromium のバイナリがインストールされており、ワーカーユーザーがヘッドレスで実行できること。開始する前に chromium --headless --dump-dom about:blank で確認してください。バイナリのプロビジョニングとコンテナサンドボックスの判断については、「関連項目」にリンクされている Chrome レンダラーのセットアップページを参照してください。

このページは how-to ガイドです。アプリケーションの近くで Chrome プロセスを実行できることを前提にしています。まず動作する例を確認したい場合は、Artisan クイックスタートをお読みください。

core とあわせてブリッジをインストールします。

Terminal window
composer require nextpdf/artisan

ワーカーユーザーが実行できる Chrome または Chromium のビルドをインストールします。Debian または Ubuntu では、ディストリビューションのパッケージを使用します。

Terminal window
apt-get install -y chromium

ワーカーユーザーとしてバイナリがヘッドレスで動作することを確認します。

Terminal window
chromium --headless --dump-dom about:blank

終了コードが 0 で DOM が空であれば、バイナリと必要な共有ライブラリが存在していることを示します。0 以外の終了コードは、ブリッジが ChromeRenderException として表面化させる障害と同じものです。まずこの段階で解消してください。

writeHtmlChrome() は、NextPDF core の Document のメソッドです。このメソッドは入力を検証し、Artisan レンダラーを resolve(解決)して、Chrome DevTools Protocol(CDP)経由で HTML を Chrome に送信します。返された PDF を解析し、ページ 0 を Form XObject として現在のカーソル位置に埋め込みます。Chrome は PHP ワーカーの子プロセスとして実行されます。ブリッジは、デバッグポート経由で別途実行されている Chrome に接続するのではなく、CDP 経由でそのプロセスを制御するため、公開したり認証したりする必要のあるネットワークエンドポイントは存在しません。

ブリッジは、デフォルト拒否のネットワークポリシー下でレンダリングします。すべてのレンダリングは、すべてのリソースオリジンを拒否し(default-src 'none')、インライン画像のみを許可する(img-src data:)Content-Security-Policy でラップされます。ブリッジはさらに、Network.setBlockedURLs(['*']) によって、CDP のトランスポート層ですべてのサブリソース URL をブロックします。その結果、HTML 内のリモート画像、スタイルシート、フォント、スクリプト、iframe は読み込まれません。すべてのアセットを data: URI としてインライン化してください。これは、信頼できない可能性がある HTML をレンダリングする際のサーバーサイドリクエストフォージェリ(SSRF)リスクに対するブリッジ側の対策であり、設定にかかわらず維持されます。

ページサイズのモデルには 2 つのモードがあります。幅と高さの両方を(PDF ポイント単位で)指定した場合、Chrome はそのとおりの用紙サイズで印刷します。高さを省略するか null にした場合、ブリッジは Chrome 内でレンダリングされたコンテンツの高さを測定し、ポイントに変換したうえで、リフローに備えた小さな安全バッファ(約 14.4 ポイント)を追加します。これにより、printToPDF の出力が 2 ページ目にはみ出し、ページ 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);

本番環境では、ワーカーごとに 1 つのレンダラーを構築し、PSR-3 ロガーを注入し、2 つの異なる例外型を個別にキャッチして、シャットダウン時に 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();
}
}

レンダラーは一度だけ構築し、再利用します。内部のブラウザープールは 1 つの Chrome プロセスを稼働させ続け、メモリ増加を抑えるために 100 回のレンダリングごとに再起動します。2 つの catch 節は、デプロイ時の障害(ランタイムの欠如)とレンダリング時の障害(リトライ可能)を分離しており、どちらの catch ブロックも空ではありません。デストラクタを待たず、ワーカーのシャットダウン時に shutdown() を呼び出して Chrome プロセスを解放してください。

スネークケースのキーを使えるよう、フレームワークの設定配列から設定を構築してください。本番環境では、使用するバイナリを確定させるために chromeBinaryPath を固定してください。

  • 空の HTML は何もしません。 writeHtmlChrome('') はドキュメントを変更せずに返します。
  • まだページがない場合。 ドキュメントにページがない場合、writeHtmlChrome() はレンダリング前にページを 1 つ追加します。
  • リモートアセットは読み込まれません — 設計上の仕様です。 <img src="https://..."> は空でレンダリングされます。すべてのアセットを data: URI としてインライン化してください。これは不具合ではなく、ネットワーク分離ポリシーによるものです。
  • インポートされるのはページ 0 のみです。 高さの自動フィットではリフロー用バッファが追加されるため、1 ページが生成されます。高さを明示的に指定した場合はバッファが追加されず、出力は要求した用紙サイズと正確に一致するため、コンテンツに合わせて高さを設定してください。
  • ブリッジが存在しない場合。 nextpdf/artisan がインストールされていない場合、core は致命的なエラーではなくレイアウト例外を発生させます。chrome-php/chrome ライブラリが存在しない場合、ブリッジはインストールコマンドを添えて ChromeNotAvailableException を発生させます。
  • defaultCss</style> defaultCss 内のあらゆる </style> シーケンスは、スタイルブレイクアウト対策として、注入前に除去されます。CSS をテンプレート化する場合は、この挙動を前提に設計してください。

初回レンダリングでは、Chrome の起動とレイアウトのコストがかかります。以降のレンダリングでは稼働中の Chrome プロセスを再利用するため、起動コストはほとんど発生しません。ワーカーごとに 1 つのレンダラーを構築して再利用してください。リクエストごとに作成しないでください。ブリッジはメモリ使用量を抑えるため、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 を設定すると、Chrome は --no-sandbox 付きで起動し、Chrome のプロセス分離が取り除かれます。これは見せかけのフラグではなく、封じ込めの実質的な低下です。コンテナの外では false のままにしてください。コンテナサンドボックスを初期化できない場合は、制約付きのコンテナ内で Chrome を非 root ユーザーとして実行し、そのデプロイは入力に対してより高い信頼を要求するものとして扱ってください。
  • ログにはメタデータのみが記録されます。 PSR-3 ロガーを注入してください。ブリッジはバイト長、寸法、ライフサイクルイベントをログに記録し、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 の影響を受けません。