Pular para o conteúdo

Renderize HTML em PDF com o renderizador Chrome do Artisan

A ponte do Artisan renderiza HTML com um processo Chrome headless e, em seguida, importa o resultado para um documento NextPDF como um Form XObject vetorial. O texto permanece selecionável e pesquisável em vez de ser rasterizado. Você anexa um ChromeRendererConfig, chama writeHtmlChrome() em um documento ou usa o ChromeHtmlRenderer diretamente, deixando o layout a cargo do Chrome. Este guia cobre a chamada de renderização, o isolamento de rede, o dimensionamento de página, a altura do conteúdo e o ciclo de vida de longa duração do renderizador em um worker.

Antes de começar, confira os pré-requisitos:

  • O NextPDF core e o nextpdf/artisan estão instalados.
  • Um binário do Chrome ou do Chromium está instalado, e o usuário do worker consegue executá-lo em modo headless. Verifique isso com chromium --headless --dump-dom about:blank antes de começar. A página de configuração do renderizador Chrome, vinculada em Veja também, cobre o provisionamento do binário e a decisão sobre o sandbox do container.

Este guia prático pressupõe que você consegue executar um processo Chrome próximo à aplicação. Para ver o primeiro exemplo executável, leia o quickstart do Artisan.

Instale a ponte junto com o core.

Terminal window
composer require nextpdf/artisan

Instale uma build do Chrome ou do Chromium que o usuário do worker possa executar. No Debian ou Ubuntu, use o pacote da distribuição.

Terminal window
apt-get install -y chromium

Confirme que o binário é executado em modo headless como o usuário do worker.

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

Um código de saída 0 com um Document Object Model (DOM) vazio significa que o binário e suas bibliotecas compartilhadas estão presentes. Uma saída diferente de zero é a mesma falha que a ponte expõe como uma ChromeRenderException. Resolva essa parte primeiro.

writeHtmlChrome() é um método do Document do NextPDF core. Ele valida a entrada, resolve o renderizador do Artisan, envia o HTML ao Chrome pelo Chrome DevTools Protocol (CDP), analisa o PDF retornado e incorpora a página 0 como um Form XObject na posição atual do cursor. O Chrome roda como um processo filho do worker PHP. A ponte controla o Chrome pelo CDP em vez de se conectar a um processo Chrome separado por uma porta de depuração, portanto não há endpoint de rede para expor ou autenticar.

A ponte renderiza com uma postura de rede que nega por padrão. Cada renderização usa uma Content-Security-Policy que nega todas as origens de recursos (default-src 'none') e permite apenas imagens embutidas (img-src data:). A ponte também bloqueia toda URL de subrecurso na camada de transporte do CDP com Network.setBlockedURLs(['*']). Como resultado, uma imagem, folha de estilo, fonte, script ou iframe remoto no HTML não carrega. Embuta cada recurso como uma URI data:. É assim que a ponte trata o risco de server-side request forgery (SSRF) ao renderizar HTML potencialmente não confiável, e isso se aplica independentemente da configuração.

O modelo de tamanho de página tem dois modos. Quando você fornece largura e altura, em pontos PDF, o Chrome imprime exatamente nesse tamanho de papel. Quando a altura é omitida ou null, a ponte mede a altura do conteúdo renderizado no Chrome, converte-a em pontos e adiciona uma pequena margem de segurança para refluxo de cerca de 14,4 pontos. Isso evita que printToPDF transborde para uma segunda página que o importador, que processa apenas a página 0, recortaria.

// 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 é a única superfície de configuração. Ele é imutável, então crie uma nova instância para alterar um valor. ChromeRenderResult::getPdfData() retorna os bytes do PDF. A página de configuração do Artisan, vinculada em Veja também, lista a referência completa de opções e as flags fixas de inicialização do Chrome.

Anexe a configuração a um documento, renderize HTML confiável e salve o resultado.

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');

O Chrome cuida do layout flex, e os números permanecem selecionáveis na saída porque a página é incorporada como um Form XObject vetorial, não como uma imagem rasterizada. Para encaixar em uma página A4 fixa, passe a largura e a altura em pontos.

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

Em produção, construa um renderizador por worker, injete um logger PSR-3, trate os dois tipos distintos de exceção separadamente e libere o processo Chrome de forma determinística no encerramento.

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

Construa o renderizador uma vez e depois reutilize-o. O pool de navegadores subjacente mantém um processo Chrome ativo e o reinicia a cada 100 renderizações para limitar o crescimento de memória. Os dois blocos catch separam uma falha de implantação, como um runtime ausente, de uma falha no momento da renderização que você pode tentar novamente uma vez. Nenhum dos blocos catch está vazio. Chame shutdown() quando o worker encerrar para liberar o processo Chrome em vez de depender do destrutor.

Construa a configuração a partir de um array de configuração do framework para usar chaves em snake-case e fixe chromeBinaryPath em produção para que o binário seja determinístico.

  • HTML vazio é uma operação nula. writeHtmlChrome('') retorna o documento inalterado.
  • Ainda não há página. Se o documento não tiver página, writeHtmlChrome() adiciona uma antes de renderizar.
  • Recursos remotos não carregam — por design. <img src="https://..."> renderiza vazio. Embuta cada recurso como uma URI data:. Esta é a postura de isolamento de rede, não um defeito.
  • Apenas a página 0 é importada. A altura com ajuste automático adiciona a margem de refluxo para que uma única página seja produzida. Com uma altura explícita, nenhuma margem é adicionada e a saída corresponde exatamente ao tamanho de papel solicitado, então dimensione a altura para acomodar o conteúdo.
  • Ponte ausente. Se nextpdf/artisan não estiver instalado, o core lança uma exceção de layout em vez de um erro fatal. Se a biblioteca chrome-php/chrome estiver ausente, a ponte lança ChromeNotAvailableException com o comando de instalação.
  • defaultCss e </style>. Qualquer sequência </style> em defaultCss é removida antes da injeção como uma defesa contra style-breakout. Planeje levando isso em conta se você usa CSS via template.

A primeira renderização arca com a inicialização do Chrome e o layout. As renderizações seguintes reutilizam o processo Chrome ativo, então raramente arcam com o custo de inicialização. Construa um renderizador por worker e reutilize-o. Não crie um por requisição. Espere um pico de latência a cada 100ª renderização, quando a ponte reinicia o processo Chrome para limitar a memória. Leve isso em conta nos objetivos de latência em vez de tratá-lo como um incidente. Combine renderTimeout com um orçamento de requisição upstream em qualquer caminho alcançável por entrada não confiável.

  • O isolamento de rede é o controle primário. A ponte não permite nenhuma busca de subrecurso de saída: CSP default-src 'none' mais um bloqueio de toda URL no nível de transporte do CDP. Ela não implementa uma allowlist de domínios porque não precisa de nenhuma. Embuta os recursos como URIs data:.
  • A entrada é limitada antes de o Chrome ser contatado. A ponte rejeita HTML acima de maxHtmlSize (padrão 5 MB), uma URI de dados base64 grande demais (uma proteção contra bomba de descompressão) e qualquer tag <meta http-equiv="refresh"> (que poderia direcionar uma navegação a um endpoint interno). Mantenha maxHtmlSize no padrão, a menos que uma carga de trabalho conhecida exija mais. Aumentá-lo amplia a superfície de esgotamento de recursos.
  • O sandbox do Chrome é um controle separado. Definir noSandbox: true inicia o Chrome com --no-sandbox, o que remove o isolamento de processo do Chrome. Isso é uma redução real na contenção, não uma flag cosmética. Deixe-a como false fora de containers. Quando o sandbox do container não consegue inicializar, execute o Chrome como um usuário não root em um container restrito e trate a implantação como algo que exige maior confiança na entrada.
  • Os logs contêm apenas metadados. Injete um logger PSR-3. A ponte registra comprimentos em bytes, dimensões e eventos de ciclo de vida, nunca HTML, bytes de PDF ou texto extraído.
  • Nunca exponha uma porta de depuração remota do Chrome. A ponte não usa nenhuma, e uma porta CDP aberta é um canal de controle sem autenticação.

O modelo de ameaças completo, incluindo a defesa contra SSRF, a fronteira explícita do sandbox e o catálogo de modos de falha, está na página de segurança e operações do Artisan vinculada em Veja também. Essa página registra as cláusulas relevantes de OWASP, CWE e NIST.

Este guia não faz nenhuma afirmação normativa de padrões por conta própria. A página upstream de segurança e operações do Artisan mapeia os controles de rede, isolamento e esgotamento de recursos da ponte para o OWASP ASVS, o CWE Top 25 (SSRF / consumo descontrolado de recursos) e o NIST SP 800-53 SC-7. Esta página do cookbook reapresenta o uso e remete essas citações normativas para aquela página. A ponte não realiza nenhuma operação criptográfica; assinatura e criptografia são responsabilidades do core ou da edição comercial e não são afetadas pelo Artisan.