Zum Inhalt springen

HTML-Rendering-Pipeline

writeHtml() läuft in einem einzigen Vorwärtsdurchlauf: Es tokenisiert, löst @page und Stile auf, berechnet das Layout und gibt PDF-Operatoren aus. Zwischen den Stufen wird kein Elementbaum aufbewahrt.

Terminal-Fenster
composer require nextpdf/core:^3

Die HTML-Rendering-Pipeline wandelt HTML+CSS in einem einzigen Vorwärtsdurchlauf in PDF-Content-Stream-Operatoren um. Sie hält keinen Dokumentbaum vor. Die folgende Stufenabfolge entspricht HtmlParser::parse() in der Implementierung auf main.

Stufe 1 – Bereinigen und normalisieren. HtmlParser::parse() lehnt Eingaben über 10 MB ab, entfernt Steuerzeichen und normalisiert Zeilenenden: sowohl CRLF als auch ein einzelnes CR werden zu LF, gemäß der HTML-Zeilenendennormalisierung, an der sich die Quelle orientiert. Anschließend setzt die Methode jedes Instanzfeld zurück, sodass kein Zustand aus einem vorherigen Aufruf erhalten bleibt.

Stufe 2 – @page- und Stilblöcke extrahieren. Der Parser extrahiert zuerst die <style>-Blöcke und wendet dann die gefundenen @page-Regeln an, um die Seitengeometrie neu zu konfigurieren. Das geschieht, bevor irgendein Token verarbeitet wird, weil die Seitengröße jede spätere Layoutentscheidung beeinflusst.

Stufe 3 – Tokenisieren. HtmlTokenizer::cleanHtml() normalisiert Whitespace und bewahrt dabei den Inhalt von <pre>. Anschließend erzeugt tokenize() eine flache list<HtmlToken>. Das ist eine Tokenliste, kein Knotengraph. Texttoken, die nur aus Whitespace bestehen, werden sofort verworfen. HtmlChildScanner::scan() baut auf Basis der flachen Liste Index-Maps auf (Kindzahlen, Tagzahlen, Leerheit), sodass strukturelle Selektoren keinen Baum brauchen.

Stufe 4 – Optionaler :has()-Vorabscan. Wenn das experimentelle Feature css.has aktiviert ist, führt CssResolver::resolveHasSelectors() einen begrenzten Vorabscan über die Tokenliste aus, um den relationalen Selektor aufzulösen. Das ist eine dokumentierte, begrenzte Ausnahme von der Einzeldurchlaufregel.

Stufe 5 – Token verarbeiten (Stil, Layout, Paint). HtmlParser::processTokens() durchläuft die Tokenliste einmal. Für jedes Element löst die Methode die Kaskade auf (Layer-1-Applikatoren schreiben HtmlStyleState), berechnet die Geometrie (Layer-3-Layout) und gibt PDF-Operatoren aus (Layer-4-Paint). Die Stilvererbung nutzt einen Push-and-Pop-HtmlStyleState-Stack. Der Cursor (x, y, Ränder, Stream-Offset) wird über HtmlBlockCursor-Snapshots zwischen den Handlern weitergegeben.

Stufe 6 – Das Ergebnis zurückgeben. parse() gibt ein unveränderliches HtmlRenderResult zurück, das den ausgegebenen Content-Stream, die Endcursorposition und die verwendeten Font-Keys enthält. Der Aufrufer (writeHtml()) gibt den Cursor an das Seitenkoordinatensystem zurück.

Die Seite Layer-Verträge behandelt die Vier-Layer-Trennung, die innerhalb von Stufe 5 läuft. Die Seite Streaming-Beschränkungen behandelt den Betrieb ohne vorgehaltenen Baum und dessen Obergrenzen.

SymbolOrtStufe
Document::writeHtml(string $html): staticsrc/Core/Concerns/HasTextOutput.phpÖffentlicher Einstiegspunkt
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpOrchestriert alle Stufen
HtmlTokenizer::cleanHtml() / tokenize()src/Html/HtmlTokenizer.phpStufe 3
HtmlChildScanner::scan()src/Html/HtmlChildScanner.phpStufe-3-Index-Maps
CssResolver::resolveHasSelectors()src/Html/CssResolver.phpStufe 4 (gated)
HtmlRenderResult (stream, endX, endY, usedFontKeys)src/Html/HtmlRenderResult.phpStufe 6

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

Rendern Sie einen gestylten Bericht mit einem eingebetteten <style>-Block. Die Pipeline extrahiert den Stilblock und wendet ihn an, bevor sie irgendein Token verarbeitet.

<?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 wird vor den Token gelesen. Eine @page-Regel, die nach dem Inhalt steht, gilt trotzdem, weil die Stilextraktion der Tokenisierung vorausgeht. Die Seitengeometrie steht vor Stufe 5 fest.
  • <pre>-Whitespace bleibt erhalten. cleanHtml() erhält den Inhalt von <pre>; Whitespace an anderen Stellen wird zusammengefasst.
  • :has() ist gegated. Ohne das experimentelle Feature css.has läuft Stufe 4 nicht und :has()-Selektoren greifen nicht.
  • Ein Stream-Buffer. Die Pipeline schreibt in einen einzigen String-Buffer. Bereits geschriebener Inhalt wird nie verschoben. Es gibt kein Re-Layout.
  • Obergrenzen greifen mitten im Durchlauf. Die Element- und Verschachtelungsobergrenzen werfen während Stufe 5, nicht davor. Ein Dokument kann daher mitten im Durchlauf fehlschlagen.

Die Pipeline arbeitet beim Durchlauf in O(Token-Anzahl). Die Tabellenspaltendimensionierung fügt pro Tabelle einen begrenzten Zeilenscan hinzu (Stufe 5, TableParser). Der :has()-Vorabscan fügt einen begrenzten Durchlauf über die Tokenliste hinzu, wenn er aktiviert ist (Stufe 4). Der Speicherbedarf liegt bei O(Verschachtelungstiefe) für den Stil-Stack, nicht bei O(Element-Anzahl) – siehe Streaming-Beschränkungen. Der Performance-Benchmark der HTML-Render-Pipeline schützt vor Regressionen mit einem 5-%-Gate (zusammengeführte Arbeit, PR #564). Das performance_budget pro Seite (wall_ms: 1500, peak_mb: 64) ist die operative Obergrenze.

Stufe 1 ist die erste Sicherheitsgrenze: die 10-MB-Eingabeobergrenze, das Entfernen von Steuerzeichen und die Zeilenendennormalisierung laufen vor der Tokenisierung. DefaultHtmlSecurityPolicy gated dann die erlaubten Tags, Attribute, CSS-Eigenschaften und URL-Schemata während Stufe 5. Siehe das Sicherheitsmodell des HTML-Moduls.

Die Zeilenendennormalisierung folgt der Zeilenendenbehandlung des HTML-Standards (CRLF und ein einzelnes CR werden zu LF). Die CSS-Konformität pro Eigenschaft ist in der CSS-Support-Matrix dokumentiert, das Kaskadenverhalten auf css-resolver. Diese Seite wiederholt die Unterstützung pro Eigenschaft nicht.

Enterprise-Fähigkeit. Premium erweitert die CSS-Abdeckung auf derselben Pipeline. Die sechsstufige Abfolge bleibt dadurch unverändert. Siehe die CSS-Support-Matrix.