Перейти к содержимому

Отрисовка HTML в PDF через отрисовщик Chrome в Artisan

Мост Artisan отрисовывает HTML в процессе headless Chrome, а затем импортирует результат в документ NextPDF как векторный Form XObject. Текст не растрируется: его по-прежнему можно выделять и искать. Вы подключаете ChromeRendererConfig, вызываете writeHtmlChrome() на документе или используете ChromeHtmlRenderer напрямую, а за макет отвечает Chrome. В этом руководстве рассматриваются вызов отрисовки, сетевая изоляция, задание размера страницы, высота содержимого и жизненный цикл долгоживущего отрисовщика в обработчике.

Предварительные требования:

  • Ядро NextPDF и nextpdf/artisan установлены.
  • Установлен двоичный файл Chrome или Chromium, и пользователь-обработчик может запускать его в режиме headless. Перед началом работы проверьте это командой chromium --headless --dump-dom about:blank. Страница настройки отрисовщика Chrome, ссылка на которую приведена в разделе “См. также”, описывает предоставление двоичного файла и выбор настроек песочницы контейнера.

В этом руководстве предполагается, что вы можете запускать процесс Chrome рядом с приложением. Первый рабочий пример см. в кратком руководстве по Artisan.

Установите мост вместе с ядром.

Окно терминала
composer require nextpdf/artisan

Установите сборку Chrome или Chromium, которую может запускать пользователь-обработчик. В Debian или Ubuntu используйте пакет из дистрибутива.

Окно терминала
apt-get install -y chromium

Убедитесь, что двоичный файл запускается в режиме headless от имени пользователя-обработчика.

Окно терминала
chromium --headless --dump-dom about:blank

Код возврата 0 с пустой объектной моделью документа (DOM) означает, что двоичный файл и его разделяемые библиотеки доступны. Ненулевой код возврата — тот же сбой, который мост сообщает как ChromeRenderException. Сначала устраните его на этом уровне.

writeHtmlChrome() — это метод Document в ядре NextPDF. Он проверяет входные данные, разрешает отрисовщик Artisan, отправляет HTML в Chrome по протоколу Chrome DevTools Protocol (CDP), разбирает возвращённый PDF и встраивает страницу 0 как Form XObject в текущей позиции курсора. Chrome работает как дочерний процесс обработчика PHP. Мост управляет Chrome по CDP, а не подключается к отдельному процессу Chrome через порт отладки, поэтому нет сетевой конечной точки, которую нужно открывать или аутентифицировать.

Мост выполняет отрисовку с сетевой политикой “запрет по умолчанию”. Каждая отрисовка использует Content-Security-Policy, который запрещает все источники ресурсов (default-src 'none') и разрешает только встроенные изображения (img-src data:). Кроме того, мост блокирует каждый URL подресурса на транспортном уровне CDP с помощью Network.setBlockedURLs(['*']). В результате удалённое изображение, таблица стилей, шрифт, скрипт или iframe в вашем HTML не загружаются. Встраивайте каждый ресурс как URI вида data:. Так мост устраняет риск подделки запроса со стороны сервера (SSRF) при отрисовке потенциально недоверенного HTML, и это применяется независимо от конфигурации.

Модель размера страницы работает в двух режимах. Если заданы и ширина, и высота в пунктах PDF, Chrome печатает строго на такой размер бумаги. Если высота не указана или равна null, мост измеряет высоту отрисованного содержимого в Chrome, преобразует её в пункты и добавляет небольшой защитный буфер на переформатирование около 14,4 пункта. Так printToPDF не уходит на вторую страницу, которую импортёр, работающий только со страницей 0, всё равно обрезал бы.

// 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 — единственная поверхность конфигурации. Он неизменяем, поэтому для изменения значения создайте новый экземпляр. ChromeRenderResult::getPdfData() возвращает байты PDF. Страница конфигурации Artisan, ссылка на которую приведена в разделе “См. также”, содержит полный справочник параметров и фиксированные флаги запуска Chrome.

Подключите конфигурацию к документу, отрисуйте доверенный HTML и сохраните результат.

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 обрабатывает flex-макет, а числа в результате остаются выделяемыми, потому что страница встраивается как векторный Form XObject, а не как растровое изображение. Чтобы уместить вывод на фиксированную страницу A4, передайте ширину и высоту в пунктах.

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

В продакшене создавайте по одному отрисовщику на обработчик, внедряйте логгер PSR-3, перехватывайте два разных типа исключений отдельно и детерминированно освобождайте процесс Chrome при завершении работы.

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

Создайте отрисовщик один раз и используйте его повторно. Базовый пул браузеров держит активным один процесс Chrome и перезапускает его каждые 100 отрисовок, чтобы ограничить рост потребления памяти. Две ветви catch разделяют сбой развёртывания, например отсутствие среды выполнения, и сбой во время отрисовки, который можно повторить один раз. Ни один блок catch не пустой. Вызывайте shutdown() при завершении работы обработчика, чтобы освободить процесс Chrome, не дожидаясь деструктора.

Создавайте конфигурацию из массива конфигурации фреймворка, чтобы использовать ключи в snake-case, и закрепляйте chromeBinaryPath в продакшене, чтобы двоичный файл выбирался детерминированно.

  • Пустой HTML не выполняет никаких действий. writeHtmlChrome('') возвращает документ без изменений.
  • Страницы ещё нет. Если в документе нет страницы, writeHtmlChrome() добавляет её перед отрисовкой.
  • Удалённые ресурсы не загружаются — так задумано. <img src="https://..."> отрисовывается пустым. Встраивайте каждый ресурс как URI вида data:. Это политика сетевой изоляции, а не дефект.
  • Импортируется только страница 0. Автоподбор высоты добавляет буфер на переформатирование, поэтому формируется одна страница. При явно заданной высоте буфер не добавляется, и результат точно соответствует запрошенному размеру бумаги, поэтому подбирайте высоту под своё содержимое.
  • Мост отсутствует. Если nextpdf/artisan не установлен, ядро выбрасывает исключение макета, а не фатальную ошибку. Если библиотека chrome-php/chrome отсутствует, мост выбрасывает ChromeNotAvailableException с командой установки.
  • defaultCss и </style>. Любая последовательность </style> в defaultCss удаляется перед внедрением как защита от выхода за пределы блока стилей. Учитывайте это в шаблонах, если вы шаблонизируете CSS.

Первая отрисовка включает стоимость запуска Chrome и построения макета. Последующие отрисовки повторно используют активный процесс Chrome, поэтому обычно не платят за запуск. Создавайте по одному отрисовщику на обработчик и используйте его повторно. Не создавайте отдельный отрисовщик на каждый запрос. Ожидайте всплеск задержки на каждой 100-й отрисовке, когда мост перезапускает процесс Chrome, чтобы ограничить память. Учитывайте это в целевых показателях задержки, а не воспринимайте как инцидент. Согласуйте renderTimeout с вышестоящим бюджетом запроса на любом пути, достижимом для недоверенного ввода.

  • Сетевая изоляция — основная мера защиты. Мост вообще не разрешает исходящую загрузку подресурсов: CSP default-src 'none' плюс блокировка каждого URL на транспортном уровне CDP. Он не реализует список разрешённых доменов, потому что в нём нет необходимости. Встраивайте ресурсы как URI вида data:.
  • Ввод ограничивается до обращения к Chrome. Мост отклоняет HTML сверх maxHtmlSize (по умолчанию 5 МБ), чрезмерно большой data-URI в base64 (защита от бомбы распаковки) и любой тег <meta http-equiv="refresh"> (который мог бы инициировать переход на внутреннюю конечную точку). Оставляйте maxHtmlSize по умолчанию, если только известная нагрузка не требует большего значения. Его увеличение расширяет поверхность исчерпания ресурсов.
  • Песочница Chrome — отдельная мера защиты. Установка noSandbox: true запускает Chrome с --no-sandbox, что отключает изоляцию процессов Chrome. Это реальное ослабление изоляции, а не косметический флаг. За пределами контейнеров оставляйте его равным false. Когда песочница контейнера не может инициализироваться, запускайте Chrome от имени пользователя без прав root в ограниченном контейнере и относитесь к такому развёртыванию как к режиму, требующему большего доверия к вводу.
  • Журналы содержат только метаданные. Внедрите логгер PSR-3. Мост записывает в журнал длины в байтах, размеры и события жизненного цикла, но никогда не записывает HTML, байты PDF или извлечённый текст.
  • Никогда не открывайте порт удалённой отладки Chrome. Мост его не использует, а открытый порт CDP — неаутентифицированный канал управления.

Полная модель угроз, включая защиту от SSRF, явную границу песочницы и каталог режимов отказа, находится на странице безопасности и эксплуатации Artisan, ссылка на которую приведена в разделе “См. также”. На этой странице закреплены соответствующие пункты OWASP, CWE и NIST.

Это руководство не выдвигает собственных нормативных заявлений о соответствии стандартам. Вышестоящая страница безопасности и эксплуатации Artisan сопоставляет меры защиты моста в области сети, изоляции и исчерпания ресурсов с OWASP ASVS, CWE Top 25 (SSRF / неконтролируемое потребление ресурсов) и NIST SP 800-53 SC-7. Эта страница сборника рецептов повторно излагает порядок использования и оставляет нормативные ссылки на той странице. Мост не выполняет криптографических операций; подпись и шифрование относятся к ядру или к коммерческой редакции и не затрагиваются Artisan.