Aller au contenu

Pipeline de rendu HTML

writeHtml() exécute une passe unique en avant : tokenisation, résolution de @page et des styles, mise en page, puis peinture des opérateurs PDF. Aucun arbre d’éléments n’est conservé entre les étapes.

Fenêtre de terminal
composer require nextpdf/core:^3

Le pipeline de rendu HTML convertit le HTML+CSS en opérateurs de flux de contenu PDF au cours d’une seule passe en avant. Il ne construit pas d’arbre de document conservé. La séquence d’étapes ci-dessous reflète HtmlParser::parse() tel qu’il est implémenté sur main.

Étape 1 — Assainir et normaliser. HtmlParser::parse() rejette toute entrée de plus de 10 Mo, supprime les caractères de contrôle et normalise les fins de ligne : les séquences CRLF comme les CR isolés deviennent des LF, conformément à la normalisation des fins de ligne HTML appliquée à la source. Il réinitialise ensuite tous les champs d’instance, de sorte qu’aucun état ne survive d’un appel au suivant.

Étape 2 — Extraire les blocs @page et de style. Le parseur commence par extraire les blocs <style>, puis applique les règles @page découvertes afin de reconfigurer la géométrie de page. Cette étape intervient avant le traitement du moindre token, car la taille de page influe sur toutes les décisions de mise en page qui suivent.

Étape 3 — Tokeniser. HtmlTokenizer::cleanHtml() normalise les espaces tout en préservant le contenu de <pre>. Ensuite, tokenize() produit une list<HtmlToken> plate. Il s’agit d’une liste de tokens, et non d’un graphe de nœuds. Les tokens de texte composés uniquement d’espaces sont écartés immédiatement. HtmlChildScanner::scan() construit des cartes d’index (nombre d’enfants, nombre de balises, vacuité) sur la liste plate, de sorte que les sélecteurs structurels n’aient pas besoin d’un arbre.

Étape 4 — Pré-balayage :has() optionnel. Lorsque la fonctionnalité expérimentale css.has est activée, CssResolver::resolveHasSelectors() effectue un pré-balayage borné de la liste de tokens pour résoudre le sélecteur relationnel. C’est une exception documentée et bornée à la règle de la passe unique.

Étape 5 — Traiter les tokens (style, mise en page, peinture). HtmlParser::processTokens() parcourt la liste de tokens une seule fois. Pour chaque élément, il résout la cascade (les applicateurs de la couche 1 écrivent HtmlStyleState), calcule la géométrie (mise en page de la couche 3) et émet les opérateurs PDF (peinture de la couche 4). L’héritage des styles s’appuie sur une pile HtmlStyleState, avec empilement et dépilement. Le curseur (x, y, marges, décalage dans le flux) passe d’un gestionnaire à l’autre via des instantanés HtmlBlockCursor.

Étape 6 — Renvoyer le résultat. parse() renvoie un HtmlRenderResult immuable qui contient le flux de contenu émis, la position finale du curseur et les clés de police utilisées. L’appelant (writeHtml()) replace le curseur dans le repère de coordonnées de la page.

La page contrats de couche décrit la séparation en quatre couches exécutée au sein de l’étape 5. La page contraintes de streaming décrit la propriété d’absence d’arbre conservé et les plafonds associés.

SymboleEmplacementÉtape
Document::writeHtml(string $html): staticsrc/Core/Concerns/HasTextOutput.phpPoint d’entrée public
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpOrchestre toutes les étapes
HtmlTokenizer::cleanHtml() / tokenize()src/Html/HtmlTokenizer.phpÉtape 3
HtmlChildScanner::scan()src/Html/HtmlChildScanner.phpCartes d’index de l’étape 3
CssResolver::resolveHasSelectors()src/Html/CssResolver.phpÉtape 4 (sous condition)
HtmlRenderResult (stream, endX, endY, usedFontKeys)src/Html/HtmlRenderResult.phpÉtape 6

Tiré de 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');

Rends un rapport stylisé avec un bloc <style> intégré. Le pipeline extrait et applique ce bloc de style avant de traiter le moindre token.

<?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 est lu avant les tokens. Une règle @page placée après le contenu s’applique tout de même, car l’extraction des styles précède la tokenisation. La géométrie de page est figée avant l’étape 5.
  • Les espaces <pre> sont préservés. cleanHtml() protège le contenu <pre> ; les espaces ailleurs sont fusionnés.
  • :has() est sous condition. Sans la fonctionnalité expérimentale css.has, l’étape 4 ne s’exécute pas et les sélecteurs :has() ne correspondent à rien.
  • Un seul tampon de flux. Le pipeline écrit dans un tampon de chaîne unique. Le contenu déjà écrit n’est jamais déplacé. Il n’y a pas de remise en page.
  • Les plafonds s’appliquent en cours de passe. Les plafonds d’éléments et d’imbrication lèvent une exception pendant l’étape 5, et non avant. Un document peut échouer à mi-parcours.

Le pipeline parcourt les tokens en O(nombre de tokens). Le dimensionnement des colonnes de tableau ajoute un balayage borné des lignes par tableau (étape 5, TableParser). Le pré-balayage :has() ajoute une passe bornée sur la liste de tokens lorsqu’il est activé (étape 4). La mémoire est en O(profondeur d’imbrication) pour la pile de styles, et non en O(nombre d’éléments) — voir les contraintes de streaming. Le benchmark de performances du pipeline de rendu HTML protège contre les régressions avec un seuil de 5 % (travail fusionné, PR #564). Le performance_budget par page (wall_ms: 1500, peak_mb: 64) est le plafond opérationnel.

L’étape 1 est la première frontière de sécurité : le plafond d’entrée de 10 Mo, la suppression des caractères de contrôle et la normalisation des fins de ligne s’exécutent avant la tokenisation. DefaultHtmlSecurityPolicy contrôle ensuite les balises, attributs, propriétés CSS et schémas d’URL autorisés pendant l’étape 5. Consulte le modèle de sécurité du module HTML.

La normalisation des fins de ligne suit le traitement des fins de ligne du standard HTML (CRLF et CR isolé deviennent LF). La conformité CSS par propriété est documentée dans la matrice de prise en charge CSS et le comportement de la cascade sur css-resolver. Cette page ne reprend pas la prise en charge par propriété.

Capacité Enterprise. Premium élargit la couverture CSS sur ce même pipeline. La séquence en six étapes ne change pas d’une édition à l’autre. Consulte la matrice de prise en charge CSS.