Lewati ke konten

Merender HTML ke PDF dengan perender Chrome Artisan

Jembatan Artisan merender HTML melalui proses Chrome headless, lalu mengimpor hasilnya ke dokumen NextPDF sebagai Form XObject vektor. Teks tetap dapat dipilih dan dicari, bukan dirasterisasi. Anda dapat melampirkan ChromeRendererConfig, memanggil writeHtmlChrome() pada dokumen, atau menggunakan ChromeHtmlRenderer secara langsung, lalu membiarkan Chrome menangani tata letak. Panduan ini mencakup pemanggilan render, isolasi jaringan, penentuan ukuran halaman, tinggi konten, dan siklus hidup renderer jangka panjang untuk worker.

Prasyarat awal:

  • Inti NextPDF dan nextpdf/artisan sudah terpasang.
  • Biner Chrome atau Chromium sudah terpasang, dan pengguna worker dapat menjalankannya secara headless. Verifikasi dengan chromium --headless --dump-dom about:blank sebelum Anda mulai. Halaman penyiapan renderer Chrome yang ditautkan di bagian Lihat juga mencakup penyediaan biner dan keputusan sandbox kontainer.

Panduan ini mengasumsikan Anda dapat menjalankan proses Chrome di dekat aplikasi. Untuk contoh pertama yang dapat dijalankan, baca quickstart Artisan.

Pasang jembatan ini bersama inti NextPDF.

Terminal window
composer require nextpdf/artisan

Pasang build Chrome atau Chromium yang dapat dijalankan oleh pengguna worker. Pada Debian atau Ubuntu, gunakan paket distribusi.

Terminal window
apt-get install -y chromium

Pastikan biner dapat berjalan secara headless sebagai pengguna worker.

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

Kode keluar 0 dengan Document Object Model (DOM) kosong berarti biner dan pustaka pendukungnya tersedia. Kode keluar selain nol adalah kegagalan yang sama dengan yang dimunculkan jembatan sebagai ChromeRenderException. Perbaiki masalah ini terlebih dahulu.

writeHtmlChrome() adalah metode pada Document inti NextPDF. Metode ini memvalidasi masukan, menyelesaikan renderer Artisan, mengirim HTML ke Chrome melalui Chrome DevTools Protocol (CDP), mengurai PDF yang dikembalikan, lalu menyematkan halaman 0 sebagai Form XObject pada posisi kursor saat ini. Chrome berjalan sebagai proses anak dari worker PHP. Jembatan ini mengendalikan Chrome melalui CDP, bukan terhubung ke proses Chrome terpisah melalui port debugging, sehingga tidak ada endpoint jaringan yang perlu diekspos atau diautentikasi.

Jembatan ini merender dengan postur jaringan yang menolak secara bawaan. Setiap render menggunakan Content-Security-Policy yang menolak semua origin sumber daya (default-src 'none') dan hanya mengizinkan gambar inline (img-src data:). Jembatan ini juga memblokir setiap URL subsumber daya pada lapisan transport CDP dengan Network.setBlockedURLs(['*']). Akibatnya, gambar, lembar gaya, fon, skrip, atau iframe jarak jauh dalam HTML Anda tidak dimuat. Sematkan setiap aset secara inline sebagai URI data:. Dengan cara ini, jembatan menangani risiko server-side request forgery (SSRF) ketika merender HTML yang mungkin tidak tepercaya, dan berlaku apa pun konfigurasinya.

Model ukuran halaman memiliki dua mode. Jika Anda menyediakan lebar dan tinggi dalam satuan poin PDF, Chrome mencetak persis pada ukuran kertas tersebut. Jika tinggi dihilangkan atau null, jembatan mengukur tinggi konten yang dirender di Chrome, mengonversinya ke poin, lalu menambahkan buffer pengaman reflow kecil sekitar 14.4 poin. Ini mencegah printToPDF meluber ke halaman kedua, yang akan dipotong oleh pengimpor karena hanya membaca halaman 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 adalah satu-satunya permukaan konfigurasi. Objek ini bersifat tetap, jadi buat instance baru untuk mengubah suatu nilai. ChromeRenderResult::getPdfData() mengembalikan bita PDF. Halaman konfigurasi Artisan yang ditautkan di bagian Lihat juga mencantumkan referensi opsi lengkap dan flag peluncuran Chrome yang tetap.

Lampirkan konfigurasi ke dokumen, render HTML tepercaya, lalu simpan.

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 menangani tata letak flex, dan angka-angkanya tetap dapat dipilih pada keluaran karena halaman disematkan sebagai Form XObject vektor, bukan gambar raster. Untuk mencocokkan ukuran halaman A4 tetap, berikan lebar dan tinggi dalam satuan poin.

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

Dalam produksi, buat satu renderer per worker, suntikkan logger PSR-3, tangkap dua jenis eksepsi berbeda secara terpisah, dan lepaskan proses Chrome secara deterministik saat shutdown.

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();
}
}

Buat renderer sekali, lalu gunakan ulang. Browser pool yang mendasarinya menjaga satu proses Chrome tetap aktif dan memulai ulangnya setiap 100 render untuk membatasi pertumbuhan memori. Kedua cabang catch memisahkan kegagalan deployment, seperti runtime yang hilang, dari kegagalan saat render yang dapat Anda coba ulang sekali. Tidak ada blok catch yang kosong. Panggil shutdown() ketika worker melakukan shutdown untuk melepaskan proses Chrome, bukan menunggu destructor.

Buat konfigurasi dari array konfigurasi framework untuk menggunakan kunci snake-case, dan tetapkan chromeBinaryPath dalam produksi agar biner yang dipakai deterministik.

  • HTML kosong adalah no-op. writeHtmlChrome('') mengembalikan dokumen tanpa perubahan.
  • Belum ada halaman. Jika dokumen tidak memiliki halaman, writeHtmlChrome() menambahkan satu sebelum merender.
  • Aset jarak jauh tidak dimuat — sesuai desain. <img src="https://..."> dirender kosong. Sematkan setiap aset secara inline sebagai URI data:. Ini adalah postur isolasi jaringan, bukan cacat.
  • Hanya halaman 0 yang diimpor. Penyesuaian tinggi otomatis menambahkan buffer reflow sehingga satu halaman dihasilkan. Dengan tinggi eksplisit, tidak ada buffer yang ditambahkan dan keluaran dicocokkan persis dengan ukuran kertas yang diminta, jadi tetapkan tinggi agar sesuai dengan konten Anda.
  • Jembatan hilang. Jika nextpdf/artisan tidak terpasang, inti memunculkan eksepsi tata letak alih-alih galat fatal. Jika pustaka chrome-php/chrome tidak ada, jembatan memunculkan ChromeNotAvailableException beserta perintah instalasinya.
  • defaultCss dan </style>. Setiap urutan </style> dalam defaultCss dihapus sebelum injeksi sebagai pertahanan terhadap pelolosan dari blok gaya (style-breakout). Perhitungkan hal ini jika Anda membuat templat CSS.

Render pertama menanggung biaya startup dan tata letak Chrome. Render berikutnya menggunakan ulang proses Chrome yang aktif, sehingga biasanya tidak menanggung biaya startup. Buat satu renderer per worker dan gunakan ulang. Jangan membuat satu renderer per permintaan. Perkirakan lonjakan latensi pada setiap render ke-100, ketika jembatan memulai ulang proses Chrome untuk membatasi memori. Masukkan hal itu ke target latensi Anda, bukan memperlakukannya sebagai insiden. Selaraskan renderTimeout dengan anggaran permintaan upstream pada jalur mana pun yang dapat dijangkau oleh masukan tidak tepercaya.

  • Isolasi jaringan adalah kendali utama. Jembatan ini sama sekali tidak mengizinkan pengambilan subsumber daya keluar: CSP default-src 'none' ditambah pemblokiran setiap URL pada lapisan transport CDP. Jembatan ini tidak menerapkan daftar izin domain karena memang tidak memerlukannya. Sematkan aset secara inline sebagai URI data:.
  • Masukan dibatasi sebelum Chrome dihubungi. Jembatan ini menolak HTML yang melebihi maxHtmlSize (standar 5 MB), URI data base64 yang terlalu besar (pengaman terhadap bom dekompresi), dan tag <meta http-equiv="refresh"> apa pun (yang dapat mengarahkan navigasi ke endpoint internal). Pertahankan maxHtmlSize pada nilai standar kecuali ada beban kerja yang diketahui membutuhkan nilai lebih tinggi. Menaikkannya memperluas permukaan risiko kehabisan sumber daya.
  • Sandbox Chrome adalah kendali terpisah. Menyetel noSandbox: true meluncurkan Chrome dengan --no-sandbox, yang menghapus isolasi proses Chrome. Ini adalah penurunan kontainmen yang nyata, bukan flag kosmetik. Biarkan tetap false di luar kontainer. Ketika sandbox kontainer tidak dapat diinisialisasi, jalankan Chrome sebagai pengguna non-root dalam kontainer yang dibatasi, dan perlakukan deployment itu sebagai kondisi yang mengharuskan tingkat kepercayaan lebih tinggi terhadap masukan.
  • Log hanya membawa metadata. Suntikkan logger PSR-3. Jembatan ini mencatat panjang bita, dimensi, dan peristiwa siklus hidup, tetapi tidak pernah mencatat HTML, bita PDF, atau teks yang diekstrak.
  • Jangan pernah mengekspos port remote-debugging Chrome. Jembatan ini tidak menggunakannya, dan port CDP yang terbuka adalah saluran kendali tanpa autentikasi.

Model ancaman lengkap, termasuk pertahanan SSRF, batas sandbox eksplisit, dan katalog mode kegagalan, terdapat pada halaman security-and-operations Artisan yang ditautkan di bawah Lihat juga. Halaman itu menetapkan klausul OWASP, CWE, dan NIST yang relevan.

Panduan ini tidak membuat klaim standar normatif tersendiri. Halaman security-and-operations Artisan upstream memetakan kendali jaringan, isolasi, dan penghabisan sumber daya jembatan ke OWASP ASVS, CWE Top 25 (SSRF / konsumsi sumber daya tak terkendali), dan NIST SP 800-53 SC-7. Halaman cookbook ini menyatakan kembali penggunaannya dan menyerahkan kutipan normatif tersebut ke halaman itu. Jembatan ini tidak melakukan operasi kriptografi apa pun; penandatanganan dan enkripsi adalah urusan inti atau edisi komersial dan tidak terpengaruh oleh Artisan.