Render HTML to a PDF page
At a glance
Section titled “At a glance”Use writeHtml() to render a fragment of Hypertext Markup Language (HTML) and Cascading Style Sheets (CSS) as Portable Document Format (PDF) page content. Pass in markup, and NextPDF renders a formatted page. The complete, runnable version of this code is examples/08-html-basic.php. Follow the steps below, or copy the example directly.
NextPDF reads your HTML once and streams the result directly into the page. This is a single-pass streaming pipeline. You do not need to understand that model to use this recipe. Keep it in mind, though, because it explains a few rules later on this page.
Install
Section titled “Install”composer require nextpdf/core:^3This command installs the nextpdf/core package. The examples on this page run on PHP 8.4, and the supported runtime is >=8.4 <9.0.
Conceptual overview
Section titled “Conceptual overview”writeHtml() takes an HTML string and draws it into the current page, starting at the current cursor position. Inside the engine, NextPDF scans your HTML once and breaks it into tokens (HtmlTokenizer). It then walks that list from left to right (HtmlParser). For each element, it writes the matching PDF drawing instructions, called content-stream operators, into a buffer. The engine never builds or keeps an element tree in memory between calls. This deliberate design is the single-pass streaming model recorded in ADR-001.
Each supported block element becomes a layout box, and each run of text becomes a text-show operator. Styles from inline style attributes and a <style> block resolve through the CSS cascade, the rules that decide which style wins when more than one applies. Text wrapping, alignment, and spacing follow the CSS Text model, which defines how source text becomes formatted, line-wrapped text (W3C CSS Text Level 3).
If you do not choose a font, body text uses a default face. That default is a standard Type 1 font, one of the 14 standard fonts named in ISO 32000-2. The default changes only when you register and select your own font, or when a conformance profile requires NextPDF to embed a substitute.
Set this expectation early: NextPDF supports a subset of HTML and CSS, not all of either. This recipe covers the supported subset. It does not claim full HTML or full CSS support. For the exact, verified status of each module, see the CSS support matrix.
API surface
Section titled “API surface”The method signature is writeHtml(string $html): static. It is declared on the NextPDF\Contracts\PdfDocumentInterface interface and implemented in NextPDF\Core\Concerns\HasTextOutput. The method renders into the current page and creates one for you if no page exists yet. The full PHPDoc table for the method is generated from the source code.
Code sample — Quick start
Section titled “Code sample — Quick start”<?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>HTML Rendering in NextPDF</h1><p>Rendered with <strong>writeHtml()</strong>.</p>');
$doc->save(__DIR__ . '/out.pdf');Code sample — Production
Section titled “Code sample — Production”This full, self-contained example is the one the test harness runs. It mirrors examples/08-html-basic.php. Instead of hard-coding an output path, it writes to the path supplied by the harness. That lets the reproducibility harness run the script twice and compare the results.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('HTML Basic');$doc->addPage();
$html = <<<'HTML'<h1 style="color: #1E3A8A;">HTML Rendering in NextPDF</h1>
<p>NextPDF renders <strong>HTML content</strong> directly into PDF pages.This is the recommended approach for <em>mixed formatting</em>.</p>
<h2>Supported elements</h2>
<ul> <li>Headings (h1-h6)</li> <li>Paragraphs with <strong>bold</strong> and <em>italic</em></li> <li>Ordered and unordered lists</li> <li>Tables with borders and alignment</li> <li>Inline styles (color, font-size, margin)</li></ul>
<h2>Ordered list</h2>
<ol> <li>Create a Document instance</li> <li>Add pages and content</li> <li>Call save() or output()</li></ol>HTML;
$doc->writeHtml($html);
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script twice.// Honour it: do not hard-code a path, do not echo the PDF to STDOUT.$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$doc->save($out !== false ? $out : __DIR__ . '/render-html-to-pdf.pdf');
echo "Wrote render-html-to-pdf.pdf\n";Expected STDOUT:
Wrote render-html-to-pdf.pdfEdge cases & gotchas
Section titled “Edge cases & gotchas”- Cursor handoff.
writeHtml()moves the cursor to the end of the rendered content. A followingcell()or secondwriteHtml()continues from there, not from the page top. - No page yet. If no page exists,
writeHtml()adds one before rendering. CalladdPage()first when you need to set a specific page size. - Element and nesting caps. The streaming engine enforces a
50,000-element cap and a 100-level nesting cap (ADR-001). A document that exceeds either cap is rejected rather than silently truncated. - Unsupported markup. Elements and properties outside the supported subset are ignored or fall back; they do not raise errors. Check coverage against the CSS support matrix before relying on a property.
- External resources. Remote images and stylesheets follow the external-resource policy; the default policy does not fetch arbitrary remote URLs.
Performance
Section titled “Performance”Tokenization and rendering happen in one pass over your input, so cost grows linearly with the number of tokens, O(n). The default budget for this recipe is wall_ms: 1500, peak_mb: 96. Because the engine streams output and keeps no Document Object Model (DOM) in memory, peak memory follows the content-stream buffer and active style stack, not the whole document size.
CSS support matrix excerpt (Verified-only rows)
Section titled “CSS support matrix excerpt (Verified-only rows)”This excerpt includes only rows graded Verified in the truth-audited CSS support matrix. “Verified” means a src/Html/ implementation and a substantive, dedicated fixture suite that passes deterministically under the structural profile.
| W3C module | Level | Status | Evidence |
|---|---|---|---|
CSS Flexible Box Layout (css_flexbox_1) | 1 | Verified | src/Html/Flex/, tests/Unit/Html/Flex/ |
CSS Grid Layout (css_grid_1) | 1 | Verified | src/Html/Grid/, WPT corpus |
CSS Cascading and Inheritance (css_cascade_3) | 3 | Verified | src/Html/Cascade/, tests/Unit/Html/Cascade/ |
CSS Table (css_tables_3) | 3 | Verified | src/Html/Table/, table fixtures + golden PDFs |
CSS Fonts (css_fonts_4) | 4 | Verified | src/Html/FontFace/, tests/Unit/Html/FontFace/ |
Properties such as text-align, text-indent, and color are graded “Claimed” in the matrix (implemented, no dedicated module fixture), so they are not listed as Verified here.
Single-pass streaming constraints (ADR-001)
Section titled “Single-pass streaming constraints (ADR-001)”The HTML engine retains no DOM. Its state is a scalar cursor plus a push/pop style stack; whitespace-only text nodes are discarded at tokenization. One consequence is that a later element cannot restyle an earlier one, and selectors that need full-tree context (for example, complex :has() cases) are constrained per ADR-006. Plan layout that depends only on document order.
Layer contracts (ADR-010)
Section titled “Layer contracts (ADR-010)”Parsing, layout, and paint are separate layers. The parser does not emit raw paint operators, and layout dispatch does not parse CSS. Crossing those boundaries is the coupling debt that ADR-010 forbids. For recipe authors, this means the public entry point is writeHtml(). Do not reach into parser internals.
Memory budget for large documents
Section titled “Memory budget for large documents”Per ADR-020, container-scoped formatting contexts (flex, table) may build an ephemeral sub-tree, bounded to 5,000 nodes per context, 20 levels deep, with a 50 MB active-memory ceiling across live contexts and 10 levels of nesting. Outside those contexts, the streaming model holds no tree. Keep individual tables and flex containers within the node bound for predictable memory.
Security notes
Section titled “Security notes”Treat HTML input as untrusted. NextPDF does not execute scripts, and the default external-resource policy does not fetch arbitrary remote URLs, so the engine itself is conservative. Even so, validate or sanitize any HTML you assemble from user input before you render it. The element and nesting caps also protect you: they bound how much work a hostile or malformed document can demand.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| CSS Text controls the translation of source text into formatted, line-wrapped text. | W3C CSS Text Level 3 | css_text_3#x1.x2.p4 | |
| The default body face resolves to a standard Type 1 font. | ISO 32000-2 | iso32000_2_sec9#x1.x29 |
This recipe shows how NextPDF renders a supported HTML and CSS subset. It does not assert full HTML or full CSS support. The verified per-module status is in the CSS support matrix.
Commercial context
Section titled “Commercial context”Not applicable.