Przejdź do głównej zawartości

Renderowanie HTML do PDF za pomocą renderera Chrome w Artisan

Mostek Artisan renderuje HTML w procesie Chrome działającym w trybie bezgłowym, a następnie importuje wynik do dokumentu NextPDF jako wektorowy obiekt Form XObject. Tekst pozostaje zaznaczalny i przeszukiwalny, zamiast zostać zrasteryzowany. Dołączasz obiekt ChromeRendererConfig, wywołujesz metodę writeHtmlChrome() na dokumencie albo używasz bezpośrednio klasy ChromeHtmlRenderer, a za układ odpowiada Chrome. Ten przewodnik omawia wywołanie renderowania, izolację sieciową, ustawianie rozmiaru strony, wysokość treści oraz długotrwały cykl życia renderera w procesie roboczym.

Na początek sprawdź wymagania wstępne:

  • Rdzeń NextPDF oraz pakiet nextpdf/artisan są zainstalowane.
  • Plik wykonywalny Chrome lub Chromium jest zainstalowany, a użytkownik procesu roboczego może uruchomić go w trybie bezgłowym. Zanim zaczniesz, sprawdź to poleceniem chromium --headless --dump-dom about:blank. Strona konfiguracji renderera Chrome, do której odnośnik znajduje się w sekcji Zobacz też, omawia udostępnianie pliku wykonywalnego oraz decyzję dotyczącą piaskownicy w kontenerze.

Ten poradnik zakłada, że możesz uruchomić proces Chrome w tym samym środowisku co aplikacja. Pierwszy uruchamialny przykład znajdziesz w przewodniku szybkiego startu Artisan.

Zainstaluj mostek obok rdzenia.

Okno terminala
composer require nextpdf/artisan

Zainstaluj wersję Chrome lub Chromium, którą może uruchomić użytkownik procesu roboczego. W systemach Debian lub Ubuntu użyj pakietu z dystrybucji.

Okno terminala
apt-get install -y chromium

Upewnij się, że plik wykonywalny uruchamia się w trybie bezgłowym jako użytkownik procesu roboczego.

Okno terminala
chromium --headless --dump-dom about:blank

Kod wyjścia 0 z pustym obiektowym modelem dokumentu (DOM) oznacza, że plik wykonywalny i jego biblioteki współdzielone są dostępne. Niezerowy kod wyjścia oznacza tę samą awarię, którą mostek zgłasza jako ChromeRenderException. Najpierw usuń problem na tym etapie.

writeHtmlChrome() to metoda klasy Document w rdzeniu NextPDF. Waliduje dane wejściowe, rozwiązuje renderer Artisan, wysyła HTML do Chrome za pośrednictwem protokołu Chrome DevTools Protocol (CDP), parsuje zwrócony PDF i osadza stronę 0 jako obiekt Form XObject w bieżącej pozycji kursora. Chrome działa jako proces potomny procesu roboczego PHP. Mostek steruje Chrome przez CDP zamiast łączyć się z osobnym procesem Chrome przez port debugowania, więc nie ma żadnego sieciowego punktu końcowego, który trzeba by wystawiać lub uwierzytelniać.

Mostek renderuje z domyślnie odmownym modelem sieci. Każde renderowanie korzysta z reguły Content-Security-Policy, która blokuje dostęp do wszystkich źródeł zasobów (default-src 'none') i zezwala wyłącznie na obrazy osadzone w treści (img-src data:). Mostek blokuje też każdy adres URL podzasobu w warstwie transportowej CDP za pomocą Network.setBlockedURLs(['*']). W rezultacie zdalny obraz, arkusz stylów, czcionka, skrypt lub iframe w kodzie HTML nie jest ładowany. Każdy zasób osadź w treści jako identyfikator URI data:. W ten sposób mostek przeciwdziała ryzyku fałszowania żądań po stronie serwera (SSRF) podczas renderowania kodu HTML, który może pochodzić z niezaufanego źródła. Mechanizm działa niezależnie od konfiguracji.

Model rozmiaru strony ma dwa tryby. Gdy podasz zarówno szerokość, jak i wysokość w punktach PDF, Chrome drukuje dokładnie do tego rozmiaru papieru. Gdy pominiesz wysokość albo ustawisz ją na null, mostek mierzy wysokość wyrenderowanej treści w Chrome, przelicza ją na punkty i dodaje niewielki bufor bezpieczeństwa na przepływ treści o wartości około 14,4 punktu. Dzięki temu wynik printToPDF nie przechodzi na drugą stronę, którą importer obsługujący tylko stronę 0 by przyciął.

// 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 to jedyna powierzchnia konfiguracji. Obiekt jest niemutowalny, więc aby zmienić wartość, utwórz nową instancję. ChromeRenderResult::getPdfData() zwraca bajty PDF. Strona konfiguracji Artisan, do której odnośnik znajduje się w sekcji Zobacz też, zawiera pełne zestawienie opcji oraz stałych flag uruchomieniowych Chrome.

Dołącz konfigurację do dokumentu, wyrenderuj zaufany kod HTML i zapisz wynik.

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 obsługuje układ flex, a liczby pozostają zaznaczalne w pliku wynikowym, ponieważ strona jest osadzona jako wektorowy obiekt Form XObject, a nie obraz rastrowy. Aby dopasować treść do stałej strony A4, podaj szerokość i wysokość w punktach.

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

W środowisku produkcyjnym utwórz jeden renderer na proces roboczy, wstrzyknij rejestrator zgodny z PSR-3, obsługuj osobno dwa odrębne typy wyjątków i zwalniaj proces Chrome w sposób deterministyczny podczas zamykania.

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

Utwórz renderer raz, a następnie używaj go ponownie. Bazowa pula przeglądarek utrzymuje jeden proces Chrome i restartuje go co 100 renderowań, aby ograniczyć wzrost zużycia pamięci. Dwa bloki catch oddzielają awarię wdrożenia, taką jak brakujące środowisko uruchomieniowe, od awarii w trakcie renderowania, którą można jednokrotnie ponowić. Żaden z bloków catch nie jest pusty. Wywołaj shutdown() przy zamykaniu procesu roboczego, aby zwolnić proces Chrome zamiast czekać na destruktor.

Zbuduj konfigurację z tablicy konfiguracyjnej frameworka, aby używać kluczy w stylu snake-case, i ustaw na stałe chromeBinaryPath w środowisku produkcyjnym, aby wybór pliku wykonywalnego był deterministyczny.

  • Pusty kod HTML nie wykonuje żadnej operacji. writeHtmlChrome('') zwraca dokument bez zmian.
  • Brak strony. Jeśli dokument nie ma żadnej strony, writeHtmlChrome() dodaje jedną przed renderowaniem.
  • Zdalne zasoby nie ładują się — celowo. <img src="https://..."> renderuje się jako pusty element. Każdy zasób osadź w treści jako identyfikator URI data:. To efekt modelu izolacji sieciowej, a nie usterka.
  • Importowana jest tylko strona 0. Automatyczne dopasowanie wysokości dodaje bufor na przepływ treści, dzięki czemu powstaje pojedyncza strona. Przy jawnie podanej wysokości żaden bufor nie jest dodawany, a wynik odpowiada dokładnie żądanemu rozmiarowi papieru, dlatego dobierz wysokość tak, aby pomieściła treść.
  • Brak mostka. Jeśli pakiet nextpdf/artisan nie jest zainstalowany, rdzeń zgłasza wyjątek układu, a nie błąd krytyczny. Jeśli brakuje biblioteki chrome-php/chrome, mostek zgłasza ChromeNotAvailableException wraz z poleceniem instalacji.
  • defaultCss a </style>. Każda sekwencja </style> w defaultCss jest usuwana przed wstrzyknięciem jako zabezpieczenie przed wyłamaniem się z kontekstu stylu. Uwzględnij to, jeśli tworzysz CSS z szablonów.

Pierwsze renderowanie wiąże się z kosztem uruchomienia Chrome i wyznaczenia układu. Kolejne renderowania wykorzystują ponownie działający proces Chrome, więc rzadko ponoszą koszt uruchomienia. Utwórz jeden renderer na proces roboczy i używaj go ponownie. Nie twórz go osobno dla każdego żądania. Spodziewaj się skoku opóźnienia przy co setnym renderowaniu, gdy mostek restartuje proces Chrome, aby ograniczyć zużycie pamięci. Uwzględnij to w swoich celach dotyczących opóźnień, zamiast traktować jako incydent. Połącz renderTimeout z budżetem żądania po stronie nadrzędnej w każdej ścieżce dostępnej dla niezaufanych danych wejściowych.

  • Izolacja sieciowa jest podstawowym mechanizmem kontroli. Mostek w ogóle nie zezwala na żadne wychodzące pobieranie podzasobów: CSP default-src 'none' oraz blokada każdego adresu URL na poziomie transportu CDP. Nie wdraża listy dozwolonych domen, ponieważ jej nie potrzebuje. Zasoby osadzaj w treści jako identyfikatory URI data:.
  • Dane wejściowe są ograniczane, zanim nastąpi kontakt z Chrome. Mostek odrzuca kod HTML przekraczający maxHtmlSize (domyślnie 5 MB), zbyt duży identyfikator URI typu data w formacie base64 (zabezpieczenie przed bombą dekompresyjną) oraz każdy znacznik <meta http-equiv="refresh"> (który mógłby wymusić nawigację do wewnętrznego punktu końcowego). Pozostaw maxHtmlSize na wartości domyślnej, chyba że znane obciążenie wymaga więcej. Jego zwiększenie poszerza powierzchnię ataku polegającego na wyczerpaniu zasobów.
  • Piaskownica Chrome jest osobnym mechanizmem kontroli. Ustawienie noSandbox: true uruchamia Chrome z flagą --no-sandbox, co usuwa izolację procesu Chrome. To rzeczywiste obniżenie poziomu izolacji, a nie kosmetyczna flaga. Poza kontenerami pozostaw ją na wartości false. Gdy piaskownica kontenera nie może się zainicjować, uruchom Chrome jako użytkownik bez uprawnień roota w ograniczonym kontenerze i traktuj takie wdrożenie jako wymagające wyższego poziomu zaufania do danych wejściowych.
  • Dzienniki zawierają wyłącznie metadane. Wstrzyknij rejestrator zgodny z PSR-3. Mostek rejestruje długości w bajtach, wymiary i zdarzenia cyklu życia, ale nigdy kodu HTML, bajtów PDF ani wyodrębnionego tekstu.
  • Nigdy nie udostępniaj portu zdalnego debugowania Chrome. Mostek go nie używa, a otwarty port CDP to nieuwierzytelniony kanał sterujący.

Pełny model zagrożeń, obejmujący obronę przed SSRF, jawnie wyznaczoną granicę piaskownicy oraz katalog trybów awarii, znajduje się na stronie Artisan dotyczącej bezpieczeństwa i operacji, do której odnośnik znajduje się w sekcji Zobacz też. Ta strona mapuje odpowiednie klauzule OWASP, CWE i NIST.

Ten przewodnik nie formułuje własnych deklaracji zgodności z normatywnymi standardami. Nadrzędna strona Artisan dotycząca bezpieczeństwa i operacji odwzorowuje mechanizmy kontroli sieci, izolacji i wyczerpania zasobów stosowane przez mostek na OWASP ASVS, CWE Top 25 (SSRF / niekontrolowane zużycie zasobów) oraz NIST SP 800-53 SC-7. Ta strona poradnika ponownie przedstawia sposób użycia, a normatywne cytaty pozostawia tamtej stronie. Mostek nie wykonuje żadnej operacji kryptograficznej; podpisywanie i szyfrowanie należą do rdzenia lub edycji komercyjnej i nie zależą od Artisan.