Ga naar inhoud

HTML naar PDF renderen met de Artisan-Chrome-renderer

De Artisan-bridge rendert HTML met een headless Chrome-proces en importeert het resultaat vervolgens als een vector-Form-XObject in een NextPDF-document. De tekst blijft selecteerbaar en doorzoekbaar in plaats van te worden gerasterd. U koppelt een ChromeRendererConfig, roept writeHtmlChrome() aan op een document of gebruikt ChromeHtmlRenderer rechtstreeks, waarna Chrome de lay-out verzorgt. Deze handleiding behandelt de renderaanroep, netwerkisolatie, paginaformaat, contenthoogte en de levenscyclus van een langlevende renderer in een worker.

De vereisten vooraf:

  • NextPDF core en nextpdf/artisan zijn geïnstalleerd.
  • Er is een Chrome- of Chromium-binary geïnstalleerd en de worker-gebruiker kan die headless uitvoeren. Controleer dit met chromium --headless --dump-dom about:blank voordat u begint. De pagina over het opzetten van de Chrome-renderer, waarnaar onder Zie ook wordt gelinkt, behandelt het beschikbaar stellen van de binary en de keuze voor de container-sandbox.

Deze handleiding gaat ervan uit dat u een Chrome-proces dicht bij de applicatie kunt uitvoeren. Lees voor het eerste uitvoerbare voorbeeld de Artisan-quickstart.

Installeer de bridge naast core.

Terminal window
composer require nextpdf/artisan

Installeer een Chrome- of Chromium-build die de worker-gebruiker kan uitvoeren. Gebruik op Debian of Ubuntu het distributiepakket.

Terminal window
apt-get install -y chromium

Controleer dat de binary headless draait als de worker-gebruiker.

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

Exitcode 0 met een leeg Document Object Model (DOM) betekent dat de binary en de bijbehorende gedeelde libraries aanwezig zijn. Een exitcode die niet nul is, levert dezelfde fout op die de bridge rapporteert als een ChromeRenderException. Los dit hier eerst op.

writeHtmlChrome() is een methode op de Document van NextPDF core. De methode valideert de invoer, resolvet de Artisan-renderer, stuurt de HTML naar Chrome via het Chrome DevTools Protocol (CDP), parseert de teruggegeven PDF en sluit pagina 0 in als een Form XObject op de huidige cursorpositie. Chrome draait als een onderliggend proces van de PHP-worker. De bridge bestuurt Chrome via CDP in plaats van verbinding te maken met een afzonderlijk Chrome-proces via een debugpoort, dus er is geen netwerk-endpoint dat blootgesteld of geauthenticeerd hoeft te worden.

De bridge rendert met een netwerkbeleid dat standaard alles weigert. Elke render gebruikt een Content-Security-Policy die alle resource-origins weigert (default-src 'none') en alleen inline-afbeeldingen toestaat (img-src data:). De bridge blokkeert daarnaast elke subresource-URL op de CDP-transportlaag met Network.setBlockedURLs(['*']). Daardoor wordt een externe afbeelding, stylesheet, lettertype, script of iframe in uw HTML niet geladen. Neem elke asset inline op als een data:-URI. Zo pakt de bridge het risico van server-side request forgery (SSRF) aan wanneer deze HTML rendert die mogelijk niet vertrouwd kan worden, en dit geldt ongeacht de configuratie.

Het model voor paginaformaat heeft twee modi. Wanneer u zowel breedte als hoogte in PDF-punten opgeeft, drukt Chrome af op precies dat papierformaat. Wanneer de hoogte wordt weggelaten of null is, meet de bridge de gerenderde contenthoogte in Chrome, zet die om naar punten en voegt een kleine veiligheidsmarge voor reflow van ongeveer 14,4 punten toe. Dat voorkomt dat printToPDF overloopt naar een tweede pagina die de importer, die alleen pagina 0 verwerkt, zou afkappen.

// 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 is het enige configuratieoppervlak. Het object is onveranderlijk, dus bouw een nieuwe instantie om een waarde te wijzigen. ChromeRenderResult::getPdfData() geeft de PDF-bytes terug. De Artisan-configuratiepagina, waarnaar onder Zie ook wordt gelinkt, bevat de volledige optiereferentie en de vaste Chrome-startvlaggen.

Koppel de config aan een document, render vertrouwde HTML en sla het resultaat op.

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 verzorgt de flex-lay-out en de cijfers blijven selecteerbaar in de uitvoer, omdat de pagina als een vector-Form-XObject wordt ingesloten en niet als een rasterafbeelding. Geef breedte en hoogte in punten op om op een vaste A4-pagina te passen.

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

Maak in productie één renderer per worker, injecteer een PSR-3-logger, vang de twee verschillende exceptietypen afzonderlijk af en geef het Chrome-proces deterministisch vrij bij het afsluiten.

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

Bouw de renderer één keer en hergebruik deze vervolgens. De onderliggende browserpool houdt één Chrome-proces in leven en herstart het elke 100 renders om de geheugengroei te begrenzen. De twee catch-takken scheiden een implementatiefout, zoals een ontbrekende runtime, van een renderfout waarbij u het eenmaal opnieuw kunt proberen. Geen van beide catch-blokken is leeg. Roep shutdown() aan wanneer de worker wordt afgesloten om het Chrome-proces vrij te geven in plaats van te wachten op de destructor.

Bouw de config op vanuit een framework-configuratiearray om snake-case-sleutels te gebruiken, en pin chromeBinaryPath vast in productie, zodat deterministisch vastligt welke binary wordt gebruikt.

  • Lege HTML doet niets. writeHtmlChrome('') geeft het document ongewijzigd terug.
  • Nog geen pagina. Als het document geen pagina heeft, voegt writeHtmlChrome() er een toe vóór het renderen.
  • Externe assets worden niet geladen — met opzet. <img src="https://..."> wordt leeg gerenderd. Neem elke asset inline op als een data:-URI. Dit is het netwerkisolatiebeleid, geen defect.
  • Alleen pagina 0 wordt geïmporteerd. Bij automatisch passende hoogte wordt de reflow-marge toegevoegd, zodat één pagina wordt geproduceerd. Bij een expliciete hoogte wordt geen marge toegevoegd en komt de uitvoer exact overeen met het opgevraagde papierformaat; stem de hoogte dus af op uw content.
  • Bridge ontbreekt. Als nextpdf/artisan niet is geïnstalleerd, werpt core een lay-out-exceptie op in plaats van een fatale fout. Als de library chrome-php/chrome ontbreekt, werpt de bridge een ChromeNotAvailableException op met het installatiecommando.
  • defaultCss en </style>. Elke </style>-reeks in defaultCss wordt vóór injectie verwijderd als verdediging tegen een style-breakout. Houd daar rekening mee wanneer u CSS via templates samenstelt.

De eerste render betaalt de kosten voor het opstarten van Chrome en voor de lay-out. Latere renders hergebruiken het draaiende Chrome-proces, dus betalen ze zelden de opstartkosten. Maak één renderer per worker en hergebruik deze. Maak er niet één per verzoek aan. Verwacht een latentiepiek bij elke 100e render, wanneer de bridge het Chrome-proces herstart om het geheugen te begrenzen. Houd daar rekening mee in uw latentiedoelstellingen in plaats van het als een incident te behandelen. Combineer renderTimeout met een upstream verzoekbudget op elk pad dat bereikbaar is via niet-vertrouwde invoer.

  • Netwerkisolatie is de primaire maatregel. De bridge staat geen enkele uitgaande subresource-fetch toe: CSP default-src 'none' plus een blokkering van elke URL op CDP-transportniveau. De bridge implementeert geen domein-allowlist, omdat die niet nodig is. Neem assets inline op als data:-URI’s.
  • De invoer wordt begrensd voordat Chrome wordt aangesproken. De bridge weigert HTML boven maxHtmlSize (standaard 5 MB), een te grote base64-data-URI (een maatregel tegen een decompressiebom) en elke <meta http-equiv="refresh">-tag (die een navigatie naar een intern endpoint zou kunnen aansturen). Houd maxHtmlSize op de standaardwaarde, tenzij een bekende werklast meer nodig heeft. Het verhogen ervan vergroot het aanvalsoppervlak voor resource-uitputting.
  • De Chrome-sandbox is een afzonderlijke maatregel. Door noSandbox: true in te stellen start Chrome met --no-sandbox, waardoor de procesisolatie van Chrome wordt verwijderd. Dat is een werkelijke vermindering van de isolatie, geen cosmetische vlag. Laat het buiten containers op false staan. Wanneer de container-sandbox niet kan initialiseren, voer Chrome dan uit als een niet-root-gebruiker in een afgeschermde container, en behandel de implementatie als een scenario waarin de invoer meer vertrouwen vereist.
  • Logs bevatten alleen metadata. Injecteer een PSR-3-logger. De bridge logt bytelengtes, afmetingen en levenscyclusgebeurtenissen, nooit HTML, PDF-bytes of geëxtraheerde tekst.
  • Stel nooit een Chrome-remote-debuggingpoort bloot. De bridge gebruikt er geen en een open CDP-poort is een niet-geauthenticeerd controlekanaal.

Het volledige dreigingsmodel, inclusief de SSRF-verdediging, de expliciete sandboxgrens en de catalogus van faalmodi, staat op de Artisan-pagina over beveiliging en operations, gelinkt onder Zie ook. Die pagina legt de relevante OWASP-, CWE- en NIST-clausules vast.

Deze handleiding doet zelf geen normatieve standaardenclaim. De bovenliggende Artisan-pagina over beveiliging en operations koppelt de netwerk-, isolatie- en resource-uitputtingsmaatregelen van de bridge aan OWASP ASVS, de CWE Top 25 (SSRF / ongecontroleerd resourceverbruik) en NIST SP 800-53 SC-7. Deze cookbookpagina herhaalt het gebruik en laat die normatieve citaties aan die pagina over. De bridge voert geen cryptografische bewerking uit; ondertekening en versleuteling vallen onder core of de commerciële editie en worden niet beïnvloed door Artisan.