Salta ai contenuti

Generare PDF da HTML con il renderer Chrome di Artisan

Il bridge Artisan esegue il rendering di HTML tramite un processo Chrome headless e importa il risultato in un documento NextPDF come Form XObject vettoriale. Il testo resta selezionabile e ricercabile invece di essere rasterizzato. Associare un ChromeRendererConfig, chiamare writeHtmlChrome() su un documento (oppure usare direttamente ChromeHtmlRenderer) e lasciare che Chrome esegua l’impaginazione. Questa guida descrive la chiamata di rendering, i criteri di isolamento di rete, il modello di dimensionamento della pagina e dell’altezza del contenuto e il ciclo di vita prolungato del renderer per un worker.

Prerequisiti immediati:

  • NextPDF core e nextpdf/artisan sono installati.
  • È installato un binario Chrome o Chromium e l’utente del worker può eseguirlo in modalità headless. Verificare con chromium --headless --dump-dom about:blank prima di iniziare. Il provisioning del binario e la decisione sulla sandbox del container sono trattati nella pagina di configurazione del renderer Chrome indicata in Vedere anche.

Questa guida ha un taglio pratico. Presuppone che sia possibile eseguire un processo Chrome vicino all’applicazione. Per un primo esempio eseguibile, consultare la guida introduttiva di Artisan.

Installare il bridge insieme al core.

Terminal window
composer require nextpdf/artisan

Installare una build di Chrome o Chromium eseguibile dall’utente worker. Su Debian o Ubuntu, usare il pacchetto della distribuzione.

Terminal window
apt-get install -y chromium

Verificare che il binario possa essere eseguito in modalità headless con l’utente del worker.

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

Un codice di uscita 0 con un DOM vuoto indica che il binario e le relative librerie condivise sono disponibili. Un codice diverso da zero corrisponde allo stesso errore che il bridge espone come ChromeRenderException. Correggerlo prima a questo livello.

writeHtmlChrome() è un metodo del Document di NextPDF core. Convalida l’input, risolve il renderer Artisan, invia l’HTML a Chrome tramite il Chrome DevTools Protocol (CDP), analizza il PDF restituito e incorpora la pagina 0 come Form XObject nella posizione del cursore corrente. Chrome viene eseguito come processo figlio del worker PHP. Il bridge lo pilota tramite CDP invece di connettersi a un’istanza Chrome separata in esecuzione su una porta di debug, quindi non esiste alcun endpoint di rete da esporre o da autenticare.

Il bridge esegue il rendering con un assetto di rete che nega tutto per impostazione predefinita. Ogni rendering è racchiuso in una Content-Security-Policy che nega tutte le origini delle risorse (default-src 'none') e consente solo le immagini inline (img-src data:). Il bridge blocca inoltre ogni URL di sottorisorsa a livello di trasporto CDP con Network.setBlockedURLs(['*']). Di conseguenza, un’immagine remota, un foglio di stile, un font, uno script o un iframe presenti nell’HTML non vengono caricati. Incorporare ogni asset come URI data:. Questa è la risposta del bridge al rischio di server-side request forgery (SSRF) durante il rendering di HTML potenzialmente non attendibile e resta valida indipendentemente dalla configurazione.

Il modello di dimensionamento della pagina prevede due modalità. Quando si forniscono sia la larghezza sia l’altezza (in punti PDF), Chrome stampa esattamente con quel formato di carta. Quando l’altezza viene omessa o è null, il bridge misura l’altezza del contenuto renderizzato in Chrome, la converte in punti e aggiunge un piccolo margine di sicurezza per il riflusso (circa 14,4 punti), così che printToPDF non sfori su una seconda pagina che l’importatore della sola pagina 0 ritaglierebbe.

// 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 è l’unico punto di configurazione ed è immutabile, quindi per modificare un valore occorre creare una nuova istanza. ChromeRenderResult::getPdfData() restituisce i byte del PDF. Il riferimento completo delle opzioni e i flag fissi di avvio di Chrome si trovano nella pagina di configurazione di Artisan indicata in Vedere anche.

Associare la configurazione a un documento, eseguire il rendering di HTML attendibile e salvare il risultato.

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 gestisce il layout flex e i numeri rimangono selezionabili nell’output perché la pagina è incorporata come Form XObject vettoriale, non come immagine raster. Per adattare l’output a una pagina A4 fissa, passare larghezza e altezza in punti.

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

In produzione, creare un renderer per ogni worker, iniettare un logger PSR-3, intercettare separatamente i due tipi distinti di eccezione e rilasciare il processo Chrome in modo deterministico durante lo spegnimento.

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

Il renderer viene creato una sola volta e riutilizzato. Il pool di browser sottostante mantiene attivo un processo Chrome e lo riavvia ogni 100 rendering per limitare la crescita della memoria. I due rami catch distinguono un errore di deployment (runtime mancante) da un errore in fase di rendering (da ritentare) e nessuno dei due blocchi catch è vuoto. Chiamare shutdown() allo spegnimento del worker per rilasciare il processo Chrome anziché attendere il distruttore.

Costruire la configurazione a partire da un array di configurazione del framework per ottenere chiavi in formato snake-case e impostare chromeBinaryPath in produzione, così da usare un binario deterministico.

  • L’HTML vuoto è un’operazione nulla. writeHtmlChrome('') restituisce il documento invariato.
  • Nessuna pagina presente. Se il documento non contiene alcuna pagina, writeHtmlChrome() ne aggiunge una prima del rendering.
  • Gli asset remoti non vengono caricati — per scelta progettuale. <img src="https://..."> viene visualizzato vuoto. Incorporare ogni asset come URI data:. È l’assetto di isolamento di rete, non un difetto.
  • Viene importata solo la pagina 0. L’altezza adattata automaticamente aggiunge il margine di riflusso in modo da produrre una singola pagina. Con un’altezza esplicita non viene aggiunto alcun margine e l’output corrisponde esattamente al formato di carta richiesto, quindi dimensionare l’altezza in base al contenuto.
  • Bridge mancante. Se nextpdf/artisan non è installato, core solleva un’eccezione di layout anziché un errore fatale. Se la libreria chrome-php/chrome è assente, il bridge solleva ChromeNotAvailableException con il comando di installazione.
  • defaultCss e </style>. Qualsiasi sequenza </style> in defaultCss viene rimossa prima dell’iniezione come difesa contro l’uscita dal contesto di stile. Tenerne conto quando il CSS viene generato tramite template.

Il primo rendering comporta sia l’avvio di Chrome sia l’impaginazione. I rendering successivi riutilizzano il processo Chrome attivo, quindi il costo di avvio viene sostenuto raramente. Creare un renderer per ogni worker e riutilizzarlo. Non crearne uno per ogni richiesta. Prevedere un picco di latenza ogni 100 rendering, quando il bridge riavvia il processo Chrome per limitare la memoria. Includerlo nei propri obiettivi di latenza anziché trattarlo come un incidente. Abbinare renderTimeout a un budget della richiesta a monte su qualsiasi percorso raggiungibile da input non attendibile.

  • L’isolamento di rete è il controllo principale. Il bridge non consente alcun recupero di sottorisorse in uscita — CSP default-src 'none' più un blocco a livello di trasporto CDP di ogni URL. Non implementa un elenco di domini consentiti perché non ne ha bisogno. Incorporare gli asset come URI data:.
  • L’input viene limitato prima di contattare Chrome. Il bridge rifiuta l’HTML che supera maxHtmlSize (5 MB per impostazione predefinita), un URI data base64 di dimensioni eccessive (una protezione contro le bombe di decompressione) e qualsiasi tag <meta http-equiv="refresh"> (che potrebbe indurre una navigazione verso un endpoint interno). Mantenere maxHtmlSize al valore predefinito, a meno che un carico di lavoro noto non richieda di più. Aumentarlo amplia la superficie di esaurimento delle risorse.
  • La sandbox di Chrome è un controllo distinto. Impostare noSandbox: true avvia Chrome con --no-sandbox, che rimuove l’isolamento dei processi di Chrome — una riduzione reale del contenimento, non un flag puramente formale. Lasciarlo a false al di fuori dei container. Quando la sandbox del container non può inizializzarsi, eseguire Chrome come utente non root in un container vincolato e trattare il deployment come un requisito di maggiore attendibilità sull’input.
  • I log contengono solo metadati. Iniettare un logger PSR-3. Il bridge registra lunghezze in byte, dimensioni ed eventi del ciclo di vita, mai HTML, byte del PDF o testo estratto.
  • Non esporre mai una porta di debug remoto di Chrome. Il bridge non ne usa alcuna e una porta CDP aperta è un canale di controllo non autenticato.

Il modello di minaccia completo — la difesa SSRF, il confine esplicito della sandbox e il catalogo delle modalità di errore — si trova nella pagina di sicurezza e operazioni di Artisan indicata in Vedere anche, dove sono fissate le clausole OWASP, CWE e NIST pertinenti.

Questa guida non introduce affermazioni normative proprie. I controlli di rete, isolamento ed esaurimento delle risorse del bridge sono mappati su OWASP ASVS, sulla CWE Top 25 (SSRF / consumo incontrollato di risorse) e su NIST SP 800-53 SC-7 nella pagina di sicurezza e operazioni di Artisan a monte. Questa pagina del cookbook ribadisce l’utilizzo e rimanda a tale pagina per le relative citazioni normative. Il bridge non esegue alcuna operazione crittografica — firma e cifratura sono responsabilità del core o dell’edizione commerciale e non sono influenzate da Artisan.