Конвейер отрисовки 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 см. на странице контракты уровней. О том, что сохраняемого дерева нет, и об ограничениях этого свойства см. на странице ограничения потоковой обработки.
Поверхность API
Заголовок раздела «Поверхность API»| Символ | Расположение | Этап |
|---|---|---|
Document::writeHtml(string $html): static | src/Core/Concerns/HasTextOutput.php | Публичная точка входа |
HtmlParser::parse(string $html): HtmlRenderResult | src/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.