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

Конвейер отрисовки HTML

Когда вы вызываете writeHtml(), запускается один прямой проход по HyperText Markup Language (HTML): входные данные токенизируются, разрешаются @page и стили, формируется макет содержимого и отрисовываются операторы Portable Document Format (PDF). Между этапами дерево элементов не сохраняется.

Окно терминала
composer require nextpdf/core:^3

Конвейер отрисовки HTML за один прямой проход преобразует HTML+CSS, то есть HTML вместе с Cascading Style Sheets (CSS), в операторы потока содержимого PDF. Он не строит сохраняемое дерево документа. Описанные ниже этапы соответствуют HtmlParser::parse() в ветке main.

Этап 1 — очистка и нормализация. HtmlParser::parse() отклоняет входные данные размером больше 10 МБ, удаляет управляющие символы и нормализует переводы строк: и CRLF, и одиночный CR становятся LF, как при нормализации переводов строк HTML в исходных данных. Затем он сбрасывает все поля экземпляра, чтобы состояние предыдущего вызова не попало в текущую обработку.

Этап 2 — извлечение @page и блоков стилей. Сначала парсер извлекает блоки <style>, затем применяет найденные правила @page, чтобы перенастроить геометрию страницы. Это происходит до обработки любого токена, потому что размер страницы влияет на каждое последующее решение по макету.

Этап 3 — токенизация. HtmlTokenizer::cleanHtml() нормализует пробельные символы, сохраняя содержимое <pre>. Затем tokenize() создаёт плоский list<HtmlToken>. Это именно список токенов, а не граф узлов. Конвейер сразу отбрасывает текстовые токены, состоящие только из пробельных символов. HtmlChildScanner::scan() строит по плоскому списку карты индексов (количество дочерних элементов, количество тегов, пустота), поэтому структурным селекторам дерево не требуется.

Этап 4 — необязательное предварительное сканирование :has(). Если вы включаете экспериментальную возможность css.has, CssResolver::resolveHasSelectors() выполняет одно ограниченное предварительное сканирование списка токенов, чтобы разрешить реляционный селектор. Этот документированный и ограниченный шаг — исключение из правила одного прохода.

Этап 5 — обработка токенов (стили, макет, отрисовка). HtmlParser::processTokens() один раз проходит по списку токенов. Для каждого элемента он разрешает каскад (применители уровня 1 записывают HtmlStyleState), вычисляет геометрию (макет уровня 3) и выдаёт операторы PDF (отрисовка уровня 4). Наследование стилей использует стек HtmlStyleState с операциями push и pop. Курсор (x, y, поля, смещение в потоке) передаётся между обработчиками через снимки HtmlBlockCursor.

Этап 6 — возврат результата. parse() возвращает неизменяемый HtmlRenderResult со сформированным потоком содержимого, конечным положением курсора и использованными ключами шрифтов. Вызывающая сторона (writeHtml()) передаёт курсор обратно в систему координат страницы.

О разделении на четыре уровня внутри этапа 5 см. на странице контракты уровней. О том, что сохраняемого дерева нет, и об ограничениях этого свойства см. на странице ограничения потоковой обработки.

СимволРасположениеЭтап
Document::writeHtml(string $html): staticsrc/Core/Concerns/HasTextOutput.phpПубличная точка входа
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpУправляет всеми этапами
HtmlTokenizer::cleanHtml() / tokenize()src/Html/HtmlTokenizer.phpЭтап 3
HtmlChildScanner::scan()src/Html/HtmlChildScanner.phpКарты индексов этапа 3
CssResolver::resolveHasSelectors()src/Html/CssResolver.phpЭтап 4 (по флагу)
HtmlRenderResult (stream, endX, endY, usedFontKeys)src/Html/HtmlRenderResult.phpЭтап 6

Источник — examples/08-html-basic.php.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('HTML Basic');
$doc->addPage();
$doc->writeHtml('<h1 style="color:#1E3A8A;">HTML Rendering</h1><p>One pass.</p>');
$doc->save(__DIR__ . '/output/08-html-basic.pdf');

Отрисовка стилизованного отчёта со встроенным блоком <style>. Конвейер извлекает этот блок и применяет его до обработки любого токена.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\HtmlParsingException;
function renderInvoice(string $bodyHtml, string $out): void
{
$doc = Document::createStandalone();
$doc->setTitle('Invoice');
$doc->addPage();
$html = '<style>@page { margin: 20mm; } '
. 'h1 { color: #1E3A8A; } '
. 'table { width: 100%; }</style>'
. $bodyHtml;
try {
$doc->writeHtml($html);
} catch (HtmlParsingException $e) {
// Sanitize/cap failures surface here. Do not retry.
throw $e;
}
$doc->save($out);
}
  • @page читается до токенов. Правило @page, расположенное после содержимого, всё равно применяется, потому что извлечение стилей предшествует токенизации. Геометрия страницы фиксируется до этапа 5.
  • Пробельные символы в <pre> сохраняются. cleanHtml() защищает содержимое <pre>; в остальных местах конвейер сворачивает пробельные символы.
  • :has() работает по флагу. Если вы не включили экспериментальную возможность css.has, этап 4 не выполняется и селекторы :has() не срабатывают.
  • Один буфер потока. Конвейер пишет в один строковый буфер и никогда не перемещает уже записанное содержимое. Повторного формирования макета нет.
  • Ограничения применяются в середине прохода. При превышении ограничений на количество элементов и глубину вложенности исключение выбрасывается во время этапа 5, а не раньше. Обработка документа может завершиться ошибкой на полпути.

Конвейер выполняет обход за O(количество токенов). Определение ширины столбцов таблицы добавляет ограниченное сканирование строк для каждой таблицы (этап 5, TableParser). Если предварительное сканирование :has() включено, оно добавляет один ограниченный проход по списку токенов (этап 4). Память для стека стилей составляет O(глубина вложенности), а не O(количество элементов); см. ограничения потоковой обработки. Бенчмарк производительности конвейера отрисовки HTML защищает от регрессий с порогом 5% (выполненная работа, PR #564). Постраничный performance_budget (wall_ms: 1500, peak_mb: 64) — эксплуатационный потолок.

Этап 1 — первая граница безопасности: ограничение входных данных в 10 МБ, удаление управляющих символов и нормализация переводов строк выполняются до токенизации. На этапе 5 DefaultHtmlSecurityPolicy контролирует разрешённые теги, атрибуты, свойства CSS и схемы URL. См. модель безопасности модуля HTML.

Нормализация переводов строк соответствует обработке переводов строк в стандарте HTML: CRLF и одиночный CR становятся LF. Соответствие CSS по отдельным свойствам документировано в матрице поддержки CSS, а поведение каскада — на странице css-resolver. Эта страница не повторяет поддержку по отдельным свойствам.

Возможность Enterprise. Premium расширяет покрытие CSS на том же конвейере. Последовательность из шести этапов не меняется между редакциями. См. матрицу поддержки CSS.