Zum Inhalt springen

HTML mit dem Artisan-Chrome-Renderer als PDF rendern

Die Artisan-Brücke rendert HTML über einen Headless-Chrome-Prozess und importiert das Ergebnis als Vektor-Form-XObject in ein NextPDF-Dokument. Der Text bleibt auswählbar und durchsuchbar, statt gerastert zu werden. Sie hängen eine ChromeRendererConfig an, rufen writeHtmlChrome() auf einem Dokument auf (oder verwenden ChromeHtmlRenderer direkt), und Chrome übernimmt das Layout. Diese Anleitung behandelt den Render-Aufruf, die Netzwerkisolationsrichtlinie, das Seitengrößen- und Inhaltshöhenmodell sowie den Renderer-Lebenszyklus in einem langlebigen Worker.

Vorab gelten diese Voraussetzungen:

  • NextPDF Core und nextpdf/artisan sind installiert.
  • Eine Chrome- oder Chromium-Binary ist installiert, und der Worker-Benutzer kann sie headless ausführen. Prüfen Sie das vor dem Start mit chromium --headless --dump-dom about:blank. Hinweise zur Bereitstellung der Binary und zur Entscheidung über die Container-Sandbox finden Sie auf der unter „Siehe auch“ verlinkten Seite zur Chrome-Renderer-Einrichtung.

Dies ist eine Schritt-für-Schritt-Anleitung. Sie setzt voraus, dass Sie einen Chrome-Prozess nahe bei der Anwendung ausführen können. Für ein erstes lauffähiges Beispiel lesen Sie den Artisan-Schnellstart.

Installieren Sie die Brücke zusammen mit Core.

Terminal-Fenster
composer require nextpdf/artisan

Installieren Sie einen Chrome- oder Chromium-Build, den der Worker-Benutzer ausführen kann. Unter Debian oder Ubuntu verwenden Sie das Distributionspaket.

Terminal-Fenster
apt-get install -y chromium

Stellen Sie sicher, dass die Binary als Worker-Benutzer headless läuft.

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

Exit-Code 0 mit leerem DOM bedeutet, dass die Binary und ihre Shared Libraries vorhanden sind. Ein Exit-Code ungleich null entspricht dem Fehler, den die Brücke als ChromeRenderException meldet. Beheben Sie ihn zuerst an dieser Stelle.

writeHtmlChrome() ist eine Methode des NextPDF-Core-Document. Sie validiert die Eingabe, löst den Artisan-Renderer auf, sendet das HTML über das Chrome DevTools Protocol (CDP) an Chrome, parst das zurückgegebene PDF und bettet Seite 0 als Form-XObject an der aktuellen Cursorposition ein. Chrome läuft als Kindprozess des PHP-Workers. Die Brücke steuert ihn über CDP, statt sich über einen Debugging-Port mit einem separat laufenden Chrome zu verbinden, sodass es keinen Netzwerkendpunkt gibt, den Sie freigeben oder authentifizieren müssten.

Die Brücke rendert mit einer Netzwerkhaltung, die standardmäßig alles verweigert. Jeder Rendervorgang wird in eine Content-Security-Policy gehüllt, die alle Ressourcenursprünge verweigert (default-src 'none') und nur Inline-Bilder erlaubt (img-src data:). Die Brücke blockiert außerdem jede Subressourcen-URL auf der CDP-Transportschicht mit Network.setBlockedURLs(['*']). Dadurch lädt ein entferntes Bild, Stylesheet, eine entfernte Schrift, ein entferntes Skript oder ein iframe in Ihrem HTML nicht. Binden Sie jedes Asset als data:-URI inline ein. Das ist die Antwort der Brücke auf das Risiko durch Server-Side Request Forgery (SSRF) beim Rendern von potenziell nicht vertrauenswürdigem HTML, und sie gilt unabhängig von der Konfiguration.

Das Seitengrößenmodell hat zwei Modi. Wenn Sie sowohl Breite als auch Höhe angeben (in PDF-Punkt), druckt Chrome genau auf dieses Papierformat. Wird die Höhe weggelassen oder ist sie null, misst die Brücke die gerenderte Inhaltshöhe in Chrome, rechnet sie in Punkt um und fügt einen kleinen Sicherheitspuffer für den Reflow hinzu (etwa 14,4 Punkt), damit printToPDF nicht auf eine zweite Seite überläuft, die der Importer (er übernimmt nur Seite 0) abschneiden würde.

// 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 ist die einzige Konfigurationsoberfläche, und sie ist unveränderlich – erstellen Sie also eine neue Instanz, um einen Wert zu ändern. ChromeRenderResult::getPdfData() gibt die PDF-Bytes zurück. Die vollständige Optionsreferenz und die festen Chrome-Start-Flags finden Sie auf der unter „Siehe auch“ verlinkten Artisan-Konfigurationsseite.

Hängen Sie die Konfiguration an ein Dokument an, rendern Sie vertrauenswürdiges HTML und speichern Sie.

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 übernimmt das Flex-Layout; die Zahlen bleiben in der Ausgabe auswählbar, weil die Seite als Vektor-Form-XObject eingebettet wird und nicht als Rasterbild. Um auf eine feste A4-Seite zu passen, übergeben Sie Breite und Höhe in Punkt.

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

In der Produktion erstellen Sie einen Renderer pro Worker, injizieren einen PSR-3-Logger, fangen die beiden unterschiedlichen Exception-Typen getrennt ab und geben den Chrome-Prozess beim Herunterfahren deterministisch frei.

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

Der Renderer wird einmal erstellt und wiederverwendet. Der zugrunde liegende Browser-Pool hält einen Chrome-Prozess am Leben und startet ihn alle 100 Render-Vorgänge neu, um das Speicherwachstum zu begrenzen. Die beiden catch-Zweige trennen einen Deployment-Fehler (fehlende Runtime) von einem Renderzeitfehler (wiederholbar), und keiner der catch-Blöcke ist leer. Rufen Sie beim Herunterfahren des Workers shutdown() auf, um den Chrome-Prozess freizugeben, statt auf den Destruktor zu warten.

Erstellen Sie die Konfiguration aus einem Framework-Konfigurationsarray, um snake_case-Schlüssel zu erhalten, und fixieren Sie chromeBinaryPath in der Produktion auf eine deterministische Binary.

  • Leeres HTML ist ein No-op. writeHtmlChrome('') gibt das Dokument unverändert zurück.
  • Noch keine Seite. Hat das Dokument noch keine Seite, fügt writeHtmlChrome() vor dem Rendern eine hinzu.
  • Entfernte Assets laden nicht — das ist beabsichtigt. <img src="https://..."> rendert leer. Binden Sie jedes Asset als data:-URI inline ein. Das ist die Netzwerkisolationshaltung, kein Defekt.
  • Nur Seite 0 wird importiert. Die automatisch angepasste Höhe enthält den Reflow-Puffer, sodass eine einzige Seite entsteht. Bei einer expliziten Höhe wird kein Puffer hinzugefügt, und die Ausgabe entspricht exakt dem angeforderten Papierformat – bemessen Sie die Höhe also so, dass sie zu Ihrem Inhalt passt.
  • Brücke fehlt. Ist nextpdf/artisan nicht installiert, wirft Core eine Layout-Exception statt eines fatalen Fehlers. Fehlt die Bibliothek chrome-php/chrome, wirft die Brücke ChromeNotAvailableException mit dem Installationsbefehl.
  • defaultCss und </style>. Jede </style>-Sequenz in defaultCss wird vor der Injektion entfernt, um Style-Break-out zu verhindern. Berücksichtigen Sie das, wenn Sie CSS über Templates erzeugen.

Der erste Rendervorgang umfasst den Chrome-Start plus Layout. Spätere Rendervorgänge verwenden den laufenden Chrome-Prozess wieder, sodass die Startkosten nur selten anfallen. Erstellen Sie einen Renderer pro Worker und verwenden Sie ihn wieder. Erstellen Sie nicht für jede Anfrage einen eigenen. Rechnen Sie bei jedem 100. Render-Vorgang mit einer Latenzspitze, wenn die Brücke den Chrome-Prozess neu startet, um das Speicherwachstum zu begrenzen. Berücksichtigen Sie das in Ihren Latenz-Zielen, statt es als Incident zu behandeln. Kombinieren Sie renderTimeout auf jedem Pfad, der über nicht vertrauenswürdige Eingaben erreichbar ist, mit einem vorgelagerten Anfragebudget.

  • Die Netzwerkisolation ist die primäre Kontrolle. Die Brücke erlaubt überhaupt keinen ausgehenden Subressourcen-Abruf — CSP default-src 'none' plus eine Sperre jeder URL auf CDP-Transportebene. Sie implementiert keine Domain-Allowlist, weil sie keine benötigt. Binden Sie Assets als data:-URIs inline ein.
  • Die Eingabe wird begrenzt, bevor Chrome kontaktiert wird. Die Brücke weist HTML über maxHtmlSize (Standard 5 MB) zurück, ebenso eine zu große base64-Data-URI (ein Schutz gegen Dekompressions-Bomben) und jedes <meta http-equiv="refresh">-Tag (das eine Navigation zu einem internen Endpunkt auslösen könnte). Belassen Sie maxHtmlSize beim Standard, es sei denn, eine bekannte Last erfordert mehr. Eine Erhöhung vergrößert die Angriffsfläche für Ressourcenerschöpfung.
  • Die Chrome-Sandbox ist eine separate Kontrolle. Wenn Sie noSandbox: true setzen, startet Chrome mit --no-sandbox, was die Chrome-Prozessisolation entfernt — eine echte Verringerung der Eindämmung, kein kosmetisches Flag. Belassen Sie es außerhalb von Containern auf false. Kann die Container-Sandbox nicht initialisiert werden, führen Sie Chrome als Nicht-Root-Benutzer in einem eingeschränkten Container aus und behandeln Sie das Deployment als Anforderung mit höherem Vertrauen an die Eingabe.
  • Logs enthalten nur Metadaten. Injizieren Sie einen PSR-3-Logger. Die Brücke protokolliert Byte-Längen, Abmessungen und Lebenszyklus-Ereignisse, niemals HTML, PDF-Bytes oder extrahierten Text.
  • Geben Sie niemals einen Chrome-Remote-Debugging-Port frei. Die Brücke nutzt keinen, und ein offener CDP-Port ist ein nicht authentifizierter Steuerkanal.

Das vollständige Bedrohungsmodell — die SSRF-Abwehr, die explizit benannte Sandbox-Grenze und der Katalog der Fehlermodi — finden Sie auf der unter „Siehe auch“ verlinkten Artisan-Seite zu Sicherheit und Betrieb, die die relevanten OWASP-, CWE- und NIST-Klauseln festhält.

Diese Anleitung erhebt selbst keinen normativen Standardanspruch. Die Netzwerk-, Isolations- und Ressourcenerschöpfungskontrollen der Brücke sind auf der vorgelagerten Artisan-Seite zu Sicherheit und Betrieb OWASP ASVS, den CWE Top 25 (SSRF / unkontrollierter Ressourcenverbrauch) und NIST SP 800-53 SC-7 zugeordnet. Diese Cookbook-Seite gibt die Verwendung wieder und überlässt die normativen Zitate jener Seite. Die Brücke führt keine kryptografische Operation aus — Signieren und Verschlüsseln sind Aufgaben von Core oder der kommerziellen Edition und bleiben von Artisan unberührt.