Skip to content

Receipts, not adjectives

Real output, verify it.

Each card below is a stylized cover; the file behind it is genuine, unretouched output from the NextPDF engine — download it, open it, run it through your own validator. And read the exact PHP that produced it: every sample ships its generating script, hash-bound to the PDF.

Documents

What the layout engine does with HTML and CSS.

  • Commercial invoice

    Core

    HTML/CSS tables, a billed-to block, right-aligned numeric columns and a totals row — straight from writeHtml().

    Download this PDF

    SHA-256 ad012a058a … e01fa175b4

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 invoice.pdf

  • Analytics report

    Core

    An executive summary, KPI stat cards, a throughput table with a totals row, and a two-page flow that exercises the continuation header.

    Download this PDF

    SHA-256 e3c0d38bde … f8c504da76

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 report.pdf

  • Barcode & label sheet

    Core + Pro

    Every supported symbology as a labelled card — retail, logistics, postal, GS1 DataBar, 2D matrix and colour JAB Code — grouped by edition.

    Download this PDF

    SHA-256 5fd79832b5 … 74bd575ec1

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 labels.pdf

  • Multi-page handbook

    Core

    A hierarchical PDF outline (reader bookmarks), a repeating header/footer, and automatic page breaks across four pages.

    Download this PDF

    SHA-256 0018c43f66 … 7d1687d98d

    4 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 handbook.pdf

  • Typography specimen

    Core

    Embedded Inter, Barlow and JetBrains Mono: weights and styles, Unicode pangrams (Latin-ext, Greek, Cyrillic), a type scale, alignment and kerning pairs.

    Download this PDF

    SHA-256 3d08a73aee … 47f54e025e

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 typography.pdf

  • Analytics charts

    Pro

    Bar, line and pie charts drawn document-integrated by the Pro chart renderers, alongside KPI stat cards.

    Download this PDF

    SHA-256 58ac23cf92 … 8092d1906b

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 charts.pdf

Standards & conformance

The named standards NextPDF actually produces — each one a real file you can put through veraPDF, Acrobat preflight, or OpenSSL.

  • Accessible tagged report (PDF/UA-2)

    Core

    A tagged PDF whose content demonstrates the accessibility story itself: a logical structure tree, real heading levels, alt text and artifact-marked furniture. ISO 14289-2.

    Download this PDF

    SHA-256 b7fb7e4586 … eb5b586f3e

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 accessible-report.pdf

  • Digital signature (PAdES)

    Core

    A service agreement signed through the real PAdES engine: CMS SignedData over a ByteRange covering the document, with an RFC 3161 timestamp. Open the signature panel in any reader and inspect it. ETSI EN 319 142.

    Download this PDF

    SHA-256 a7560da779 … 3a133ac17c

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 signed-agreement.pdf

  • Factur-X e-invoice

    Enterprise

    A PDF/A e-invoice carrying an embedded EN 16931 CrossIndustryInvoice XML payload (Factur-X / ZUGFeRD) — a hybrid a machine and a human can both read.

    Download this PDF

    SHA-256 cd22cc86c8 … 8b9032cefc

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 e-invoice.pdf

  • PDF/A-4 archival record

    Enterprise

    Long-term archival output: embedded font subsets, an sRGB output intent and XMP archival metadata, so the document still renders identically decades from now. ISO 19005-4.

    Download this PDF

    SHA-256 4de96389a2 … 0938cb8724

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 archival.pdf

  • AES-256 encryption & permissions

    Core

    A document secured with AES-256 (ISO 32000-2, V5/R6/AESV3) and a permission policy. It opens without a password for this demo; copy and modify permissions are set and enforced by conforming readers.

    Download this PDF

    SHA-256 349f028604 … 658ee534d8

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 encrypted.pdf

  • Colour-managed print output

    Core

    A colour-managed sheet carrying a PDF/X-4-oriented output intent with an embedded ICC v4 profile. Honest scope: the output-intent structure PDF/X preflight expects — not a certified PDF/X file.

    Download this PDF

    SHA-256 9c0b8b27cd … 55c3436ca3

    1 page · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 print-ready.pdf

  • Linearized for Fast Web View

    Core

    Linearized output (ISO 32000-2 Annex F): the first page streams and renders before the whole file has finished downloading.

    Download this PDF

    SHA-256 d466517d52 … 26798d13a0

    2 pages · nextpdf/core@cfcaab813 · generated 2026-07-14

    Verify shasum -a 256 web-optimized.pdf

The covers above are stylized; the downloadable PDF behind each one is the real thing, and “View source” shows the exact PHP that produced it — the script is hash-bound to the PDF at build time, so the code you read is the code that ran. The SHA-256, engine build and dateare on every card: download the file and run the shasum line to confirm the bytes match. Validator output isn't pre-baked here on purpose — the point is that you validate it, against the tool you trust. For the exact command per standard, seestandards & how to verify them.

SourceCommercial invoice

Produced byphp tools/render-samples/scripts/invoice.php

tools/render-samples/scripts/invoice.php
<?php

declare(strict_types=1);

/**
 * Sample: Commercial Invoice (NextPDF Core, PDF 2.0).
 *
 * Rebuilt onto the shared SampleTemplate: branded top band + logo masthead +
 * spec table + "NextPDF SAMPLE" footer, with the invoice body rendered through
 * the design-system stylesheet (SampleTemplate::sharedCss()). Real Core API
 * only — Document::writeHtml() tables, numeric alignment, document metadata.
 *
 * @see NextPDF\Samples\SampleTemplate
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'invoice',
    title: 'Commercial Invoice',
    description: 'A branded commercial invoice with a billed-to block, an invoice detail panel, a line-item table with aligned numeric columns, and a totals row.',
    capability: 'HTML/CSS tables',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong> bill-to, totals row

$body = <<<'HTML'
<table class="np-meta"><tr>
    <td style="width: 58%; vertical-align: top;">
        <p class="np-eyebrow">BILLED TO</p>
        <p class="np-strong">Northwind Trading Co.</p>
        <p class="np-addr">Attn: Accounts Payable<br>420 Market Street<br>Seattle, WA 98101<br>[email protected]</p>
    </td>
    <td style="width: 42%; vertical-align: top;">
        <p class="np-eyebrow">INVOICE</p>
        <table class="np-kv">
            <tr><td class="np-kv-k">Number</td><td class="np-kv-v">INV-2026-0042</td></tr>
            <tr><td class="np-kv-k">Issued</td><td class="np-kv-v">2026-07-13</td></tr>
            <tr><td class="np-kv-k">Due</td><td class="np-kv-v np-accent np-strong">2026-08-12</td></tr>
            <tr><td class="np-kv-k">Terms</td><td class="np-kv-v">Net 30</td></tr>
        </table>
    </td>
</tr></table>

<table class="np-table" style="margin-top: 14pt;">
    <thead>
        <tr>
            <th style="width: 6%;">#</th>
            <th style="width: 46%;">Description</th>
            <th style="width: 12%;" class="np-num">Qty</th>
            <th style="width: 18%;" class="np-num">Unit price</th>
            <th style="width: 18%;" class="np-num">Amount</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td><td>NextPDF Pro license &mdash; annual subscription</td>
            <td class="np-num">2</td><td class="np-num">$299.00</td><td class="np-num">$598.00</td>
        </tr>
        <tr class="np-zebra">
            <td>2</td><td>NextPDF Enterprise license &mdash; annual subscription</td>
            <td class="np-num">1</td><td class="np-num">$999.00</td><td class="np-num">$999.00</td>
        </tr>
        <tr>
            <td>3</td><td>Priority support &mdash; 12 months, 4-hour SLA</td>
            <td class="np-num">1</td><td class="np-num">$199.00</td><td class="np-num">$199.00</td>
        </tr>
        <tr class="np-zebra">
            <td>4</td><td>Onboarding &amp; integration workshop (remote)</td>
            <td class="np-num">1</td><td class="np-num">$450.00</td><td class="np-num">$450.00</td>
        </tr>
    </tbody>
</table>

<table class="np-summary" style="margin-top: 8pt;">
    <tr><td style="width: 64%;"></td><td class="np-num np-muted" style="width: 22%;">Subtotal</td><td class="np-num" style="width: 14%;">$2,246.00</td></tr>
    <tr><td></td><td class="np-num np-muted">Tax (8.5%)</td><td class="np-num">$190.91</td></tr>
    <tr class="np-totals"><td></td><td class="np-num">Total due</td><td class="np-num">$2,436.91</td></tr>
</table>

<p class="np-note">Payment is due within 30 days. Please reference invoice INV-2026-0042 on all remittances. All amounts in USD.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 3pt 0; }
    p.np-strong, .np-strong { color: #111827; font-weight: bold; margin: 0; }
    p.np-addr { color: #6B7280; font-size: 9pt; margin: 2pt 0 0 0; }
    table.np-kv { width: 100%; }
    td.np-kv-k { color: #6B7280; font-size: 9pt; padding: 1pt 0; }
    td.np-kv-v { color: #111827; font-size: 9pt; text-align: right; padding: 1pt 0; }
    table.np-summary td { padding: 3pt 6pt; font-size: 9pt; }
    table.np-summary tr.np-totals td { background-color: #1E3A8A; color: #FFFFFF; font-weight: bold; }
    p.np-note { color: #6B7280; font-size: 8pt; margin-top: 16pt; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/invoice.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceAnalytics report

Produced byphp tools/render-samples/scripts/report.php

tools/render-samples/scripts/report.php
<?php

declare(strict_types=1);

/**
 * Sample: Analytics Report (NextPDF Core, PDF 2.0).
 *
 * Rebuilt onto the shared SampleTemplate: branded masthead on page 1, an
 * executive summary, a three-card KPI row, and a throughput table with a
 * totals row; page 2 carries a genuine CSS Multi-column Layout 1 findings
 * block (`column-count`, not a table-column workaround) and a numbered
 * recommendations list.
 *
 * The page 1 -> page 2 break is an EXPLICIT Document::addPage() between two
 * writeHtml() calls (the same pattern as handbook.php), not CSS
 * `page-break-before`. This is a deliberate engine-limitation workaround,
 * verified empirically (2026-07-13 minimal repro): NextPDF's multi-column
 * engine is a single-page-per-container, stream-based algorithm
 * (NextPDF\Html\MultiCol\MultiColLayoutEngine — "cross-page fragmentation ...
 * is delegated to the page-break controller and is out of scope for this
 * engine"). Letting the auto page break (Document::setAutoPageBreak()) land
 * INSIDE the findings `column-count` block mid-render splits it across the
 * page boundary and corrupts the column redistribution (all items collapse
 * into a single narrow column with the tail silently dropped) — CSS
 * `break-inside: avoid` on the container does NOT prevent this, because the
 * container's total height is only known after it finishes rendering, one
 * pass, so the page-break controller cannot pre-empt a break before it opens.
 * The explicit addPage() guarantees the multicol block always starts fresh
 * on a page with a full body height available, which every item here fits
 * inside easily. The continuation header (slim band, footer "2 / 2") is
 * still exercised genuinely — it renders from the page-number-keyed header
 * callback (SampleTemplate::installHeader()), not from anything specific to
 * how the page break was triggered.
 *
 * @see NextPDF\Samples\SampleTemplate
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 * @see NextPDF\Html\MultiCol\MultiColLayoutEngine
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'report',
    title: 'Analytics Report',
    description: 'A structured quarterly report with an executive summary, KPI stat cards, a throughput table, and multi-column findings that exercise the continuation header.',
    capability: 'HTML/CSS layout, multi-column',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong> emphasis + KPI values

$page1 = <<<'HTML'
<p class="np-byline">NextPDF Engine &mdash; Q2 2026 &middot; Prepared for the Platform Team</p>

<p class="np-h2" style="margin-top: 0;">Executive summary</p>
<p>Document throughput grew across every workload class this quarter while median render
latency continued to fall. The engine processed <strong>4.2 million</strong> documents at a
<strong>99.98%</strong> success rate, and peak-hour concurrency held steady under a 3&times;
synthetic load test. Growth was broadest in the invoice and signed-archival profiles, while
labels and barcode workloads held their existing lead on both volume and reliability.</p>
<p>The figures below summarize volume, latency, and conformance by document profile; the
continuation page breaks the quarter down into the findings that drove these numbers and the
actions the platform team is carrying into Q3.</p>

<div class="np-stat-row">
    <div class="np-stat">
        <p class="np-stat-value">4.2M</p>
        <p class="np-stat-delta">+18% QoQ</p>
        <p class="np-stat-label">Documents rendered</p>
    </div>
    <div class="np-stat">
        <p class="np-stat-value">38 ms</p>
        <p class="np-stat-delta">&minus;14% QoQ</p>
        <p class="np-stat-label">Median render latency</p>
    </div>
    <div class="np-stat">
        <p class="np-stat-value">99.98%</p>
        <p class="np-stat-delta">+0.03 pts QoQ</p>
        <p class="np-stat-label">Render success rate</p>
    </div>
</div>

<p class="np-h2">Throughput by document profile</p>
<table class="np-table">
    <thead>
        <tr>
            <th style="width: 34%;">Profile</th>
            <th style="width: 18%;" class="np-num">Documents</th>
            <th style="width: 16%;" class="np-num">Median (ms)</th>
            <th style="width: 14%;" class="np-num">p99 (ms)</th>
            <th style="width: 18%;" class="np-num">Success</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Invoices &amp; statements</td>
            <td class="np-num">1,840,200</td><td class="np-num">38</td><td class="np-num">91</td><td class="np-num">99.99%</td>
        </tr>
        <tr class="np-zebra">
            <td>Reports &amp; dashboards</td>
            <td class="np-num">986,540</td><td class="np-num">64</td><td class="np-num">182</td><td class="np-num">99.98%</td>
        </tr>
        <tr>
            <td>Labels &amp; barcodes</td>
            <td class="np-num">742,310</td><td class="np-num">21</td><td class="np-num">53</td><td class="np-num">100.00%</td>
        </tr>
        <tr class="np-zebra">
            <td>Signed &amp; archival (PDF/A)</td>
            <td class="np-num">631,070</td><td class="np-num">112</td><td class="np-num">298</td><td class="np-num">99.95%</td>
        </tr>
        <tr>
            <td>Encrypted deliveries</td>
            <td class="np-num">198,410</td><td class="np-num">45</td><td class="np-num">103</td><td class="np-num">99.97%</td>
        </tr>
        <tr class="np-totals">
            <td>All profiles</td>
            <td class="np-num">4,398,530</td><td class="np-num">53</td><td class="np-num">178</td><td class="np-num">99.98%</td>
        </tr>
    </tbody>
</table>
<p class="np-note">Figures are illustrative sample data generated for demonstration purposes only.</p>
HTML;

$page2 = <<<'HTML'
<p class="np-h2" style="margin-top: 0;">Key findings</p>
<div class="np-findings-col">
    <p class="np-finding">Median latency fell 14% quarter-over-quarter after the layout-cache rollout &mdash; the largest single-quarter improvement on record.</p>
    <p class="np-finding">Worker-safe rendering held steady under a 3&times; synthetic peak-traffic load test with zero dropped jobs.</p>
    <p class="np-finding">Barcode and label workloads reached a perfect 100% success rate for the second consecutive quarter.</p>
    <p class="np-finding">Archival output passed conformance checks on every sampled batch, with zero PDF/A validator failures.</p>
    <p class="np-finding">p99 latency on signed &amp; archival documents remains the highest of any profile, tracking closely with PAdES signing and timestamp overhead.</p>
    <p class="np-finding">Font-embedding failures dropped to zero after the strict PDF&nbsp;2.0 embedding gate shipped in Core 3.3.</p>
</div>

<p class="np-h2">Recommended actions</p>
<table class="np-reco">
    <tr><td class="np-reco-num">1</td><td class="np-reco-text">Extend the layout cache to the reports profile to recover p99 headroom.</td></tr>
    <tr><td class="np-reco-num">2</td><td class="np-reco-text">Promote the signed-output pipeline to the default for compliance tenants.</td></tr>
    <tr><td class="np-reco-num">3</td><td class="np-reco-text">Add a synthetic canary for the archival profile to catch drift early.</td></tr>
    <tr><td class="np-reco-num">4</td><td class="np-reco-text">Investigate CMS timestamp batching to trim signed-document p99 further.</td></tr>
</table>

<p class="np-note">Generated by the NextPDF engine. Figures are illustrative sample data.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-byline { color: #6B7280; font-size: 8.5pt; margin: 0 0 14pt 0; }
    .np-stat-row { display: flex; align-items: flex-start; gap: 10pt; margin: 8pt 0 16pt 0; break-inside: avoid; }
    .np-stat { flex: 1; border: 0.5pt solid #D1D5DB; border-radius: 3pt; background-color: #F8FAFC; padding: 9pt 12pt; }
    p.np-stat-value { font-family: 'Barlow'; font-weight: bold; font-size: 19pt; line-height: 1.1; color: #1E3A8A; margin: 0 0 2pt 0; }
    p.np-stat-delta { color: #6B7280; font-size: 7pt; margin: 0 0 5pt 0; }
    p.np-stat-label { color: #6B7280; font-size: 7.5pt; line-height: 1.2; letter-spacing: 0.3pt; margin: 0; }
    p.np-note { color: #6B7280; font-size: 8pt; margin: 8pt 0 14pt 0; }
    .np-findings-col { column-count: 2; column-gap: 20pt; column-rule: 0.5pt solid #D1D5DB; margin: 4pt 0 4pt 0; }
    p.np-finding { font-size: 9pt; color: #111827; margin: 0 0 10pt 0; text-indent: -10pt; padding-left: 10pt; }
    table.np-reco { width: 100%; border-collapse: collapse; margin: 2pt 0 4pt 0; }
    td.np-reco-num { width: 20pt; font-family: 'Barlow'; font-weight: bold; font-size: 9.5pt; color: #1E3A8A; padding: 0 6pt 9pt 0; vertical-align: top; }
    td.np-reco-text { font-size: 9.5pt; color: #111827; padding: 0 0 9pt 0; vertical-align: top; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $page1);

$doc->addPage();
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $page2);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/report.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceBarcode & label sheet

Produced byphp tools/render-samples/scripts/labels.php

tools/render-samples/scripts/labels.php
<?php

declare(strict_types=1);

/**
 * Sample: Barcode & Label Sheet (NextPDF Core + Pro, PDF 2.0).
 *
 * Rebuilt onto the shared SampleTemplate: branded top band + logo masthead +
 * spec table + "NextPDF SAMPLE" footer, with the label gallery rendered
 * through LabelCardGrid — one card per symbology, sectioned by category, in
 * an order driven entirely by the canonical SymbologyMatrix. The matrix is
 * completeness-checked against the engine's BarcodeType/Barcode2DType enums
 * by bin/check-symbology-matrix.php (run by render-all.ps1 immediately
 * before this render), so the sheet can never silently drift from the
 * engine's real symbology surface.
 *
 * Real API only — LabelCardGrid drives write1DBarcode() / write2DBarcode()
 * and the JabCode color path (Barcode2D::generateColor()) directly for every
 * symbology on the sheet, including the height-modulated postal codes
 * (POSTNET / PLANET / USPS IMb) and the Pro rMQR / Han Xin matrices.
 *
 * @see NextPDF\Samples\LabelCardGrid
 * @see NextPDF\Samples\SymbologyMatrix
 * @see NextPDF\Core\Concerns\HasBarcodes
 */

require __DIR__ . '/../lib/bootstrap-premium.php';

use NextPDF\Barcode\Barcode2DType;
use NextPDF\Samples\Edition;
use NextPDF\Samples\LabelCardGrid;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Samples\SymbologyEntry;
use NextPDF\Samples\SymbologyMatrix;
use NextPDF\Samples\SymbologySection;

/**
 * LIB GAP WORKAROUND (reported, not fixed in lib — scripts own only their
 * own render): the canonical SymbologyMatrix::entries() GS1 DataBar row
 * carries the sample payload '(01)00012345678905', a GS1 AI element string
 * that is only valid for Gs1DataBarEncoder's §7 EXPANDED / EXPANDED_STACKED
 * variants. write2DBarcode() (the only entry point LabelCardGrid::card()
 * uses) exposes no parameter to select those variants, so encode() always
 * takes the default OMNIDIRECTIONAL path, whose normalisePayload() requires
 * a plain 13- or 14-digit numeric GTIN and throws
 * InvalidArgumentException('GS1 DataBar payload must be a non-empty numeric
 * string (13 or 14 digits).') on the AI-bracketed string — verified by
 * running this script unmodified. Substitutes the checksum-valid GTIN-14
 * from the encoder's OWN ISO/IEC 24724:2011 Annex F.1 round-trip reference
 * example (Gs1DataBarEncoder class docblock), 24012345678905 — mod-10
 * verified here independently — for this render only.
 */
function gs1DataBarPayloadWorkaround(SymbologyEntry $entry): SymbologyEntry
{
    if ($entry->type !== Barcode2DType::GS1DataBar) {
        return $entry;
    }

    return new SymbologyEntry(
        type: $entry->type,
        label: $entry->label,
        dimension: $entry->dimension,
        edition: $entry->edition,
        section: $entry->section,
        payload: '24012345678905',
    );
}

$spec = new SampleSpec(
    id: 'labels',
    title: 'Barcode & Label Sheet',
    description: 'Every supported symbology as a labelled card, categorized by edition: retail, logistics, postal, GS1 DataBar, 2D matrix, and advanced 2D codes including color JAB Code.',
    capability: '1D/2D barcode symbologies',
    edition: Edition::Pro,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B'], ['JetBrainsMono', '']]); // card name/chip + payload

$introCss = <<<'CSS'
    <style>
    p.np-intro { color: #6B7280; font-size: 9pt; margin: 0 0 12pt 0; }
    </style>
    CSS;
$doc->writeHtml(
    SampleTemplate::sharedCss() . $introCss
        . '<p class="np-intro">The complete marketable symbology surface, one labelled card per code: '
        . 'name, edition, the rendered barcode, and a checksum-valid sample payload in JetBrains Mono.</p>',
);

$margins = $doc->getMargins();
$left = $margins->left;
$contentWidth = $doc->getPageWidth() - $margins->left - $margins->right;
$pageBreakY = $doc->getPageHeight() - $margins->bottom;

$grid = new LabelCardGrid($doc, $left, $contentWidth, $pageBreakY);

foreach (SymbologySection::ordered() as $section) {
    $grid->sectionHeader($section);
    foreach (SymbologyMatrix::section($section) as $entry) {
        $grid->card(gs1DataBarPayloadWorkaround($entry));
    }
}
$grid->flushRow();

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/labels.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceMulti-page handbook

Produced byphp tools/render-samples/scripts/handbook.php

tools/render-samples/scripts/handbook.php
<?php

declare(strict_types=1);

/**
 * Sample: Integration Handbook with PDF Outline (NextPDF Core, PDF 2.0).
 *
 * Rebuilt onto the shared SampleTemplate: branded masthead on page 1, a
 * repeating slim band + footer on every page, and a hierarchical PDF
 * outline (Document::bookmark()) interleaved with the writeHtml() body
 * across four chapters. Demonstrates multi-page layout, the bookmark
 * navigation API, and a JetBrains Mono code panel — real Core API only.
 *
 * @see NextPDF\Core\Document
 * @see NextPDF\Core\Concerns\HasNavigation::bookmark()
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'handbook',
    title: 'Integration Handbook',
    description: 'A multi-page technical handbook with a hierarchical PDF navigation outline, an edition capability matrix, a runnable integration example, and conformance and operations guidance.',
    capability: 'PDF outline & multi-page layout',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B'], ['JetBrainsMono', '']]);

// Custom rules beyond sharedCss(). Every rule here states font-weight and
// color explicitly (never relies on an inherited default) — verified by a
// standalone diagnostic render: an element nested inside a <td> that omits
// font-weight inherits whatever weight the table's own layout pass last
// touched (a real engine quirk), so table-cell content always carries its
// own explicit weight. Direct classes/tags on <td>/<th> themselves (not
// nested children) resolve correctly, matching table.np-table's own th/td
// rules in sharedCss().
$css = <<<'CSS'
    <style>
    .np-center { text-align: center; }
    .np-th-edition { font-weight: bold; color: #111827; text-align: center; }
    .np-cell-yes { text-align: center; color: #111827; font-weight: normal; }
    .np-cell-no { text-align: center; color: #6B7280; font-weight: normal; }
    .np-std-name { font-weight: bold; color: #111827; }
    table.np-code { width: 100%; margin: 6pt 0 4pt 0; }
    table.np-code td { background-color: #F8FAFC; border: 0.4px solid #D1D5DB; padding: 9pt 12pt; font-family: 'JetBrainsMono'; font-size: 8pt; color: #111827; line-height: 1.55; }
    .np-pipeline { color: #1E3A8A; font-weight: bold; font-size: 10.5pt; margin: 6pt 0 2pt 0; }
    </style>
    CSS;

// ── Chapter 1 — Engine & Edition Matrix (page 1) ───────────────────────
$doc->bookmark('1. Engine & Edition Matrix', level: 0);
$chapter1 = <<<'HTML'
<p class="np-h2">1. Engine &amp; Edition Matrix</p>
<p>NextPDF ships as three editions built on one shared engine core. A document generated on Core keeps rendering correctly after an upgrade &mdash; editions add capability, they never change behavior.</p>
<table class="np-table" style="margin-top: 10pt;">
    <thead>
        <tr>
            <th style="width: 46%;">Capability</th>
            <th style="width: 18%;" class="np-th-edition">Core</th>
            <th style="width: 18%;" class="np-th-edition">Pro</th>
            <th style="width: 18%;" class="np-th-edition">Enterprise</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>HTML/CSS rendering &amp; tables</td>
            <td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr class="np-zebra">
            <td>Vector drawing &amp; shapes</td>
            <td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr>
            <td>1D barcodes (Code128, EAN, UPC)</td>
            <td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr class="np-zebra">
            <td>Digital signatures (PAdES B-B / B-T)</td>
            <td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr>
            <td>Interactive charts</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr class="np-zebra">
            <td>2D barcodes (QR, DataMatrix, PDF417)</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr>
            <td>Tagged, accessible output (PDF/UA)</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr class="np-zebra">
            <td>PDF/A archival profiles</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr>
            <td>AES-256 encryption &amp; permissions</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td>
        </tr>
        <tr class="np-zebra">
            <td>Fast Web View (linearization)</td>
            <td class="np-cell-no">&mdash;</td><td class="np-cell-no">&mdash;</td><td class="np-cell-yes">Included</td>
        </tr>
    </tbody>
</table>
<p style="margin-top: 10pt;">Every edition renders the matrix above through the same <code>Document</code> API &mdash; upgrading changes what is available, never how you call it.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $chapter1);

// ── Chapter 2 — Five-Minute Integration ─────────────────────────────────
$doc->addPage();
$doc->bookmark('2. Five-Minute Integration', level: 0);
$chapter2Intro = <<<'HTML'
<p class="np-h2">2. Five-Minute Integration</p>
<p>Every NextPDF document follows the same three calls: create, write, save. The example below produces a complete, valid PDF file on a fresh PHP 8.4 installation.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $chapter2Intro);

$doc->bookmark('2.1 Install', level: 1);
$section21 = <<<'HTML'
<p class="np-h3">2.1 Install</p>
<p>Add the engine to any Composer-managed project. No native PHP extensions are required for the Core feature set.</p>
<table class="np-code"><tr><td>composer require nextpdf/core</td></tr></table>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section21);

$doc->bookmark('2.2 Render Your First PDF', level: 1);
$section22 = <<<'HTML'
<p class="np-h3">2.2 Render Your First PDF</p>
<p>This script creates a page, writes HTML content, and saves a PDF &mdash; the same three calls scale from a single page to a 500-page report.</p>
<table class="np-code"><tr><td>
&lt;?php<br>
&nbsp;<br>
use NextPDF\Core\Document;<br>
&nbsp;<br>
$doc = Document::createStandalone();<br>
$doc-&gt;setTitle('Quarterly Report');<br>
$doc-&gt;addPage();<br>
$doc-&gt;writeHtml('&lt;h1&gt;Hello, NextPDF&lt;/h1&gt;');<br>
$doc-&gt;save('report.pdf');<br>
</td></tr></table>
<p style="margin-top: 10pt;">The <code>Document</code> class exposes the same fluent API for tables, barcodes, charts, and PAdES signatures &mdash; see the examples at getnextpdf.com for the complete surface.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section22);

// ── Chapter 3 — Conformance & Archival ──────────────────────────────────
$doc->addPage();
$doc->bookmark('3. Conformance & Archival', level: 0);
$chapter3Intro = <<<'HTML'
<p class="np-h2">3. Conformance &amp; Archival</p>
<p>Every NextPDF document is a conformant ISO 32000-2 (PDF 2.0) file by construction. The engine also supports the long-term-preservation and accessibility profiles regulated industries require.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $chapter3Intro);

$doc->bookmark('3.1 Long-Term Archival', level: 1);
$section31 = <<<'HTML'
<p class="np-h3">3.1 Long-Term Archival</p>
<p>PDF/A output embeds every font and color profile the document needs, so an archived file renders identically decades after it was produced &mdash; without depending on external resources.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section31);

$doc->bookmark('3.2 Accessibility', level: 1);
$section32 = <<<'HTML'
<p class="np-h3">3.2 Accessibility</p>
<p>Tagged output exposes headings, tables, and reading order to assistive technology, meeting the structural requirements of PDF/UA.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section32);

$doc->bookmark('3.3 Digital Signatures', level: 1);
$section33 = <<<'HTML'
<p class="np-h3">3.3 Digital Signatures</p>
<p>PAdES signatures attach cryptographic proof of authorship and, at the B-T level, a timestamp that survives certificate expiry.</p>
<table class="np-table" style="margin-top: 8pt;">
    <thead>
        <tr><th style="width: 32%;">Standard</th><th style="width: 68%;">Scope</th></tr>
    </thead>
    <tbody>
        <tr><td class="np-std-name">ISO 32000-2:2020</td><td>Base document format (PDF 2.0)</td></tr>
        <tr class="np-zebra"><td class="np-std-name">ISO 19005 (PDF/A)</td><td>Long-term archival fidelity</td></tr>
        <tr><td class="np-std-name">ISO 14289 (PDF/UA)</td><td>Structured, accessible output</td></tr>
        <tr class="np-zebra"><td class="np-std-name">ETSI EN 319 142 (PAdES)</td><td>Digital signature profiles B-B / B-T / B-LT</td></tr>
    </tbody>
</table>
<p style="margin-top: 10pt;">Every profile above builds on the same <code>Document</code> API from Chapter 2 &mdash; no separate SDK, extension, or configuration file to learn.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section33);

// ── Chapter 4 — Operations ───────────────────────────────────────────────
$doc->addPage();
$doc->bookmark('4. Operations', level: 0);
$chapter4Intro = <<<'HTML'
<p class="np-h2">4. Operations</p>
<p>Each document instance is self-contained, so NextPDF renders concurrently across queue workers and serverless handlers without shared mutable state.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $chapter4Intro);

$doc->bookmark('4.1 Deployment Pipeline', level: 1);
$section41 = <<<'HTML'
<p class="np-h3">4.1 Deployment Pipeline</p>
<p>A typical production pipeline composes the document, validates its structure, applies a digital signature, and delivers the finished file:</p>
<p class="np-pipeline">Compose → Validate → Sign → Deliver</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section41);

$doc->bookmark('4.2 Monitoring & Support SLA', level: 1);
$section42 = <<<'HTML'
<p class="np-h3">4.2 Monitoring &amp; Support SLA</p>
<p>Priority support plans include a 4-hour response SLA and direct engineering escalation for regulated production workloads.</p>
<p style="margin-top: 10pt;">Together, deterministic rendering, worker-safe isolation, and a monitored SLA are why regulated pipelines standardize on NextPDF for document delivery that cannot drift between runs.</p>
HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section42);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/handbook.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceTypography specimen

Produced byphp tools/render-samples/scripts/typography.php

tools/render-samples/scripts/typography.php
<?php

declare(strict_types=1);

/**
 * Sample: Type Specimen (NextPDF Core, PDF 2.0).
 *
 * Rebuilt onto the shared SampleTemplate. This is the ONE sample that
 * legitimately exercises every vendored face — weight/style grid, display
 * weights, monospace, Unicode script coverage (Latin Extended, Greek,
 * Cyrillic), the design-system type scale, measure/line-length, the
 * alignment quartet, letter-spacing + case transforms, and a tracking
 * (character-spacing) demonstration on kerning-sensitive letter pairs.
 *
 * Typography::probeAllFaces() is used (rather than the lazy
 * SampleTemplate::registerFaces()) because every vendored face is actually
 * painted on these two pages — the docblock on probeAllFaces() sanctions
 * this specimen as the one caller.
 *
 * ENGINE GOTCHA (verified by texttrace probe, not fixed here — lib/ is
 * read-only for this sample): NextPDF\Html\FontResolver::resolveFontFamily()
 * strips every non-alphanumeric character from a CSS font-family declaration
 * (`preg_replace('/[^a-z0-9]/', '', $normalized)`), so `font-family:
 * 'Barlow-Medium'` normalizes to the probe key "barlowmedium" — but the raw
 * API (`setFont('Barlow-Medium', ...)`) and Typography::fontKey() key the
 * SAME face as "barlow-medium" (hyphen kept). The CSS route therefore never
 * matches the registered face and resolveFontFamily() returns ''; a probe
 * render confirmed the painted glyphs come out as an EMPTY-font-name span
 * (an anonymous serif substitute), not a document-default fallback — the
 * "#1 defect class" this template's font rules warn about. Barlow Medium is
 * painted here via the raw text API (setFont + cell), never via
 * writeHtml() CSS, to avoid it; texttrace on the final PDF confirms zero
 * empty-font-name spans and all nine vendored faces genuinely painted.
 *
 * @see NextPDF\Samples\Typography::probeAllFaces()
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\PrintPalette;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Samples\Typography;

$spec = new SampleSpec(
    id: 'typography',
    title: 'Type Specimen',
    description: 'A complete type specimen for the NextPDF vendored font system: the Inter weight and style grid, Barlow display weights, JetBrains Mono, Latin Extended / Greek / Cyrillic Unicode coverage, the design-system type scale, measure and alignment studies, and letter-spacing control.',
    capability: 'Typography & font embedding',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);

// Every vendored face is painted across these two pages — the specimen is
// the one sanctioned caller of the eager probe (see Typography::probeAllFaces()).
Typography::probeAllFaces($doc);

$css = <<<'CSS'
    <style>
    .np-h2 { margin: 7pt 0 3pt 0; }
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 1.5pt 0; }
    p.np-lede { color: #6B7280; font-size: 8.5pt; margin: 0 0 4pt 0; }
    table.np-specimen { width: 100%; border-collapse: collapse; margin-top: 1pt; }
    table.np-specimen td { border-bottom: 0.4px solid #D1D5DB; padding: 2.5pt 6pt; vertical-align: middle; color: #111827; }
    table.np-specimen td.np-specimen-label { width: 22%; color: #6B7280; font-size: 8pt; }
    table.np-scale td { padding: 1pt 6pt; }
    table.np-measure { width: 100%; border-collapse: collapse; margin: 0 0 6pt 0; }
    td.np-measure-col { border-right: 0.4px solid #D1D5DB; padding: 0 10pt 0 0; font-size: 9pt; line-height: 1.3; color: #111827; }
    p.np-script-sample { font-size: 11pt; margin: 0 0 4pt 0; color: #111827; }
    p.np-align-sample { font-size: 9pt; line-height: 1.25; margin: 0 0 2pt 0; color: #111827; border-bottom: 0.4px solid #D1D5DB; padding-bottom: 3pt; }
    </style>
    CSS;

// ── Section 1: Inter weight & style grid ────────────────────────────────
$weightGrid = [
    ['label' => 'Regular', 'weight' => 'normal', 'style' => 'normal', 'text' => 'The quick brown fox jumps over the lazy dog.'],
    ['label' => 'Bold', 'weight' => 'bold', 'style' => 'normal', 'text' => 'Pack my box with five dozen liquor jugs.'],
    ['label' => 'Italic', 'weight' => 'normal', 'style' => 'italic', 'text' => 'How vexingly quick daft zebras jump!'],
    ['label' => 'Bold Italic', 'weight' => 'bold', 'style' => 'italic', 'text' => 'Waltz, bad nymph, for quick jigs vex.'],
];
$weightRows = '';
foreach ($weightGrid as $row) {
    $weightRows .= '<tr>'
        . '<td class="np-specimen-label">' . \htmlspecialchars($row['label'], ENT_QUOTES) . '</td>'
        . '<td style="font-family: \'Inter\'; font-weight: ' . $row['weight'] . '; font-style: ' . $row['style'] . '; font-size: 13pt;">'
        . \htmlspecialchars($row['text'], ENT_QUOTES) . '</td>'
        . '</tr>';
}

$section1 = <<<HTML
    <p class="np-h2">1. Weight &amp; Style — Inter</p>
    <table class="np-specimen">{$weightRows}</table>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section1);

// ── Section 2: Barlow display weights (raw API — see engine-gotcha note above) + JetBrains Mono ──
$doc->writeHtml(
    SampleTemplate::sharedCss() . $css
    . '<p class="np-h2">2. Display &amp; Monospace — Barlow &amp; JetBrains Mono</p>',
);

$barlowRows = [
    ['label' => 'Regular', 'family' => 'Barlow', 'style' => '', 'text' => 'Precision typesetting, engineered.'],
    ['label' => 'Medium', 'family' => 'Barlow-Medium', 'style' => '', 'text' => 'Precision typesetting, engineered.'],
    ['label' => 'Bold', 'family' => 'Barlow', 'style' => 'B', 'text' => 'Precision typesetting, engineered.'],
];

// Label + specimen columns (22% / 78%, mirroring table.np-specimen's
// td.np-specimen-label split) so each weight is named, not merely implied
// by visual heft. A hairline under every row but the last mirrors the
// section-1 HTML specimen table's row rule.
$margins = $doc->getMargins();
$labelWidth = ($doc->getPageWidth() - $margins->left - $margins->right) * 0.22;
$doc->setDrawColor(...PrintPalette::rgb(PrintPalette::HAIRLINE));
$doc->setLineWidth(0.4);
foreach ($barlowRows as $i => $row) {
    $border = $i === \count($barlowRows) - 1 ? false : 'B';
    $doc->setFont(Typography::FAMILY_TEXT, '', 8.0);
    $doc->setTextColor(...PrintPalette::rgb(PrintPalette::MUTED));
    $doc->cell($labelWidth, 16.0, $row['label'], border: $border, newLine: false);
    $doc->setFont($row['family'], $row['style'], 13.0);
    $doc->setTextColor(...PrintPalette::rgb(PrintPalette::INK));
    $doc->cell(0, 16.0, $row['text'], border: $border, newLine: true);
}
$doc->ln(4.0);
$doc->setFont(Typography::FAMILY_TEXT, '', Typography::SIZE_BODY);

$monoBlock = <<<'HTML'
    <p style="font-family: 'JetBrainsMono'; font-size: 8pt; margin: 0; color: #111827;">function render(): PdfBytes { <span style="font-weight: bold;">return $doc-&gt;save();</span> }</p>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $monoBlock);

// ── Section 3: Unicode script coverage ───────────────────────────────────
$section3 = <<<HTML
    <p class="np-h2">3. Unicode Script Coverage</p>
    <p class="np-eyebrow">LATIN EXTENDED &mdash; GERMAN PANGRAM</p>
    <p class="np-script-sample">Zwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich.</p>
    <p class="np-eyebrow">GREEK &mdash; PANGRAM</p>
    <p class="np-script-sample">Ξεσκεπάζω την ψυχοφθόρα βδελυγμία.</p>
    <p class="np-eyebrow">CYRILLIC &mdash; RUSSIAN PANGRAM</p>
    <p class="np-script-sample" style="margin-bottom: 0;">Съешь же ещё этих мягких французских булок, да выпей чаю.</p>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section3);

// ── Section 4: Type scale ladder ─────────────────────────────────────────
$scale = [
    ['pt' => Typography::SIZE_TITLE, 'role' => 'Title — Barlow Bold', 'family' => 'Barlow', 'weight' => 'bold'],
    ['pt' => Typography::SIZE_SECTION, 'role' => 'Section — Barlow Bold', 'family' => 'Barlow', 'weight' => 'bold'],
    ['pt' => Typography::SIZE_SUBSECTION, 'role' => 'Subsection — Barlow Bold', 'family' => 'Barlow', 'weight' => 'bold'],
    ['pt' => Typography::SIZE_BODY, 'role' => 'Body — Inter Regular, 1.45 leading', 'family' => 'Inter', 'weight' => 'normal'],
    ['pt' => Typography::SIZE_TABLE, 'role' => 'Table — Inter Regular', 'family' => 'Inter', 'weight' => 'normal'],
    ['pt' => Typography::SIZE_MONO, 'role' => 'Mono — JetBrains Mono', 'family' => 'JetBrainsMono', 'weight' => 'normal'],
    ['pt' => Typography::SIZE_FOOTER, 'role' => 'Footer — Inter Regular', 'family' => 'Inter', 'weight' => 'normal'],
];
$scaleRows = '';
foreach ($scale as $step) {
    $ptLabel = \rtrim(\rtrim(\sprintf('%.1F', $step['pt']), '0'), '.');
    $scaleRows .= '<tr>'
        . '<td class="np-num" style="width: 12%;">' . $ptLabel . '&nbsp;pt</td>'
        . '<td style="width: 32%; color: #6B7280; font-size: 8pt;">' . \htmlspecialchars($step['role'], ENT_QUOTES) . '</td>'
        . '<td style="font-family: \'' . $step['family'] . '\'; font-weight: ' . $step['weight'] . '; font-size: ' . $step['pt'] . 'pt;">Aa Bb Cc 0123</td>'
        . '</tr>';
}

$section4 = <<<HTML
    <p class="np-h2">4. Type Scale</p>
    <table class="np-table np-scale">{$scaleRows}</table>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section4);

// ═══════════════════════════════════════════════════════════════════════
// Page 2 — measure, alignment, tracking
// ═══════════════════════════════════════════════════════════════════════
$doc->addPage();

$measureText = 'A comfortable measure keeps the reader&rsquo;s eye anchored on the line. '
    . 'Narrower columns break more often and slow the cadence; wider columns flow '
    . 'faster but demand more from the eye travelling back to the next line start.';

$measureCols = [
    ['label' => 'Full measure — 100%', 'width' => '100%'],
    ['label' => '120 mm column — 66.7%', 'width' => '66.7%'],
    ['label' => '90 mm column — 50%', 'width' => '50%'],
];
$measureBlocks = '';
foreach ($measureCols as $col) {
    $measureBlocks .= '<p class="np-eyebrow">' . \htmlspecialchars($col['label'], ENT_QUOTES) . '</p>'
        . '<table class="np-measure"><tr><td class="np-measure-col" style="width: ' . $col['width'] . ';">'
        . $measureText . '</td>' . ($col['width'] === '100%' ? '' : '<td></td>') . '</tr></table>';
}

$section5 = <<<HTML
    <p class="np-h2">5. Measure &amp; Line Length</p>
    {$measureBlocks}
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section5);

// ── Section 6: alignment quartet ─────────────────────────────────────────
$alignText = 'NextPDF composes documents with the same care a typographer brings to '
    . 'print, while developers keep the simplicity of a single, predictable API.';
$aligns = [
    ['label' => 'LEFT', 'align' => 'left'],
    ['label' => 'CENTER', 'align' => 'center'],
    ['label' => 'RIGHT', 'align' => 'right'],
    ['label' => 'JUSTIFY', 'align' => 'justify'],
];
$alignRows = '';
foreach ($aligns as $i => $a) {
    $isLast = $i === \count($aligns) - 1;
    $alignRows .= '<p class="np-eyebrow">' . $a['label'] . '</p>'
        . '<p class="np-align-sample" style="text-align: ' . $a['align'] . ';' . ($isLast ? ' border-bottom: none; padding-bottom: 0;' : '') . '">'
        . $alignText . '</p>';
}

$section6 = <<<HTML
    <p class="np-h2">6. Alignment Quartet</p>
    {$alignRows}
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section6);

// ── Section 7: letter-spacing & case ─────────────────────────────────────
$caseRows = [
    ['label' => 'Uppercase, wide tracking', 'transform' => 'uppercase', 'spacing' => '1.5pt'],
    ['label' => 'Capitalize, normal tracking', 'transform' => 'capitalize', 'spacing' => '0pt'],
    ['label' => 'Lowercase, tight tracking', 'transform' => 'lowercase', 'spacing' => '-0.3pt'],
];
$caseHtmlRows = '';
foreach ($caseRows as $row) {
    $caseHtmlRows .= '<tr>'
        . '<td class="np-specimen-label">' . \htmlspecialchars($row['label'], ENT_QUOTES) . '</td>'
        . '<td style="font-size: 11pt; text-transform: ' . $row['transform'] . '; letter-spacing: ' . $row['spacing'] . ';">NextPDF Type System</td>'
        . '</tr>';
}

$section7 = <<<HTML
    <p class="np-h2">7. Letter-Spacing &amp; Case</p>
    <table class="np-specimen">{$caseHtmlRows}</table>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section7);

// ── Section 8: tracking on kerning-sensitive pairs ───────────────────────
$kernText = 'AVIATION WATER TOTAL WAVY TASTE';
$section8 = <<<HTML
    <p class="np-h2">8. Kerning-Sensitive Pairs — Tracking Off / On</p>
    <p class="np-lede">Character-spacing (Tc) control applied to letter pairs — AV, WA, TO, TA — that
    read loosest at zero tracking. This is manual tracking, not automatic glyph-pair kerning.</p>
    <table class="np-specimen">
        <tr><td class="np-specimen-label">0&nbsp;pt tracking (off)</td>
            <td style="font-family: 'Inter'; font-weight: bold; font-size: 13pt; letter-spacing: 0pt;">{$kernText}</td></tr>
        <tr><td class="np-specimen-label" style="color: #8A5A00;">&minus;0.4&nbsp;pt tracking (on)</td>
            <td style="font-family: 'Inter'; font-weight: bold; font-size: 13pt; letter-spacing: -0.4pt; color: #8A5A00;">{$kernText}</td></tr>
    </table>
    HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $section8);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/typography.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceAnalytics charts

Produced byphp tools/render-samples/scripts/charts.php

tools/render-samples/scripts/charts.php
<?php

declare(strict_types=1);

/**
 * Sample: Analytics Charts Dashboard (NextPDF Pro, PDF 2.0).
 *
 * A two-page analytics dashboard rebuilt onto the shared SampleTemplate:
 * branded masthead + a KPI stat row, then three charts drawn through the
 * Pro chart module's document-integrated renderTo() seam — BarChart (render
 * volume by document profile), LineChart (latency trend, two series), and
 * PieChart (edition mix by rendered volume). Every chart label is painted
 * via the engine's public drawing surface (Document::text()) in the
 * caller's own registered/embedded font, never the legacy raw-stream
 * `/ChartFont` path.
 *
 * @see NextPDF\Pro\Chart\BarChart::renderTo()
 * @see NextPDF\Pro\Chart\LineChart::renderTo()
 * @see NextPDF\Pro\Chart\PieChart::renderTo()
 * @see NextPDF\Pro\Chart\ChartCanvas
 */

require __DIR__ . '/../lib/bootstrap-premium.php';

use NextPDF\Pro\Chart\BarChart;
use NextPDF\Pro\Chart\ChartBox;
use NextPDF\Pro\Chart\ChartColor;
use NextPDF\Pro\Chart\LineChart;
use NextPDF\Pro\Chart\PieChart;
use NextPDF\Samples\Edition;
use NextPDF\Samples\PrintPalette;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'charts',
    title: 'Analytics Dashboard',
    description: 'A Pro-tier analytics dashboard combining a KPI stat row with bar, line, and pie charts rendered through the document-integrated chart renderer.',
    capability: 'Pro chart renderer (bar / line / pie)',
    edition: Edition::Pro,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong>/bold KPI values + insight highlights

$leftX = SampleTemplate::mm(SampleTemplate::MARGIN_LEFT_MM);
$rightX = $doc->getPageWidth() - SampleTemplate::mm(SampleTemplate::MARGIN_RIGHT_MM);
$bodyWidth = $rightX - $leftX;

// Shared sample-specific CSS (in addition to SampleTemplate::sharedCss()).
// Re-declared on every writeHtml() call below: each call parses an
// independent HTML fragment, so <style> rules do not carry across calls.
$css = <<<'CSS'
    <style>
    p.np-lede { color: #6B7280; font-weight: normal; font-size: 9.5pt; margin: 0 0 4pt 0; }
    p.np-legend { color: #374151; font-weight: normal; font-size: 8.5pt; margin: 0 0 2pt 0; }
    p.np-caption { color: #6B7280; font-weight: normal; font-size: 8pt; margin: 0; }
    ul.np-insights { margin: 4pt 0 0 0; padding-left: 14pt; }
    ul.np-insights li { color: #111827; font-weight: normal; font-size: 9.5pt; line-height: 1.5; margin: 0 0 10pt 0; }
    </style>
    CSS;

// ── Page 1: KPI row + render volume by document profile ──────────────
$kpiRow = <<<HTML
    <p class="np-h2">Q2 2026 rendering telemetry</p>
    <p class="np-lede">Live engine metrics across every document profile, drawn directly through the Pro chart API &mdash; no external charting library.</p>
    <table cellpadding="0" cellspacing="0" style="width: 100%; margin-top: 8pt;"><tr>
        <td style="width: 25%; padding: 10pt 16pt 0 0; border-top: 1.5pt solid #1E3A8A;">
            <p style="color: #1E3A8A; font-weight: bold; font-size: 18pt; margin: 0 0 2pt 0;">4.2M</p>
            <p style="color: #6B7280; font-weight: normal; font-size: 7pt; letter-spacing: 0.4pt; margin: 0;">DOCUMENTS RENDERED</p>
        </td>
        <td style="width: 25%; padding: 10pt 16pt 0 0; border-top: 1.5pt solid #1E3A8A;">
            <p style="color: #1E3A8A; font-weight: bold; font-size: 18pt; margin: 0 0 2pt 0;">99.98%</p>
            <p style="color: #6B7280; font-weight: normal; font-size: 7pt; letter-spacing: 0.4pt; margin: 0;">SUCCESS RATE</p>
        </td>
        <td style="width: 25%; padding: 10pt 16pt 0 0; border-top: 1.5pt solid #1E3A8A;">
            <p style="color: #1E3A8A; font-weight: bold; font-size: 18pt; margin: 0 0 2pt 0;">52 ms</p>
            <p style="color: #6B7280; font-weight: normal; font-size: 7pt; letter-spacing: 0.4pt; margin: 0;">MEDIAN LATENCY</p>
        </td>
        <td style="width: 25%; padding: 10pt 0 0 0; border-top: 1.5pt solid #1E3A8A;">
            <p style="color: #1E3A8A; font-weight: bold; font-size: 18pt; margin: 0 0 2pt 0;">3</p>
            <p style="color: #6B7280; font-weight: normal; font-size: 7pt; letter-spacing: 0.4pt; margin: 0;">EDITIONS IN PRODUCTION</p>
        </td>
    </tr></table>
    HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $kpiRow);

$barIntro = <<<HTML
    <p class="np-h2" style="margin-top: 14pt;">Render volume by document profile</p>
    <p class="np-lede">Documents rendered per profile this quarter, aggregated from production render telemetry (thousands).</p>
    HTML;
$doc->setY($doc->getY() + 6.0);
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $barIntro);

$barBoxY = $doc->getY() + 8.0;
$barBoxHeight = 330.0;
$doc->setFont('Inter', '', 8.0); // chart labels paint in the caller's CURRENT font
BarChart::fromData(
    ['Invoices', 'Reports', 'Labels', 'Archival'],
    [1_840_200, 986_540, 742_310, 631_070],
)
    ->withBarColor(ChartColor::hex(PrintPalette::PRISM_900))
    ->withAxisColor(ChartColor::hex(PrintPalette::MUTED))
    ->withBarGap(0.35)
    ->withFontSize(7.5)
    ->renderTo($doc, new ChartBox($leftX, $barBoxY, $bodyWidth, $barBoxHeight));

$doc->setY($barBoxY + $barBoxHeight + 14.0);
$doc->writeHtml(SampleTemplate::sharedCss() . $css
    . '<p class="np-caption">Figures are illustrative sample data. All four profiles cleared 99.9%+ render success this quarter.</p>');

// ── Page 2: latency trend + edition mix ───────────────────────────────
$doc->addPage();

$lineIntro = <<<HTML
    <p class="np-h2">Latency trend, this quarter vs. last</p>
    <p class="np-lede">Median render latency by week, current quarter compared with the prior quarter (milliseconds).</p>
    <p class="np-legend"><span style="color: #1E3A8A;">&#9632;</span> This quarter &nbsp;&nbsp; <span style="color: #D97706;">&#9632;</span> Last quarter</p>
    HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $lineIntro);

$lineBoxY = $doc->getY() + 6.0;
$lineBoxHeight = 175.0;
$doc->setFont('Inter', '', 8.0);
LineChart::fromData(
    ['Wk1', 'Wk2', 'Wk3', 'Wk4', 'Wk5', 'Wk6', 'Wk7', 'Wk8'],
    [64, 62, 60, 59, 57, 55, 53, 52],
)
    ->addSeries('Last quarter', [72, 71, 70, 69, 68, 66, 65, 63], ChartColor::hex(PrintPalette::CHIP_PRO))
    ->withAxisColor(ChartColor::hex(PrintPalette::MUTED))
    ->withDots(true)
    ->withGrid(true)
    ->withFontSize(7.5)
    ->renderTo($doc, new ChartBox($leftX, $lineBoxY, $bodyWidth, $lineBoxHeight));

$doc->setY($lineBoxY + $lineBoxHeight + 20.0);

$pieWidth = 340.0;
$pieHeight = 190.0;
$pieX = $leftX + ($bodyWidth - $pieWidth) / 2;

$pieIntro = <<<HTML
    <p class="np-h2">Edition mix by rendered volume</p>
    <p class="np-lede">Share of this quarter's rendered documents by NextPDF edition.</p>
    HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $pieIntro);

$pieBoxY = $doc->getY() + 4.0;

$doc->setFont('Inter', '', 7.5);
PieChart::fromData(['Core', 'Pro', 'Enterprise'], [58, 27, 15])
    ->withColors([
        ChartColor::hex(PrintPalette::CHIP_CORE),
        ChartColor::hex(PrintPalette::CHIP_PRO),
        ChartColor::hex(PrintPalette::CHIP_ENTERPRISE),
    ])
    ->withPercentages(true)
    ->withLegend(true)
    ->withFontSize(7.5)
    ->renderTo($doc, new ChartBox($pieX, $pieBoxY, $pieWidth, $pieHeight));

$doc->setY($pieBoxY + $pieHeight + 16.0);

$insights = <<<HTML
    <p class="np-h3" style="margin-top: 0;">Key findings</p>
    <ul class="np-insights">
        <li><strong style="color: #111827;">Enterprise</strong><span style="font-weight: normal; color: #111827;"> adoption grew to 15% of rendered volume as regulated workloads adopt PDF/A archival output.</span></li>
        <li><strong style="color: #111827;">Median latency</strong><span style="font-weight: normal; color: #111827;"> fell to 52&nbsp;ms this quarter, a 14% improvement over the prior quarter.</span></li>
        <li><strong style="color: #111827;">Reports &amp; dashboards</strong><span style="font-weight: normal; color: #111827;"> is now the second-largest profile by volume, trailing only invoices &amp; statements.</span></li>
    </ul>
    HTML;
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $insights);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/charts.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceAccessible tagged report (PDF/UA-2)

Produced byphp tools/render-samples/scripts/accessible-report.php

tools/render-samples/scripts/accessible-report.php
<?php

declare(strict_types=1);

/**
 * Sample: Accessible Tagged Report (NextPDF Core, PDF/UA-2).
 *
 * Rebuilt onto the shared SampleTemplate, but with a template variant this
 * one sample family member needs: ISO 14289-2:2024 (PDF/UA-2) tagged PDF is
 * enabled via Document::enableTaggedPdf() BEFORE apply()/addPage(). Once a
 * structure tree exists, NextPDF\Core\Concerns\HasPages suppresses the
 * template's header/footer CALLBACKS outright — raw content-stream chrome
 * painted outside the structure tree would be untagged content, a PDF/UA-2
 * §8.2.2 violation — so the band, brand mark, spec table, and footer are
 * repainted here as real BODY content through paths the engine itself
 * proves safe under tagging:
 *   - decorative bars/chips: a plain HTML block with `aria-hidden="true"`,
 *     which NextPDF\Accessibility\TaggedContentEmitter routes to a real
 *     `/Artifact` marked-content span (WAI-ARIA 1.2 §6.6.6 / ISO 14289-2 §7.8)
 *     — confirmed by reading HtmlToStructureMap::isAriaHidden() and its call
 *     site in TaggedContentEmitter::openTag().
 *   - the footer brand/URL/page-marker text: Document::text(), which
 *     auto-wraps its payload in a fresh <P> StructElem when tagging is
 *     active (HasTextOutput::text(), confirmed by reading the method body).
 * The genuinely real content — headings, prose, lists, and the two data
 * tables — flows through writeHtml() so the HTML→StructureTree pipeline
 * tags it properly (H1 > H2 > H3, L/LI, TABLE/TR/TH/TD).
 *
 * KNOWN ENGINE GAP (reported, not worked around by editing lib/engine code):
 * there is no path in this engine release that BOTH paints a real image AND
 * carries a screen-reader alternative. Document::image()/imageSvg() paint
 * pixels but expose no /Alt (image() is explicitly documented as always
 * decorative-Artifact); the HTML `<img alt="...">` path tags a /Figure with
 * /Alt correctly but Document::writeHtml() never wires an image-resource
 * resolver for plain `<img>` (only `background-image`/`mask-image` get one —
 * confirmed by reading HasTextOutput's bindHtmlParser*Resolver methods and by
 * an empirical smoke render: an `<img>` with a valid relative src reserves
 * layout space but paints no pixels). Shipping a `<Figure>` that is
 * deliberately a blank box would misrepresent the sample, so this report
 * demonstrates every OTHER accessibility feature (heading hierarchy, lists,
 * header-cell tables) and is honest in its own checklist (page 2) that it
 * carries no images.
 *
 * @see NextPDF\Samples\SampleTemplate
 * @see NextPDF\Core\Concerns\HasSecurity::enableTaggedPdf()
 * @see NextPDF\Core\Concerns\HasPages (header/footer suppression under tagging)
 * @see NextPDF\Accessibility\TaggedContentEmitter::openTag() (aria-hidden routing)
 * @see NextPDF\Core\Concerns\HasTextOutput::text() (auto <P> wrapping)
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\PrintPalette;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Samples\Typography;
use NextPDF\ValueObjects\Margin;

$spec = new SampleSpec(
    id: 'accessible-report',
    title: 'Accessible Tagged Report',
    description: 'A screen-reader-navigable report demonstrating NextPDF\'s PDF/UA-2 (ISO 14289-2:2024) tagged-PDF pipeline: a logical H1-H2-H3 heading tree, semantic lists, and header-cell data tables.',
    capability: 'Tagged PDF / structure tree',
    edition: Edition::Core,
    standards: ['ISO 32000-2', 'ISO 14289-2 (PDF/UA-2)'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);

// enableTaggedPdf() MUST run before apply()/addPage(): the HTML pipeline
// only wires the TaggedContentEmitter when a StructureTree root already
// exists at writeHtml() time.
$doc->enableTaggedPdf('en');
$doc->setLanguage('en');

$template->apply($doc);

// This sample's template VARIANT ("body-furniture"): the band needs to reach
// the physical page edges as real body content (the template's stock 26mm
// top margin, and its left/right margins, exist to clear a header CALLBACK
// band that never fires once tagged — see file docblock). ALL four margins
// are pulled to 0: an empirically confirmed engine constraint is that a
// block's CSS width/position is clamped to the page's margin box regardless
// of an explicit `width`/negative `margin`, so true full-bleed decoration is
// only reachable with zero document margins. Every real (non-decorative)
// element is then wrapped in a `padding-left/right` container below that
// reproduces the template's usual 15mm reading margin — bottom stays at the
// template's own auto-page-break constant for footer clearance.
$doc->setMargins(new Margin(
    top: 0.0,
    right: 0.0,
    bottom: SampleTemplate::mm(SampleTemplate::PAGE_BREAK_MM),
    left: 0.0,
));

$doc->addPage();

// <strong> emphasis paints Inter-B; the real <h1>/<h2>/<h3> headings paint
// Barlow-B (sharedCss() maps heading elements to the Barlow family); inline
// `.np-mono2` spans (StructElem/H1/TH/etc. and the verapdf command line)
// paint JetBrainsMono regular. A face only referenced from CSS, never
// touched by setFont(), silently falls back to a substitute serif (FONT
// RULES), so all three are pre-registered here even though the masthead
// title in this sample is real markup, not a raw Document::text() call.
$template->registerFaces($doc, [['Inter', 'B'], ['Barlow', 'B'], ['JetBrainsMono', '']]);

// ---- Geometry shared by both pages (points) ---------------------------
//
// ENGINE CONSTRAINT (empirically confirmed, not fixed by editing lib/engine
// code): HtmlBlockHandler has no support for an explicit CSS `height` on a
// block — a declared `height` is silently ignored and the box shrinks to its
// content's line-box. Padding IS honoured, but a background paints only the
// box's content + padding-BOTTOM, not padding-TOP (its top edge sits at the
// content's own top, one padding-top's-worth below the box's outer edge).
// Every fixed-height decorative fill below is therefore built the same way:
// a near-zero content line (`font-size: 1pt; line-height: 0`) plus
// `padding-top: 0` and `padding-bottom: <target height>`, which an empirical
// probe (get_drawings() on a smoke render) confirmed paints a rect starting
// at the box's own top edge with the declared height (+/- ~1.2pt from the
// residual near-zero line box — comfortably inside every ST1/ST2 tolerance
// used below).
$bandFirstPt = SampleTemplate::mm(SampleTemplate::BAND_FIRST_MM);
$bandContPt = SampleTemplate::mm(SampleTemplate::BAND_CONTINUATION_MM);
$logoSizePt = SampleTemplate::mm(SampleTemplate::MASTHEAD_LOGO_MM);
$leftPt = SampleTemplate::mm(SampleTemplate::MARGIN_LEFT_MM);
$rightPt = SampleTemplate::mm(SampleTemplate::MARGIN_RIGHT_MM);
$prism = PrintPalette::PRISM_900;
$muted = PrintPalette::MUTED;
$ink = PrintPalette::INK;

$fillBar = static fn (float $heightPt, string $hex, string $extra = ''): string => \sprintf(
    '<div aria-hidden="true" style="width: 100%%; background-color: %s; font-size: 1pt; '
        . 'line-height: 0; padding-top: 0; padding-bottom: %.3Fpt; %s">&nbsp;</div>',
    $hex,
    $heightPt,
    $extra,
);

$cells = $spec->specTableCells();

// Inline mono style for text inside table cells: the engine does not apply
// class rules to elements nested in table cells (sharedCss() engine notes),
// so .np-mono2 cannot be used there — the same JetBrainsMono face is stated
// inline instead.
$monoCell = "font-family: 'JetBrainsMono'; font-size: 8pt;";

// ---- Page 1 -------------------------------------------------------------
$specTableRows = '';
foreach ($cells['labels'] as $i => $label) {
    $specTableRows .= '<th style="text-align:left; color: ' . $muted . '; font-size: 6.5pt; '
        . 'letter-spacing: 0.3pt; font-weight: normal; padding: 0 10pt 3pt 0; border-bottom: 0.6pt solid ' . PrintPalette::HAIRLINE . ';">'
        . \strtoupper(\htmlspecialchars($label, ENT_QUOTES)) . '</th>';
}
$specTableValues = '';
foreach ($cells['values'] as $value) {
    $specTableValues .= '<td style="color: ' . $ink . '; font-size: 8.5pt; padding: 4pt 10pt 0 0;">'
        . \htmlspecialchars($value, ENT_QUOTES) . '</td>';
}

$band1 = $fillBar($bandFirstPt, $prism);
// The template family's page-1 signature: a 1 mm amber rule directly below
// the prism band (SampleTemplate::BAND_RULE_MM). The fillBar residual line
// box adds ~1.2 pt, so the target height is pre-compensated to keep the
// painted rule at the template's true 1 mm weight (single amber accent,
// use 1 of 2 on this page).
$rule1 = $fillBar(
    \max(0.0, SampleTemplate::mm(SampleTemplate::BAND_RULE_MM) - 1.2),
    PrintPalette::ACCENT,
);
$logoChip = $fillBar($logoSizePt, $prism, 'width: ' . \sprintf('%.3F', $logoSizePt) . 'pt; margin-top: 24pt; border-radius: 3pt;');

$page1 = <<<HTML
{$band1}
{$rule1}
<div style="padding-left: {$leftPt}pt; padding-right: {$rightPt}pt;">
{$logoChip}
<h1 style="margin-top: 14pt;">Accessible Tagged Report</h1>
<table style="width: 100%; border-collapse: collapse; margin-top: 10pt;"><tr>{$specTableRows}</tr><tr>{$specTableValues}</tr></table>
<p style="margin-top: 18pt;">This report is generated with <strong>Document::enableTaggedPdf()</strong>
&mdash; NextPDF's opt-in path to ISO 14289-2:2024 (PDF/UA-2). Every heading, paragraph, list, and
table below is a real <span class="np-mono2">StructElem</span> in the file's logical structure
tree, wired with stable MCIDs, so a screen reader, a refreshable braille display, or any other
assistive technology can traverse the document in its true reading order &mdash; independent of
the visual column, table, or page layout on screen.</p>
<h2>Why Structure Matters</h2>
<p>A page is not a grid of pixels to an assistive technology &mdash; it is a tree. Sighted readers
skim a page by its visual hierarchy: a bold headline, an indented list, a ruled table. A screen
reader has no visual hierarchy to skim &mdash; it has only the structure tree the file declares.
Without one, a PDF is, in practice, an opaque image of text.</p>
<h3>What NextPDF Emits for This Page</h3>
<ul>
    <li>Headings (<span class="np-mono2">H1</span>&ndash;<span class="np-mono2">H3</span>) with no
    level skipped, so the outline a screen reader announces matches the one printed here.</li>
    <li>Paragraphs (<span class="np-mono2">P</span>) carrying the reading order shown on the page.</li>
    <li>This list itself, as <span class="np-mono2">L</span> with <span class="np-mono2">LI</span>
    children &mdash; announced as a list of three items, not three stray sentences.</li>
    <li>Data tables with real header cells (<span class="np-mono2">TH</span>), demonstrated by
    the spec panel above and the page-2 checklist.</li>
</ul>
<h2>Who This Helps</h2>
<p>Tagged structure is what makes a PDF usable with a screen reader (JAWS, NVDA, VoiceOver), a
refreshable braille display, or a text-to-speech tool &mdash; and it is what lets a sighted reader
reflow the same content to a phone-sized column without losing the outline. It is also, in most
jurisdictions that require it, the difference between a document that satisfies a legal
accessibility obligation (the EU's EN 301 549, the US Section 508, WCAG 2.2) and one that only
looks like it does.</p>
<h2>What This File Declares</h2>
<p>Beyond the visible content, <span class="np-mono2">enableTaggedPdf()</span> writes the
machine-readable conformance markers a validator inspects first &mdash; each row below is
present in this very file:</p>
<table class="np-table" style="margin-top: 8pt;">
    <thead>
        <tr>
            <th style="width: 30%;">Marker</th>
            <th style="width: 26%;">Value in this file</th>
            <th style="width: 44%;">What it tells a validator</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><span style="{$monoCell}">/MarkInfo</span></td>
            <td><span style="{$monoCell}">/Marked true</span></td>
            <td>The document claims a complete structure tree</td>
        </tr>
        <tr class="np-zebra">
            <td><span style="{$monoCell}">/StructTreeRoot</span></td>
            <td>present</td>
            <td>Root of the logical structure an assistive technology walks</td>
        </tr>
        <tr>
            <td><span style="{$monoCell}">Catalog /Lang</span></td>
            <td><span style="{$monoCell}">en</span></td>
            <td>Validated BCP 47 language for speech synthesis</td>
        </tr>
        <tr class="np-zebra">
            <td><span style="{$monoCell}">XMP pdfuaid:part / :rev</span></td>
            <td><span style="{$monoCell}">2 / 2024</span></td>
            <td>The file self-identifies as PDF/UA-2 (ISO 14289-2:2024)</td>
        </tr>
    </tbody>
</table>
</div>
HTML;

$css = <<<CSS
    <style>
    p { font-weight: normal; color: {$ink}; }
    li { font-weight: normal; color: {$ink}; }
    .np-mono2 { font-family: 'JetBrainsMono'; font-size: 8pt; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $page1);

writeFooter($doc, 1, 2);

// ---- Page 2 (continuation) ----------------------------------------------
$doc->addPage();

$checklistRows = [
    ['Document title & language', 'Catalog /Lang and the Info dictionary Title resolve to a real, validated BCP 47 tag', 'Met'],
    ['Logical structure tree', 'Every heading, paragraph, list, and table maps to a StructElem with a stable MCID', 'Met'],
    ['Heading order', 'H1 precedes H2 precedes H3 on page 1, with no level skipped', 'Met'],
    ['Data table headers', 'Column headers are real TH structure elements, not styled TD cells', 'Met'],
    ['Reading order', 'The content stream is ordered exactly as an assistive technology will announce it', 'Met'],
    ['Image alternative text', 'Every meaningful image carries a text alternative', 'N/A &mdash; no images in this report'],
];
$checklistBody = '';
foreach ($checklistRows as $i => $row) {
    $rowClass = $i % 2 === 1 ? ' class="np-zebra"' : '';
    $checklistBody .= "<tr{$rowClass}>"
        . '<td>' . \htmlspecialchars($row[0], ENT_QUOTES) . '</td>'
        . '<td>' . $row[1] . '</td>'
        . '<td>' . $row[2] . '</td>'
        . '</tr>';
}

$band2 = $fillBar($bandContPt, $prism);

$page2 = <<<HTML
{$band2}
<div style="padding-left: {$leftPt}pt; padding-right: {$rightPt}pt;">
<h2 style="margin-top: 24pt;">PDF/UA-2 Conformance Checklist</h2>
<p>The table below is itself a demonstration: its header row is tagged with real
<span class="np-mono2">TH</span> elements, so an assistive technology announces each column's
label &mdash; Criterion, Requirement, Status &mdash; before reading a row's value, rather than
leaving the reader to guess what a bare number or word refers to.</p>
<table class="np-table" style="margin-top: 10pt;">
    <thead>
        <tr>
            <th style="width: 26%;">Criterion</th>
            <th style="width: 54%;">Requirement</th>
            <th style="width: 20%;">Status</th>
        </tr>
    </thead>
    <tbody>
        {$checklistBody}
    </tbody>
</table>
<div aria-hidden="true" style="font-size: 1pt; line-height: 0; padding-top: 0; padding-bottom: 10pt;">&nbsp;</div>
<h3>Verifying These Claims Yourself</h3>
<p>A vendor's own checklist is a claim, not proof. Point an independent validator at the file
rather than taking this page's word for it:</p>
<ul>
    <li><span class="np-mono2">verapdf --flavour ua2 accessible-report.pdf</span> &mdash; the
    open-source veraPDF reference implementation of the ISO 14289-2 Machine-Checkable
    Requirements.</li>
    <li>The PDF Association's PAC tool, which walks the same structure tree visually, node by
    node.</li>
    <li>Adobe Acrobat's built-in Accessibility Checker, or any screen reader's own reading-order
    inspector.</li>
</ul>
<p>This sample is generated end to end by NextPDF Core &mdash; no post-processing accessibility
remediation pass was applied. What an independent validator sees in the structure tree is exactly
what <span class="np-mono2">Document::writeHtml()</span> produced from the markup on this page.</p>
</div>
HTML;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $page2);

writeFooter($doc, 2, 2);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/accessible-report.pdf';
$doc->save($output);

echo "Created: {$output}\n";

/**
 * Paint the footer as real body content: three Document::text() calls,
 * which HasTextOutput::text() auto-wraps in a fresh <P> StructElem whenever
 * tagging is active (confirmed by reading the method), so the brand mark,
 * site URL, and page marker are real tagged content rather than untagged
 * chrome. The total page count is a literal 2 (both pages() build with an
 * explicit addPage() below, so this is a stated fact about the document
 * being produced, not a guess) — the template's dynamic pageTotalPlaceholder()
 * mechanism exists for the header/footer CALLBACK path, which is unreachable
 * once tagging suppresses callbacks (see file docblock).
 */
function writeFooter(\NextPDF\Core\Document $doc, int $pageNumber, int $totalPages): void
{
    $fontFamily = Typography::FAMILY_TEXT;
    $size = Typography::SIZE_FOOTER;
    $fontKey = Typography::fontKey($fontFamily);

    $doc->setFont($fontFamily, '', $size);
    $doc->setTextColor(...PrintPalette::rgb(PrintPalette::MUTED));

    $pageWidth = $doc->getPageWidth();
    $pageHeight = $doc->getPageHeight();
    $left = \NextPDF\Samples\SampleTemplate::mm(\NextPDF\Samples\SampleTemplate::MARGIN_LEFT_MM);
    $right = $pageWidth - \NextPDF\Samples\SampleTemplate::mm(\NextPDF\Samples\SampleTemplate::MARGIN_RIGHT_MM);
    $y = $pageHeight - \NextPDF\Samples\SampleTemplate::mm(\NextPDF\Samples\SampleTemplate::FOOTER_BASELINE_Y_MM);

    $metrics = $doc->fontMetrics();

    $brand = \NextPDF\Samples\SampleTemplate::FOOTER_BRAND;
    $doc->text($left, $y, $brand);

    $url = \NextPDF\Samples\SampleTemplate::FOOTER_URL;
    $urlWidth = $metrics->getStringWidth($url, $fontKey, $size);
    $urlX = $left + (($right - $left) - $urlWidth) / 2;
    $doc->text($urlX, $y, $url);

    $marker = "{$pageNumber} / {$totalPages}";
    $markerWidth = $metrics->getStringWidth($marker, $fontKey, $size);
    $doc->text($right - $markerWidth, $y, $marker);

    $doc->setTextColor(...PrintPalette::rgb(PrintPalette::INK));
    $doc->setFont(Typography::FAMILY_TEXT, '', Typography::SIZE_BODY);
}
SourceDigital signature (PAdES)

Produced byphp tools/render-samples/scripts/signed-agreement.php

tools/render-samples/scripts/signed-agreement.php
<?php

declare(strict_types=1);

/**
 * Sample: Digitally Signed Agreement (NextPDF Core, PDF 2.0, PAdES B-T).
 *
 * Rebuilt onto the shared SampleTemplate. Drives the REAL Core signing
 * pipeline end to end: Document::setSignature(certInfo, PAdES_B_T, tsaClient)
 * + save() emit a signed PDF whose CMS SignedData (RSASSA-PSS · SHA-256)
 * covers the full /ByteRange and carries one RFC 3161 signature time-stamp
 * as the id-aa-timeStampToken unsigned attribute (ETSI EN 319 142-1 B-T).
 *
 * The time-stamp comes from an in-process demonstration TSA: a PSR-18
 * client that parses the engine's outgoing RFC 3161 TimeStampReq and
 * answers with a TimeStampResp built by the repository's TsaTokenFactory —
 * a genuine CMS token, RSA-signed by the fixture TSA certificate (critical
 * id-kp-timeStamping EKU), echoing the request nonce and SHA-256 imprint.
 * No network I/O. The page states this provenance honestly.
 *
 * After save() the script re-verifies its own output fail-closed with
 * engine APIs: Inspector (signature presence), a ByteRange/messageDigest
 * binding check (ISO 32000-2 §12.8 / RFC 5652 §11.2), token-embedding
 * proof, and TsaClient::verifyToken() (7-step RFC 3161/5652/5816 + EKU).
 *
 * NON-DETERMINISTIC by nature (signing time, CMS/nonce randomness); the
 * visible page paints only spec-pinned dates so the raster stays stable.
 *
 * @see NextPDF\Core\Concerns\HasSecurity::setSignature()
 * @see NextPDF\Security\Signature\PadesOrchestrator
 * @see NextPDF\Security\Timestamp\TsaClient
 * @see NextPDF\Tests\Integration\Security\Timestamp\TsaTokenFactory
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Inspect\InspectConfig;
use NextPDF\Inspect\Inspector;
use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
use NextPDF\Support\Http\Response;
use NextPDF\Support\Http\Stream;
use NextPDF\Tests\Integration\Security\Timestamp\TsaTokenFactory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * In-process demonstration TSA (RFC 3161) as a PSR-18 HTTP client.
 *
 * Parses the TimeStampReq the engine's TsaClient sends (SHA-256
 * messageImprint + nonce), then answers with a TimeStampResp whose
 * TimeStampToken is built by the repository's {@see TsaTokenFactory}:
 * a real CMS SignedData over the imprint, RSA-PKCS#1v1.5/SHA-256-signed
 * by the fixture TSA certificate, with content-type / message-digest /
 * signing-certificate-v2 signed attributes (RFC 5652 §11, RFC 5816 §3)
 * so the strict CmsTimestampEmbedder pre-flight passes. Demonstration
 * quality: honest, verifiable crypto — NOT a trusted time source.
 */
final class SignedAgreementDemoTsa implements ClientInterface
{
    /** SHA-256 AlgorithmIdentifier: SEQUENCE { OID 2.16.840.1.101.3.4.2.1, NULL }. */
    private const string SHA256_ALG_ID_DER =
        "\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00";

    /** The last TimeStampToken issued (DER ContentInfo) — for post-save verification. */
    public string $lastIssuedToken = '';

    public function __construct(private readonly TsaTokenFactory $tokenFactory)
    {
    }

    #[\Override]
    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        ['digest' => $digest, 'nonce' => $nonce] = $this->parseTimeStampReq((string) $request->getBody());

        $token = $this->tokenFactory->build(
            producedAt: new DateTimeImmutable('now', new DateTimeZone('UTC')),
            messageImprintHash: $digest,
            nonce: $nonce,
        );
        $this->lastIssuedToken = $token;

        // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken ContentInfo }
        // PKIStatusInfo ::= SEQUENCE { status INTEGER 0 (granted) }   — RFC 3161 §2.4.2
        $body = "\x30\x03\x02\x01\x00" . $token;

        return new Response(
            statusCode: 200,
            body: new Stream("\x30" . self::derLength(\strlen($body)) . $body),
            headers: ['Content-Type' => 'application/timestamp-reply'],
        );
    }

    /**
     * Parse the digest + nonce out of an RFC 3161 §2.4.1 TimeStampReq.
     *
     * TimeStampReq ::= SEQUENCE { version INTEGER, messageImprint SEQUENCE
     * { hashAlgorithm AlgorithmIdentifier, hashedMessage OCTET STRING },
     * nonce INTEGER, certReq BOOLEAN }. Fail-closed on any divergence.
     *
     * @return array{digest: non-empty-string, nonce: int}
     */
    private function parseTimeStampReq(string $der): array
    {
        $offset = 0;
        self::expectTag($der, $offset, 0x30);                    // TimeStampReq
        self::readLength($der, $offset);
        self::skipElement($der, $offset);                        // version INTEGER

        self::expectTag($der, $offset, 0x30);                    // MessageImprint
        self::readLength($der, $offset);

        $algStart = $offset;                                     // hashAlgorithm
        self::skipElement($der, $offset);
        if (\substr($der, $algStart, $offset - $algStart) !== self::SHA256_ALG_ID_DER) {
            throw new RuntimeException('Demo TSA issues SHA-256 imprints only; the request declared a different AlgorithmIdentifier.');
        }

        self::expectTag($der, $offset, 0x04);                    // hashedMessage
        $digestLen = self::readLength($der, $offset);
        $digest = \substr($der, $offset, $digestLen);
        $offset += $digestLen;
        if (\strlen($digest) !== 32) {
            throw new RuntimeException('Demo TSA expected a 32-byte SHA-256 imprint, got ' . \strlen($digest) . ' bytes.');
        }

        self::expectTag($der, $offset, 0x02);                    // nonce INTEGER
        $nonceLen = self::readLength($der, $offset);
        $nonceBytes = \substr($der, $offset, $nonceLen);
        if ($nonceBytes !== '' && $nonceBytes[0] === "\x00") {
            $nonceBytes = \substr($nonceBytes, 1);               // positive-sign pad
        }
        if ($nonceBytes === ''
            || \strlen($nonceBytes) > 8
            || (\strlen($nonceBytes) === 8 && (\ord($nonceBytes[0]) & 0x80) !== 0)
        ) {
            throw new RuntimeException('Demo TSA could not decode the request nonce as a positive 63-bit integer.');
        }
        $nonce = 0;
        foreach (\str_split($nonceBytes) as $byte) {
            $nonce = ($nonce << 8) | \ord($byte);
        }

        return ['digest' => $digest, 'nonce' => $nonce];
    }

    // ── Minimal DER helpers (parse fail-closed; encode definite-length) ──

    private static function expectTag(string $data, int &$offset, int $tag): void
    {
        if ($offset >= \strlen($data) || \ord($data[$offset]) !== $tag) {
            throw new RuntimeException(\sprintf('Demo TSA: expected DER tag 0x%02X at offset %d.', $tag, $offset));
        }
        $offset++;
    }

    private static function readLength(string $data, int &$offset): int
    {
        if ($offset >= \strlen($data)) {
            throw new RuntimeException('Demo TSA: DER data ended while reading a length.');
        }
        $byte = \ord($data[$offset++]);
        if ($byte < 0x80) {
            return $byte;
        }
        $numBytes = $byte & 0x7F;
        if ($numBytes === 0 || $numBytes > 4) {
            throw new RuntimeException('Demo TSA: invalid DER length encoding.');
        }
        $length = 0;
        for ($i = 0; $i < $numBytes; $i++) {
            if ($offset >= \strlen($data)) {
                throw new RuntimeException('Demo TSA: DER data ended inside a long-form length.');
            }
            $length = ($length << 8) | \ord($data[$offset++]);
        }

        return $length;
    }

    private static function skipElement(string $data, int &$offset): void
    {
        $offset++; // tag
        $length = self::readLength($data, $offset);
        if ($offset + $length > \strlen($data)) {
            throw new RuntimeException('Demo TSA: DER element extends beyond the data boundary.');
        }
        $offset += $length;
    }

    private static function derLength(int $length): string
    {
        if ($length < 0x80) {
            return \chr($length);
        }
        if ($length < 0x100) {
            return "\x81" . \chr($length);
        }
        if ($length < 0x10000) {
            return "\x82" . \pack('n', $length);
        }

        return "\x83" . \chr(($length >> 16) & 0xFF) . \pack('n', $length & 0xFFFF);
    }
}

/**
 * Decode the first CERTIFICATE block of a PEM file to DER bytes.
 *
 * @return non-empty-string
 */
function signedAgreementPemToDer(string $pem, string $label): string
{
    if (\preg_match('/-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----/s', $pem, $m) !== 1) {
        throw new RuntimeException("{$label}: not a PEM CERTIFICATE block.");
    }
    $der = \base64_decode((string) \preg_replace('/\s+/', '', $m[1]), strict: true);
    if ($der === false || $der === '') {
        throw new RuntimeException("{$label}: PEM payload did not base64-decode.");
    }

    return $der;
}

// ─── Sample spec + branded template ─────────────────────────────────────────

$spec = new SampleSpec(
    id: 'signed-agreement',
    title: 'Digitally Signed Agreement',
    description: 'A master service agreement executed with a genuine PAdES B-T signature: a CMS SignedData over the full ByteRange plus an RFC 3161 signature time-stamp, produced and re-verified by the Core signing engine.',
    capability: 'PAdES digital signing',
    edition: Edition::Core,
    standards: ['ETSI EN 319 142-1', 'RFC 3161'],
    signatureLevel: 'PAdES B-T',
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // bold party names, signature values, ladder marker

$generated = $spec->generatedDateResolved();

$body = <<<HTML
<p class="np-byline">Master Service Agreement MSA-2026-0184 — executed and cryptographically sealed by the NextPDF signing engine</p>

<table class="np-meta"><tr>
    <td style="width: 31%; vertical-align: top;">
        <p class="np-eyebrow">PROVIDER</p>
        <p class="np-party">PATEON Network Technology Inc.</p>
        <p class="np-addr">7F, No. 100 Songren Road<br>Xinyi District, Taipei 110<br>[email protected]</p>
    </td>
    <td style="width: 31%; vertical-align: top;">
        <p class="np-eyebrow">CUSTOMER</p>
        <p class="np-party">Northwind Trading Co.</p>
        <p class="np-addr">420 Market Street<br>Seattle, WA 98101<br>[email protected]</p>
    </td>
    <td style="width: 38%; vertical-align: top;">
        <p class="np-eyebrow">AGREEMENT</p>
        <table class="np-kv">
            <tr><td class="np-kv-k">Number</td><td class="np-kv-v">MSA-2026-0184</td></tr>
            <tr><td class="np-kv-k">Effective</td><td class="np-kv-v">{$generated}</td></tr>
            <tr><td class="np-kv-k">Term</td><td class="np-kv-v">24 months</td></tr>
            <tr><td class="np-kv-k">Law</td><td class="np-kv-v">Taiwan (R.O.C.)</td></tr>
        </table>
    </td>
</tr></table>

<p class="np-sh3" style="margin-top: 10pt;">1 · Services</p>
<p class="np-clause">Provider grants Customer a non-exclusive licence to the NextPDF engine for the term of
this Agreement, together with onboarding and the service levels set out in Order Form OF-2026-0311.</p>

<p class="np-sh3">2 · Fees and term</p>
<p class="np-clause">Fees are invoiced annually in advance. The Agreement renews for successive twelve-month
periods unless either party gives written notice sixty days before the end of the current term.</p>

<p class="np-sh3">3 · Integrity and authenticity</p>
<p class="np-clause">This counterpart bears a PAdES baseline signature (ETSI EN 319 142-1): the signature binds
every byte outside the signature container, and an RFC 3161 time-stamp fixes the moment of signing.</p>

<p class="np-sh2">Signature evidence</p>
<div class="np-sig-panel">
<table class="np-sig-grid"><tr>
    <td class="np-sig-left">
        <table class="np-sig-kv">
            <tr><td class="np-sig-k">Signatory</td><td class="np-sig-v-strong">NextPDF Test RSA 2048 — demonstration certificate</td></tr>
            <tr><td class="np-sig-k">Format</td><td class="np-sig-v">CMS SignedData, detached · ISO 32000-2 §12.8</td></tr>
            <tr><td class="np-sig-k">Algorithm</td><td class="np-sig-v">RSASSA-PSS · SHA-256 · RSA-2048</td></tr>
            <tr><td class="np-sig-k">Coverage</td><td class="np-sig-v">Full-file /ByteRange — every byte except the signature container</td></tr>
            <tr><td class="np-sig-k">Time-stamp</td><td class="np-sig-v">RFC 3161 token over the signature value · demonstration TSA</td></tr>
        </table>
    </td>
    <td class="np-sig-right">
        <table class="np-seal">
            <tr><td class="np-seal-eyebrow">PAdES BASELINE</td></tr>
            <tr><td class="np-seal-level">B-T</td></tr>
            <tr><td class="np-seal-std">ETSI EN 319 142-1</td></tr>
            <tr><td class="np-seal-sub">Signature + RFC 3161 time-stamp</td></tr>
        </table>
    </td>
</tr></table>
</div>

<p class="np-sh2">The PAdES baseline ladder</p>
<table class="np-table">
    <thead>
        <tr>
            <th style="width: 13%;">Level</th>
            <th style="width: 57%;">What it adds</th>
            <th style="width: 30%;">This document</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="np-lad-level">B-B</td>
            <td class="np-lad">CMS signature with signed attributes (RFC 5652)</td>
            <td class="np-lad">Included</td>
        </tr>
        <tr class="np-zebra">
            <td class="np-lad-level">B-T</td>
            <td class="np-lad">Trusted RFC 3161 time-stamp over the signature value</td>
            <td class="np-lad-achieved">Achieved — this document</td>
        </tr>
        <tr>
            <td class="np-lad-level">B-LT</td>
            <td class="np-lad">DSS with certificates and OCSP/CRL revocation material</td>
            <td class="np-lad-muted">NextPDF Enterprise</td>
        </tr>
        <tr class="np-zebra">
            <td class="np-lad-level">B-LTA</td>
            <td class="np-lad">Archival document time-stamps for indefinite validation</td>
            <td class="np-lad-muted">NextPDF Enterprise</td>
        </tr>
    </tbody>
</table>

<p class="np-note">Demonstration signature: signed with the repository's self-signed RSA-2048 test certificate
and time-stamped by an in-process demonstration TSA — not a publicly-trusted or eIDAS-qualified authority.
The cryptography is real and re-verified by the engine on every render; production deployments supply their
own PKCS#12 material and a qualified TSA.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-byline { color: #6B7280; font-size: 8.5pt; margin: 0 0 6pt 0; }
    p.np-sh2 { font-family: 'Barlow'; font-weight: bold; font-size: 13pt; color: #1E3A8A; margin: 6pt 0 2pt 0; }
    p.np-sh3 { font-family: 'Barlow'; font-weight: bold; font-size: 10.5pt; color: #1E3A8A; margin: 4pt 0 1pt 0; }
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 3pt 0; }
    p.np-party { color: #111827; font-weight: bold; margin: 0; }
    p.np-addr { color: #6B7280; font-size: 9pt; font-weight: normal; margin: 2pt 0 0 0; }
    table.np-kv { width: 100%; }
    td.np-kv-k { width: 34%; color: #6B7280; font-size: 9pt; font-weight: normal; padding: 1pt 0; }
    td.np-kv-v { width: 66%; color: #111827; font-size: 9pt; font-weight: normal; text-align: right; padding: 1pt 0; }
    p.np-clause { color: #111827; font-weight: normal; margin: 0 0 3pt 0; }
    .np-sig-panel { border: 0.5pt solid #D1D5DB; border-radius: 3pt; background-color: #F8FAFC; padding: 6pt 10pt; margin: 2pt 0 2pt 0; }
    table.np-sig-grid { width: 100%; border-collapse: collapse; }
    td.np-sig-left { vertical-align: top; }
    td.np-sig-right { width: 132pt; vertical-align: top; padding-left: 10pt; }
    table.np-sig-kv { width: 100%; border-collapse: collapse; }
    td.np-sig-k { width: 22%; color: #6B7280; font-size: 8.5pt; font-weight: normal; padding: 1pt 8pt 1pt 0; vertical-align: top; }
    td.np-sig-v { color: #111827; font-size: 8.5pt; font-weight: normal; padding: 1pt 0; vertical-align: top; }
    td.np-sig-v-strong { color: #111827; font-size: 8.5pt; font-weight: bold; padding: 1pt 0; vertical-align: top; }
    table.np-seal { width: 100%; border-collapse: collapse; }
    td.np-seal-eyebrow { background-color: #FFFFFF; border-top: 2pt solid #8A5A00; padding: 6pt 9pt 0 9pt; color: #8A5A00; font-size: 6.5pt; font-weight: bold; letter-spacing: 0.5pt; }
    td.np-seal-level { background-color: #FFFFFF; padding: 0 9pt; font-family: 'Barlow'; font-weight: bold; font-size: 14pt; color: #1E3A8A; }
    td.np-seal-std { background-color: #FFFFFF; padding: 1pt 9pt 0 9pt; color: #111827; font-size: 7.5pt; font-weight: normal; }
    td.np-seal-sub { background-color: #FFFFFF; padding: 0 9pt 6pt 9pt; color: #6B7280; font-size: 7pt; font-weight: normal; }
    td.np-lad { color: #111827; font-size: 8.5pt; font-weight: normal; }
    td.np-lad-level { font-family: 'Barlow'; font-weight: bold; font-size: 8.5pt; color: #1E3A8A; }
    td.np-lad-muted { color: #6B7280; font-size: 8.5pt; font-weight: normal; }
    td.np-lad-achieved { color: #8A5A00; font-size: 8.5pt; font-weight: bold; }
    p.np-note { color: #6B7280; font-size: 8pt; font-weight: normal; margin-top: 5pt; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

// ─── Signing material (repository test fixtures) ────────────────────────────

$engineRepo = \getenv('NEXTPDF_ENGINE_REPO') ?: 'C:\\Users\\admin\\Documents\\nextpdf';
$certDir = $engineRepo . '/tests/Fixtures/Certificates';

foreach ([
    'test-rsa-2048-cert.pem',
    'test-rsa-2048-key.pem',
    'test-rsa-2048-tsa-cert.pem',
    'test-rsa-2048-tsa-key.pem',
] as $fixture) {
    if (!\is_file($certDir . '/' . $fixture)) {
        \fwrite(STDERR, "Certificate fixture absent: {$certDir}/{$fixture}. Run tests/Fixtures/Certificates/generate.sh\n");
        exit(1);
    }
}

$certInfo = new CertificateInfo(
    certificate: (string) \file_get_contents($certDir . '/test-rsa-2048-cert.pem'),
    privateKey: (string) \file_get_contents($certDir . '/test-rsa-2048-key.pem'),
);

$tsaCertDer = signedAgreementPemToDer(
    (string) \file_get_contents($certDir . '/test-rsa-2048-tsa-cert.pem'),
    'TSA fixture certificate',
);
$demoTsa = new SignedAgreementDemoTsa(new TsaTokenFactory(
    signerCertDer: $tsaCertDer,
    privateKeyPem: (string) \file_get_contents($certDir . '/test-rsa-2048-tsa-key.pem'),
));
$tsaClient = new TsaClient(
    tsaUrl: 'https://tsa.demo.invalid/rfc3161', // never contacted: the demo TSA answers in-process
    httpClient: $demoTsa,
);

$doc->setSignature($certInfo, SignatureLevel::PAdES_B_T, $tsaClient);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/signed-agreement.pdf';
$doc->save($output);

// ─── Post-save verification (engine APIs; fail-closed) ──────────────────────

$pdf = (string) \file_get_contents($output);

// 1. Engine inspector: the saved file carries a signature dictionary.
$inspection = (new Inspector())->inspect($pdf, InspectConfig::quick());
if (!$inspection->hasSigned) {
    throw new RuntimeException('Verification failed: Inspector reports no signature on the saved PDF.');
}

// 2. ISO 32000-2 §12.8.1: /ByteRange covers the whole file except the /Contents hole.
if (\preg_match('#/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]#', $pdf, $m) !== 1) {
    throw new RuntimeException('Verification failed: signed PDF carries no numeric /ByteRange.');
}
[$r1Off, $r1Len, $r2Off, $r2Len] = [(int) $m[1], (int) $m[2], (int) $m[3], (int) $m[4]];
if ($r1Off !== 0 || $r2Off + $r2Len !== \strlen($pdf)) {
    throw new RuntimeException('Verification failed: /ByteRange does not span the whole file.');
}
$hole = \substr($pdf, $r1Off + $r1Len, $r2Off - ($r1Off + $r1Len));
if (\preg_match('#^<([0-9A-Fa-f]+)>$#', $hole, $hex) !== 1) {
    throw new RuntimeException('Verification failed: the ByteRange gap is not a single /Contents hex string.');
}
$cms = (string) \hex2bin($hex[1]);

// 3. RFC 5652 §11.2: the CMS messageDigest attribute binds exactly the covered bytes.
$covered = \substr($pdf, $r1Off, $r1Len) . \substr($pdf, $r2Off, $r2Len);
if (!\str_contains($cms, "\x04\x20" . \hash('sha256', $covered, binary: true))) {
    throw new RuntimeException('Verification failed: CMS messageDigest does not bind the ByteRange-covered bytes.');
}

// 4. B-T: the issued RFC 3161 token is embedded as the id-aa-timeStampToken
//    unsigned attribute (OID 1.2.840.113549.1.9.16.2.14, RFC 5652 §5.3).
$timestampAttrOid = "\x06\x0b\x2a\x86\x48\x86\xf7\x0d\x01\x09\x10\x02\x0e";
if ($demoTsa->lastIssuedToken === ''
    || !\str_contains($cms, $timestampAttrOid)
    || !\str_contains($cms, $demoTsa->lastIssuedToken)
) {
    throw new RuntimeException('Verification failed: the RFC 3161 token is not embedded in the CMS unsigned attributes.');
}

// 5. Engine token verifier: CMS walk, single SignerInfo, signed attributes,
//    message-digest binding, ESSCertIDv2, critical id-kp-timeStamping EKU,
//    RSA signature over signedAttrs, producedAt tolerance. Throws on failure.
$tokenCheck = $tsaClient->verifyToken($demoTsa->lastIssuedToken, $tsaCertDer);

echo "Created: {$output}\n";
\printf(
    "Verified: PAdES B-T — CMS binds %d covered bytes; RFC 3161 token (%d bytes, %s, produced %s) embedded and cryptographically verified.\n",
    \strlen($covered),
    \strlen($demoTsa->lastIssuedToken),
    $tokenCheck->hashAlgorithm,
    $tokenCheck->producedAt->format('Y-m-d\TH:i:s\Z'),
);
SourceFactur-X e-invoice

Produced byphp tools/render-samples/scripts/e-invoice.php

tools/render-samples/scripts/e-invoice.php
<?php

declare(strict_types=1);

/**
 * Sample: Factur-X e-Invoice (NextPDF Enterprise, PDF/A-3b + EN 16931).
 *
 * A hybrid electronic invoice: the visible page is rendered through the shared
 * SampleTemplate onto a PDF/A-3b carrier (enablePdfA(PdfAVersion::PdfA3b) —
 * ISO 19005-3:2012, the archival level that permits embedded files), and the
 * SAME invoice travels beside it as EN 16931 CrossIndustryInvoice XML
 * (factur-x.xml), embedded via the Enterprise ZugferdEmbedder facade:
 *
 *   1. ZugferdEmbedder::create(EN16931, $xml)->embed($pdfAManager, ...) runs
 *      the XmlGuard security pass + the EN 16931 business-term validation,
 *      verifies the PDF/A carrier supports embedded files, and injects the
 *      Factur-X XMP extension schema (fx:DocumentType / fx:DocumentFileName /
 *      fx:Version / fx:ConformanceLevel + the pdfaExtension declaration) into
 *      the DOCUMENT's own PdfAManager metadata — the writer emits that exact
 *      XmpMetadata instance at save time.
 *   2. The payload itself is attached through Document::embedFile(): the
 *      Document's attachment container is private and lazily created by its
 *      own embed API (there is no adoption seam for a pre-filled
 *      FileAttachment — recorded as an engine-side finding, not worked around
 *      here), so the facade is handed a scratch container for its recording
 *      step and the real attach goes through the public Document surface with
 *      the same payload, filename, description, and AFRelationship.
 *
 * DETERMINISM: Document::embedFile() derives the embedded stream's
 * /Params /ModDate from the source file's mtime (the engine's Reproducible
 * Builds path) — the XML is staged under the Factur-X mandated filename with
 * its mtime pinned to the sample's generated date, so a re-render is
 * byte-identical. (The string variant embedFileFromString() stamps wall-clock
 * time and would break the render-all determinism gate.)
 *
 * The engine writes the full PDF/A-3 associated-file wiring: the catalog /AF
 * array, the /Names /EmbeddedFiles name tree, and the Filespec with
 * /AFRelationship /Alternative (Factur-X 1.08 §6.2 recommended value).
 *
 * TEMPLATE VARIANT "body-furniture" (footer only): under PDF/A the engine
 * suppresses the FOOTER stream in flushCurrentPage() (HasOutput — the default
 * footer would paint unembedded Base14 Helvetica), and unlike the plain
 * PDF/A-4 path this suppression also swallows the template's footer CALLBACK
 * on the PDF/A-3b (and 4f) conformance modes — verified empirically against
 * archival.php (PDF/A-4: callback footer paints) vs this sample (PDF/A-3b:
 * no footer stream). The HEADER callback still runs (band + amber rule are
 * painted), so only the footer is re-emitted here as real body content via
 * Document::text() — the same pattern accessible-report.php uses under
 * tagging, with the template's own geometry constants. The page marker is a
 * literal "1 / 1": the document is one page by construction (guarded below).
 *
 * @see NextPDF\Enterprise\Invoice\ZugferdEmbedder
 * @see NextPDF\Core\Concerns\HasSecurity::enablePdfA()
 * @see NextPDF\Core\Concerns\HasFileAttachments::embedFile()
 */

require __DIR__ . '/../lib/bootstrap-premium.php';

use NextPDF\Enterprise\Archive\PdfAManager;
use NextPDF\Enterprise\Archive\PdfAVersion;
use NextPDF\Enterprise\Invoice\ZugferdEmbedder;
use NextPDF\Enterprise\Invoice\ZugferdProfile;
use NextPDF\Navigation\AFRelationship;
use NextPDF\Navigation\FileAttachment;
use NextPDF\Samples\Edition;
use NextPDF\Samples\PrintPalette;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Samples\Typography;

// Capability is the masthead SHORT form (like invoice.php 'HTML/CSS tables'
// vs the registry's long gallery line): the spec-table column is ~40 chars —
// the registry's 70-char capability squeezes the engine's auto table layout
// until neighbouring spec cells overprint (verified by pdf-audit G1/G4).
$spec = new SampleSpec(
    id: 'e-invoice',
    title: 'Factur-X e-Invoice',
    description: 'A PDF/A e-invoice carrying an embedded EN 16931 CrossIndustryInvoice XML payload (Factur-X / ZUGFeRD) produced by the Enterprise ZugferdEmbedder.',
    capability: 'PDF/A-3 + Factur-X XML embedding',
    edition: Edition::Enterprise,
    // Masthead SHORT forms (the registry keeps the long parenthesised list):
    // the spec-table STANDARDS column fits ~34 chars before the engine's
    // content-driven table layout squeezes neighbouring cells into overprint
    // (pdf-audit G1/G4, verified); the full standard names appear in the body.
    standards: ['ISO 32000-2', 'ISO 19005', 'EN 16931'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);

// PDF/A-3b MUST be enabled before apply()/addPage()/content: the writer fixes
// the output intent, XMP conformance packet, and font-embedding policy at
// document setup time. 3b (not 4f) so the sample demonstrates the classic
// Factur-X 1.08 / ZUGFeRD 2.4 carrier that today's AP platforms ingest.
$doc->enablePdfA(PdfAVersion::PdfA3b);

$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
// Body paints Inter-B (<strong>, totals) and JetBrainsMono (machine
// identifiers: factur-x.xml, the BT-24 urn, fx: XMP property names).
$template->registerFaces($doc, [['Inter', 'B'], ['JetBrainsMono', '']]);

// Engine HTML notes this sample verified by texttrace (2026-07-13, PDF/A path):
//  * Inside table cells only the FONT-FAMILY inline property applies; color /
//    font-size / font-weight inline declarations are ignored. td-level CLASS
//    rules apply color / font-weight / text-align / padding (never font-size),
//    and a TABLE-level class font-size cascades into its cells. All cell
//    styling below therefore rides td classes + table-level sizes, with
//    inline styles used solely for the JetBrainsMono spans.
//  * <strong> latches bold PAST its own block into a following table's first
//    cell, so the lead paragraph is written in its OWN writeHtml() call —
//    each call starts from the document's clean body state.
$lead = <<<'HTML'
<p class="np-lead">One invoice, two audiences: this page is the human-readable layer of a
    <strong>hybrid Factur-X e-invoice</strong><span class="np-normal">, and the same invoice travels
    beside it as EN&nbsp;16931 structured data (</span><span class="np-mono">factur-x.xml</span><span
    class="np-normal">) that accounts-payable platforms post without retyping a figure.</span></p>
HTML;

$body = <<<'HTML'
<table class="np-eyerow" style="table-layout: fixed; width: 100%; margin-top: 12pt;">
    <tr><td style="width: 50%;">SELLER</td><td style="width: 50%;">BILLED TO</td></tr>
</table>
<table class="np-party" style="table-layout: fixed; width: 100%;">
    <tr>
        <td class="np-name" style="width: 50%;">PATEON / NextPDF</td>
        <td class="np-name" style="width: 50%;">Northwind Trading Co.</td>
    </tr>
    <tr>
        <td class="np-dim" style="width: 50%;">VAT US94105X042 &middot; United States</td>
        <td class="np-dim" style="width: 50%;">Buyer reference NW-AP-2026-0042 &middot; United States</td>
    </tr>
</table>

<table class="np-facts" style="table-layout: fixed; width: 100%; margin-top: 10pt;">
    <tr>
        <td class="np-fk" style="width: 12%;">Number</td><td class="np-fv np-strong" style="width: 38%;">INV-2026-0042</td>
        <td class="np-fk" style="width: 12%;">Issued</td><td class="np-fv" style="width: 38%;">2026-06-27</td>
    </tr>
    <tr>
        <td class="np-fk" style="width: 12%;">Type</td><td class="np-fv" style="width: 38%;">Commercial invoice (380)</td>
        <td class="np-fk" style="width: 12%;">Due</td><td class="np-fv np-strong np-accent" style="width: 38%;">2026-07-27</td>
    </tr>
    <tr>
        <td class="np-fk" style="width: 12%;">Terms</td><td class="np-fv" style="width: 38%;">Net 30</td>
        <td class="np-fk" style="width: 12%;">Currency</td><td class="np-fv" style="width: 38%;">USD</td>
    </tr>
</table>

<table class="np-table" style="margin-top: 12pt;">
    <thead>
        <tr>
            <th style="width: 6%;">#</th>
            <th style="width: 46%;">Description</th>
            <th style="width: 12%;" class="np-num">Qty</th>
            <th style="width: 18%;" class="np-num">Unit price</th>
            <th style="width: 18%;" class="np-num">Amount</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td><td>NextPDF Pro License &mdash; annual subscription</td>
            <td class="np-num">2</td><td class="np-num">$299.00</td><td class="np-num">$598.00</td>
        </tr>
        <tr class="np-zebra">
            <td>2</td><td>NextPDF Enterprise License &mdash; annual subscription</td>
            <td class="np-num">1</td><td class="np-num">$999.00</td><td class="np-num">$999.00</td>
        </tr>
        <tr>
            <td>3</td><td>Priority Support &mdash; 12 months, 4-hour SLA</td>
            <td class="np-num">1</td><td class="np-num">$199.00</td><td class="np-num">$199.00</td>
        </tr>
        <tr class="np-zebra">
            <td>4</td><td>Onboarding and integration workshop (remote)</td>
            <td class="np-num">1</td><td class="np-num">$450.00</td><td class="np-num">$450.00</td>
        </tr>
    </tbody>
</table>

<table class="np-summary" style="margin-top: 8pt;">
    <tr><td style="width: 60%;"></td><td class="np-num np-muted" style="width: 26%;">Net total</td><td class="np-num" style="width: 14%;">$2,246.00</td></tr>
    <tr><td></td><td class="np-num np-muted">VAT 8.5% (category S)</td><td class="np-num">$190.91</td></tr>
    <tr class="np-totals"><td></td><td class="np-num">Amount due (USD)</td><td class="np-num">$2,436.91</td></tr>
</table>

<div class="np-hybrid">
    <p class="np-hybrid-title">EN 16931 machine-readable layer</p>
    <table class="np-hkv" style="table-layout: fixed; width: 100%;">
        <tr>
            <td class="np-hk" style="width: 24%;">Attachment</td>
            <td class="np-hv" style="width: 76%;"><span style="font-family: 'JetBrainsMono';">factur-x.xml</span> &mdash; UN/CEFACT Cross-Industry Invoice</td>
        </tr>
        <tr>
            <td class="np-hk" style="width: 24%;">Profile</td>
            <td class="np-hv" style="width: 76%;">EN 16931 (Factur-X 1.08 / ZUGFeRD 2.4)</td>
        </tr>
        <tr>
            <td class="np-hk" style="width: 24%;">Specification (BT-24)</td>
            <td class="np-hv" style="width: 76%;"><span style="font-family: 'JetBrainsMono';">urn:cen.eu:en16931:2017</span></td>
        </tr>
        <tr>
            <td class="np-hk" style="width: 24%;">Association</td>
            <td class="np-hv" style="width: 76%;"><span style="font-family: 'JetBrainsMono';">AFRelationship /Alternative</span> &mdash; an equivalent representation of this page</td>
        </tr>
        <tr>
            <td class="np-hk" style="width: 24%;">XMP declaration</td>
            <td class="np-hv" style="width: 76%;"><span style="font-family: 'JetBrainsMono';">fx:DocumentType INVOICE</span> &middot; <span style="font-family: 'JetBrainsMono';">fx:ConformanceLevel EN 16931</span></td>
        </tr>
        <tr>
            <td class="np-hk" style="width: 24%;">Carrier</td>
            <td class="np-hv" style="width: 76%;">PDF/A-3b (ISO 19005-3:2012), fonts embedded, sRGB output intent</td>
        </tr>
    </table>
    <p class="np-hybrid-note">Accounting systems extract and post the XML directly; auditors and
        people keep this archival page &mdash; one file, byte-for-byte the same invoice in both worlds.</p>
</div>

<p class="np-note">Payment due within 30 days of the invoice date (Net 30) &mdash; payable by 2026-07-27.
    Reference INV-2026-0042 on all remittances. Demonstration data only. All amounts in USD.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-lead { color: #111827; margin: 0; }
    span.np-normal { font-weight: normal; color: #111827; }
    .np-strong { color: #111827; font-weight: bold; }
    table.np-eyerow { font-size: 7pt; }
    table.np-eyerow td { color: #6B7280; font-weight: normal; padding: 0 0 2pt 0; }
    td.np-name { color: #111827; font-weight: bold; padding: 0; }
    td.np-dim { color: #6B7280; font-weight: normal; padding: 1pt 0 0 0; }
    table.np-facts { font-size: 9pt; }
    .np-fk { color: #6B7280; font-weight: normal; padding: 1.5pt 0; }
    .np-fv { color: #111827; padding: 1.5pt 10pt 1.5pt 0; }
    table.np-summary { font-size: 9pt; }
    table.np-summary td { padding: 3pt 6pt; }
    table.np-summary tr.np-totals td { background-color: #1E3A8A; color: #FFFFFF; font-weight: bold; }
    div.np-hybrid { margin-top: 16pt; background-color: #F8FAFC; border-left: 2.5pt solid #1E3A8A; padding: 10pt 14pt; }
    p.np-hybrid-title { color: #1E3A8A; font-weight: bold; font-size: 9.5pt; margin: 0 0 2pt 0; }
    table.np-hkv { font-size: 8.5pt; }
    .np-hk { color: #6B7280; font-weight: normal; padding: 1.5pt 0; }
    .np-hv { color: #374151; font-weight: normal; padding: 1.5pt 0; }
    p.np-hybrid-note { color: #374151; font-size: 8.5pt; font-weight: normal; line-height: 1.5; margin: 5pt 0 0 0; }
    p.np-note { color: #6B7280; font-size: 8pt; font-weight: normal; margin-top: 12pt; }
    </style>
    CSS;

// Two calls by design: the writer starts each writeHtml() from the clean body
// state, so the lead's <strong> cannot latch bold into the first table cell
// of the block that follows (engine quirk, texttrace-verified).
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $lead);
$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

// ── Footer as body content (template geometry, see file docblock) ──────────
if ($doc->getNumPages() !== 1) {
    throw new RuntimeException(
        'e-invoice must stay a single page (the body-emitted footer states "1 / 1"); got ' . $doc->getNumPages() . ' pages.',
    );
}

$pageWidth = $doc->getPageWidth();
$pageHeight = $doc->getPageHeight();
$left = SampleTemplate::mm(SampleTemplate::MARGIN_LEFT_MM);
$right = $pageWidth - SampleTemplate::mm(SampleTemplate::MARGIN_RIGHT_MM);
$ruleY = $pageHeight - SampleTemplate::mm(SampleTemplate::FOOTER_RULE_Y_MM);
$baseY = $pageHeight - SampleTemplate::mm(SampleTemplate::FOOTER_BASELINE_Y_MM);

$doc->setDrawColor(...PrintPalette::rgb(PrintPalette::HAIRLINE));
$doc->setLineWidth(0.4);
$doc->line($left, $ruleY, $right, $ruleY);

$footerKey = Typography::fontKey(Typography::FAMILY_TEXT);
$footerSize = Typography::SIZE_FOOTER;
$doc->setFont(Typography::FAMILY_TEXT, '', $footerSize);
$doc->setTextColor(...PrintPalette::rgb(PrintPalette::MUTED));

$doc->text($left, $baseY, SampleTemplate::FOOTER_BRAND);

$metrics = $doc->fontMetrics();
$urlWidth = $metrics->getStringWidth(SampleTemplate::FOOTER_URL, $footerKey, $footerSize);
$doc->text($left + (($right - $left) - $urlWidth) / 2, $baseY, SampleTemplate::FOOTER_URL);

$marker = '1 / 1';
$markerWidth = $metrics->getStringWidth($marker, $footerKey, $footerSize);
$doc->text($right - $markerWidth, $baseY, $marker);

$doc->setTextColor(...PrintPalette::rgb(PrintPalette::INK));
$doc->setFont(Typography::FAMILY_TEXT, '', Typography::SIZE_BODY);

// ── Factur-X embedding (Enterprise ZugferdEmbedder) ─────────────────────────
$xmlPath = \dirname(__DIR__) . DIRECTORY_SEPARATOR . 'assets'
    . DIRECTORY_SEPARATOR . 'xml' . DIRECTORY_SEPARATOR . 'facturx-en16931-sample.xml';
$facturXml = \file_get_contents($xmlPath);
if ($facturXml === false || $facturXml === '') {
    throw new RuntimeException("Vendored EN 16931 payload not readable: {$xmlPath}");
}

$pdfaManager = $doc->getPdfAManager();
if (!$pdfaManager instanceof PdfAManager) {
    throw new RuntimeException('enablePdfA() did not yield the Enterprise PdfAManageris nextpdf/enterprise installed?');
}

$attachmentDescription = 'Factur-X / EN 16931 electronic invoice data';

// XmlGuard security pass + EN 16931 business-term validation + carrier check
// + Factur-X XMP extension schema injection into the DOCUMENT's PdfAManager
// metadata (emitted by the writer at save time). The facade records its
// attach step into the container it is handed; the Document's own container
// has no public adoption seam, so this one is scratch and the real attach
// follows through the Document API with identical values.
$result = ZugferdEmbedder::create(ZugferdProfile::EN16931, $facturXml)
    ->withDescription($attachmentDescription)
    ->withAfRelationship(AFRelationship::Alternative)
    ->embed($pdfaManager, new FileAttachment());

// Stage the payload under the Factur-X mandated filename (factur-x.xml — it
// must match the fx:DocumentFileName the XMP schema just declared) with the
// mtime pinned to the sample's generated date: Document::embedFile() derives
// the embedded stream's /Params /ModDate from filemtime (UTC), which keeps
// the output byte-deterministic across re-renders.
$stageDir = \sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'npf-einvoice-' . \bin2hex(\random_bytes(6));
if (!\mkdir($stageDir, 0700, true)) {
    throw new RuntimeException("Failed to create staging directory: {$stageDir}");
}

try {
    $stagedXml = $stageDir . DIRECTORY_SEPARATOR . $result->attachedFilename;
    if (\basename($stagedXml) !== ZugferdProfile::EN16931->getXmlFilename()) {
        throw new RuntimeException('Staged filename must equal the Factur-X profile filename (factur-x.xml).');
    }
    if (!\copy($xmlPath, $stagedXml)) {
        throw new RuntimeException("Failed to stage the Factur-X payload at: {$stagedXml}");
    }
    $pinnedMtime = (int) (new DateTimeImmutable($spec->generatedDateResolved() . 'T00:00:00+00:00'))->format('U');
    if (!\touch($stagedXml, $pinnedMtime)) {
        throw new RuntimeException("Failed to pin the staged payload mtime: {$stagedXml}");
    }

    $doc->embedFile($stagedXml, $attachmentDescription, AFRelationship::Alternative);

    $output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/e-invoice.pdf';
    $doc->save($output);
} finally {
    if (isset($stagedXml) && \is_file($stagedXml)) {
        \unlink($stagedXml);
    }
    if (\is_dir($stageDir)) {
        \rmdir($stageDir);
    }
}

echo "Created: {$output}\n";
SourcePDF/A-4 archival record

Produced byphp tools/render-samples/scripts/archival.php

tools/render-samples/scripts/archival.php
<?php

declare(strict_types=1);

/**
 * Sample: Records Retention Statement (NextPDF Enterprise, PDF/A-4).
 *
 * Rebuilt onto the shared SampleTemplate. Demonstrates the engine's archival
 * conformance path: Document::enablePdfA() (no argument = PDF/A-4, the
 * ISO 19005-4:2020 long-term preservation level of PDF 2.0) is engaged BEFORE
 * apply()/addPage(), so the writer emits the PDF/A furniture the profile
 * mandates — an embedded sRGB output intent, an XMP metadata packet carrying
 * pdfaid part 4, and fully embedded font programs. No external ICC file is
 * supplied: the engine bundles the sRGB IEC 61966-2.1 profile, its
 * /OutputIntent entry, and the conformance XMP itself.
 *
 * The template's standard masthead + header CALLBACK are used unchanged
 * (plain PDF/A-4 creates no structure tree, so the tagged-mode header
 * suppression does not apply), but the engine suppresses the FOOTER for any
 * PDF/A document regardless of callback (HasOutput::flushCurrentPage():
 * `$this->pdfaManager !== null` short-circuits renderFooter(), because the
 * DEFAULT footer would paint unembedded Base14 Helvetica — an ISO
 * 19005-4:2020 §6.2.1 violation). The footer furniture is therefore painted
 * below as real body content via Document::text()/line() in embedded Inter,
 * the same body-furniture pattern the tagged samples use, at the exact
 * template footer geometry. The page marker is the literal "1 / 1": this is
 * a single-page document by construction (one writeHtml() flow, page count
 * pinned 1..1 by the pdf-audit expectation), and the dynamic
 * pageTotalPlaceholder() mechanism only exists on the unreachable callback
 * path.
 *
 * Under PDF/A the engine embeds every painted face, so the pdf-audit F1 gate
 * reports NO unembedded font on this file — archival conformance forbids any
 * non-embedded glyph.
 *
 * @see NextPDF\Core\Concerns\HasSecurity::enablePdfA()
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap-premium.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\PrintPalette;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;
use NextPDF\Samples\Typography;

$spec = new SampleSpec(
    id: 'archival',
    title: 'Records Retention Statement',
    description: 'An archival master preserved in PDF/A-4 (ISO 19005-4:2020): a records retention schedule with an embedded sRGB output intent, an XMP conformance packet, and fully embedded font subsets for faithful long-term reproduction.',
    capability: 'PDF/A-4 archival conformance',
    edition: Edition::Enterprise,
    standards: ['ISO 32000-2', 'ISO 19005-4 (PDF/A-4)'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);

// enablePdfA() MUST run before apply()/addPage()/content: the writer decides
// the PDF/A output intent, XMP packet, and font-embedding policy at document
// setup time. No argument => PDF/A-4 (ISO 19005-4:2020).
$doc->enablePdfA();

$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong> emphasis + kv labels + totals

$body = <<<'HTML'
<p>This record is preserved as an archival master in <strong>PDF/A-4</strong>
    (ISO 19005-4:2020) &mdash; the long-term preservation conformance level of
    PDF 2.0. Every font is embedded as a subset, all colour is bound to an
    embedded sRGB output intent, and the descriptive metadata is written as an
    embedded XMP packet, so the file stays faithfully reproducible decades after
    creation &mdash; independent of the software that renders it.</p>

<table class="np-meta" style="table-layout: fixed; width: 100%; margin-top: 12pt;"><tr>
    <td style="width: 58%; vertical-align: top;">
        <p class="np-eyebrow">PRESERVATION INTENT</p>
        <p class="np-side">The retention schedule below governs how long each
            class of business record is kept, what event starts the clock, and
            how the record is disposed of at the end of its term. Preserving the
            schedule in a validated archival format means the policy itself
            survives migrations, vendor changes, and format churn &mdash; a
            reader in 2050 opens the same authoritative statement produced today,
            byte-for-byte reproducible from this deterministic build.</p>
    </td>
    <td style="width: 42%; vertical-align: top; padding-left: 16pt;">
        <p class="np-eyebrow">ARCHIVAL METADATA</p>
        <table class="np-kv">
            <tr><td class="np-kv-k">Conformance</td><td class="np-kv-v np-strong">PDF/A-4</td></tr>
            <tr><td class="np-kv-k">Standard</td><td class="np-kv-v">ISO 19005-4:2020</td></tr>
            <tr><td class="np-kv-k">Base format</td><td class="np-kv-v">PDF 2.0</td></tr>
            <tr><td class="np-kv-k">Colour intent</td><td class="np-kv-v">sRGB IEC 61966-2.1</td></tr>
            <tr><td class="np-kv-k">Metadata</td><td class="np-kv-v">XMP (embedded)</td></tr>
            <tr><td class="np-kv-k">Fonts</td><td class="np-kv-v">Embedded subsets</td></tr>
        </table>
    </td>
</tr></table>

<p class="np-h3" style="margin-top: 20pt;">Records retention schedule</p>

<table class="np-table" style="margin-top: 6pt;">
    <thead>
        <tr>
            <th style="width: 32%;">Record class</th>
            <th style="width: 18%;" class="np-num">Retention period</th>
            <th style="width: 26%;">Retention trigger</th>
            <th style="width: 24%;">Disposition</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Financial &amp; tax records</td>
            <td class="np-num">7 years</td>
            <td>Fiscal year end</td>
            <td>Secure destruction</td>
        </tr>
        <tr class="np-zebra">
            <td>Contracts &amp; agreements</td>
            <td class="np-num">10 years</td>
            <td>Contract expiry</td>
            <td>Archival review</td>
        </tr>
        <tr>
            <td>Personnel records</td>
            <td class="np-num">6 years</td>
            <td>End of employment</td>
            <td>Secure destruction</td>
        </tr>
        <tr class="np-zebra">
            <td>Corporate governance</td>
            <td class="np-num np-accent np-strong">Permanent</td>
            <td>&mdash;</td>
            <td>Permanent archive</td>
        </tr>
        <tr>
            <td>Regulatory correspondence</td>
            <td class="np-num">5 years</td>
            <td>Matter closure</td>
            <td>Secure destruction</td>
        </tr>
        <tr class="np-zebra">
            <td>Audit &amp; compliance logs</td>
            <td class="np-num">3 years</td>
            <td>Report issuance</td>
            <td>Secure destruction</td>
        </tr>
    </tbody>
</table>

<p class="np-schedule-note">Retention periods are illustrative of a typical
    corporate records policy; the governing schedule for any jurisdiction is set
    by that organisation's legal and compliance owners.</p>

<div class="np-conformance">
    <p class="np-conformance-title">ISO 19005-4:2020 conformance</p>
    <p class="np-conformance-body">This file declares <strong>pdfaid part 4</strong>
        in its XMP packet, carries a <strong>GTS_PDFA</strong> output intent with an
        embedded sRGB IEC 61966-2.1 profile, and embeds every font program as a
        subset &mdash; the three structural guarantees ISO 19005-4 requires for
        long-term reproduction. Validate independently against the veraPDF PDF/A-4
        reference implementation before relying on the file for preservation.</p>
</div>
HTML;

$css = <<<'CSS'
    <style>
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 4pt 0; }
    p.np-strong, .np-strong { color: #111827; font-weight: bold; margin: 0; }
    p.np-side { color: #6B7280; font-size: 9pt; margin: 0; }
    table.np-kv { width: 100%; }
    td.np-kv-k { width: 42%; color: #6B7280; font-size: 9pt; padding: 1.5pt 0; }
    td.np-kv-v { width: 58%; color: #111827; font-size: 9pt; text-align: right; padding: 1.5pt 0; }
    p.np-schedule-note { color: #6B7280; font-size: 8pt; margin: 8pt 0 0 0; }
    div.np-conformance { margin-top: 12pt; background-color: #F8FAFC; border-left: 2.5pt solid #1E3A8A; padding: 10pt 14pt 6pt 14pt; }
    p.np-conformance-title { color: #1E3A8A; font-weight: bold; font-size: 9.5pt; margin: 0; }
    p.np-conformance-body { color: #374151; font-size: 8.5pt; line-height: 1.5; margin: 0; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

// ---- Footer as body content (see file docblock: the engine suppresses the
// footer callback for every PDF/A document) — template geometry + palette,
// painted in embedded Inter 7 pt. -----------------------------------------
$pageWidth = $doc->getPageWidth();
$pageHeight = $doc->getPageHeight();
$left = SampleTemplate::mm(SampleTemplate::MARGIN_LEFT_MM);
$right = $pageWidth - SampleTemplate::mm(SampleTemplate::MARGIN_RIGHT_MM);
$ruleY = $pageHeight - SampleTemplate::mm(SampleTemplate::FOOTER_RULE_Y_MM);
$baseY = $pageHeight - SampleTemplate::mm(SampleTemplate::FOOTER_BASELINE_Y_MM);

$doc->setDrawColor(...PrintPalette::rgb(PrintPalette::HAIRLINE));
$doc->setLineWidth(0.4);
$doc->line($left, $ruleY, $right, $ruleY);

$footerFontKey = Typography::fontKey(Typography::FAMILY_TEXT);
$footerSize = Typography::SIZE_FOOTER;
$doc->setFont(Typography::FAMILY_TEXT, '', $footerSize);
$doc->setTextColor(...PrintPalette::rgb(PrintPalette::MUTED));

$doc->text($left, $baseY, SampleTemplate::FOOTER_BRAND);

$metrics = $doc->fontMetrics();
$urlWidth = $metrics->getStringWidth(SampleTemplate::FOOTER_URL, $footerFontKey, $footerSize);
$doc->text($left + (($right - $left) - $urlWidth) / 2, $baseY, SampleTemplate::FOOTER_URL);

$marker = '1 / 1'; // single-page by construction; see file docblock
$markerWidth = $metrics->getStringWidth($marker, $footerFontKey, $footerSize);
$doc->text($right - $markerWidth, $baseY, $marker);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/archival.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceAES-256 encryption & permissions

Produced byphp tools/render-samples/scripts/encrypted.php

tools/render-samples/scripts/encrypted.php
<?php

declare(strict_types=1);

/**
 * Sample: Encrypted Document (NextPDF Core, PDF 2.0, AES-256).
 *
 * Rebuilt onto the shared SampleTemplate. Demonstrates the engine's ISO
 * 32000-2 §7.6.4 Standard security handler: AES-256 in CBC mode (V5 / R6 /
 * AESV3 crypt filters), an EMPTY user password so the document opens without
 * a prompt, a clearly-demo owner password, and a Table 22 permission bitmask
 * that denies copy/extract and modify-contents.
 *
 * NON-DETERMINISTIC by design: AES-CBC uses a random IV per encrypted string
 * and stream, so re-renders differ at the byte level (the visual raster is
 * stable). This sample is exempt from the render-all determinism gate.
 *
 * @see NextPDF\Core\Concerns\HasSecurity::setEncryption()
 * @see NextPDF\Security\Encryption\Aes256Encryptor
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'encrypted',
    title: 'Encrypted Document',
    description: 'An AES-256 encrypted document (Standard security handler, V5/R6/AESV3) with an empty user password, a demo owner password, and a permission bitmask denying copy/extract and modification.',
    capability: 'AES-256 encryption',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong> emphasis, kv values, Denied/Allowed states

$body = <<<'HTML'
<p>This document is <strong>AES-256 encrypted</strong>; it opens without a password
    for this demonstration; copy and modify permissions are set and enforced by
    conforming readers. Every string and stream in the file &mdash; page content,
    fonts, metadata values &mdash; is stored as AES-256 ciphertext; what you are
    reading now was decrypted by your viewer using the file's own encryption
    dictionary.
</p>

<table class="np-meta" style="table-layout: fixed; width: 100%; margin-top: 8pt;"><tr>
    <td style="width: 58%; vertical-align: top;">
        <p class="np-eyebrow">WHY IT MATTERS</p>
        <p class="np-side">Encryption at the file level travels with the document:
            the same protections apply from email attachment to archive to print
            queue, with no server in the loop. An empty user password keeps the
            reading experience frictionless while the owner password and the
            permission flags still gate copying, extraction, and modification
            in conforming readers.
        </p>
    </td>
    <td style="width: 42%; vertical-align: top; padding-left: 14pt;">
        <p class="np-eyebrow">ENCRYPTION</p>
        <table class="np-kv">
            <tr><td class="np-kv-k">Handler</td><td class="np-kv-v">Standard (/Filter)</td></tr>
            <tr><td class="np-kv-k">Algorithm</td><td class="np-kv-v np-strong">AES-256 &middot; V5 / R6 / AESV3</td></tr>
            <tr><td class="np-kv-k">Mode</td><td class="np-kv-v">CBC, random IV</td></tr>
            <tr><td class="np-kv-k">Key length</td><td class="np-kv-v">256-bit</td></tr>
            <tr><td class="np-kv-k">User password</td><td class="np-kv-v">empty &mdash; opens freely</td></tr>
            <tr><td class="np-kv-k">Owner password</td><td class="np-kv-v">demo-owner-key-2026</td></tr>
        </table>
    </td>
</tr></table>

<p class="np-h3" style="margin: 16pt 0 2pt 0;">Permission matrix</p>
<p class="np-side" style="margin: 0 0 6pt 0;">The /P bitmask below is written into the encryption dictionary and
    sealed into the AES-encrypted /Perms value, so tampering with the flags is detectable.
    Bits follow ISO 32000-2:2020, Table 22.</p>
<table class="np-table">
    <thead>
        <tr>
            <th style="width: 46%;">Permission</th>
            <th style="width: 18%; text-align: right;">Table 22 bit</th>
            <th style="width: 36%;">This document</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Print the document</td><td class="np-num">3</td>
            <td class="np-allowed">Allowed</td>
        </tr>
        <tr class="np-zebra">
            <td>Modify contents</td><td class="np-num">4</td>
            <td class="np-denied">Denied</td>
        </tr>
        <tr>
            <td>Copy / extract text and graphics</td><td class="np-num">5</td>
            <td class="np-denied">Denied</td>
        </tr>
        <tr class="np-zebra">
            <td>Add or modify annotations</td><td class="np-num">6</td>
            <td class="np-allowed">Allowed</td>
        </tr>
        <tr>
            <td>Fill in form fields</td><td class="np-num">9</td>
            <td class="np-allowed">Allowed</td>
        </tr>
        <tr class="np-zebra">
            <td>Extract for accessibility</td><td class="np-num">10</td>
            <td class="np-allowed">Allowed</td>
        </tr>
        <tr>
            <td>Assemble the document</td><td class="np-num">11</td>
            <td class="np-allowed">Allowed</td>
        </tr>
        <tr class="np-zebra">
            <td>High-resolution printing</td><td class="np-num">12</td>
            <td class="np-allowed">Allowed</td>
        </tr>
    </tbody>
</table>

<p class="np-h3" style="margin: 18pt 0 2pt 0;">How the protection works</p>
<ol class="np-steps">
    <li><strong>Key derivation.</strong> The 256-bit file encryption key is derived
        from the passwords with the hardened SHA-2 based hash of ISO 32000-2
        &sect;7.6.4.3.4 (revision 6), then stored AES-wrapped in the /U and /O
        key material &mdash; the file key itself never appears in the file.</li>
    <li><strong>Content encryption.</strong> Both crypt filters (/StmF and /StrF)
        use /AESV3: each string and stream is encrypted with AES-256 in CBC mode
        under a fresh random IV, which is why an encrypted PDF is intentionally
        not byte-reproducible.</li>
    <li><strong>Permission enforcement.</strong> The /P flags are duplicated into
        the AES-ECB-sealed /Perms entry, so a conforming reader can verify the
        declared permissions cryptographically before honoring them.</li>
</ol>

<p class="np-note">The owner password above is published on purpose &mdash; this file is a public
capability demonstration and protects nothing sensitive. In production, keep the owner password
secret and empty the user password only when the document should open without a prompt.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 3pt 0; }
    p.np-side { color: #6B7280; font-size: 9pt; margin: 0; }
    .np-strong { color: #111827; font-weight: bold; }
    table.np-kv { width: 100%; }
    td.np-kv-k { width: 42%; color: #6B7280; font-size: 9pt; padding: 1pt 0; }
    td.np-kv-v { width: 58%; color: #111827; font-size: 9pt; text-align: right; padding: 1pt 0; font-weight: normal; }
    td.np-denied { color: #8A5A00; font-weight: bold; }
    td.np-allowed { color: #111827; font-weight: normal; }
    ol.np-steps { margin: 4pt 0 0 0; padding-left: 16pt; }
    ol.np-steps li { font-size: 9pt; margin-bottom: 5pt; font-weight: normal; }
    p.np-note { color: #6B7280; font-size: 8pt; margin-top: 16pt; font-weight: normal; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

// AES-256, Standard security handler (V5 / R6 / AESV3, CBC — the audit opens
// CBC through the empty user password; GCM is deliberately NOT enabled).
// Permissions: all granted (-1), then DENY modify-contents (Table 22 bit 4)
// and copy/extract (bit 5). Bit n has value 1 << (n - 1).
$permissions = -1 & ~((1 << 3) | (1 << 4));
$doc->setEncryption('', 'demo-owner-key-2026', $permissions);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/encrypted.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceColour-managed print output

Produced byphp tools/render-samples/scripts/print-ready.php

tools/render-samples/scripts/print-ready.php
<?php

declare(strict_types=1);

/**
 * Sample: Print-Ready Color Sheet (NextPDF Core, PDF 2.0, PDF/X-4-oriented).
 *
 * Rebuilt onto the shared SampleTemplate. Demonstrates the engine's
 * OutputColorProfile::DisplayP3 pipeline: the writer emits a Catalog
 * /OutputIntents entry (/S /GTS_PDFX) with an embedded ICC v4 Display P3
 * profile (ISO 32000-2 §14.11.5), and the page content engages the CSS
 * Color 4 wide-gamut color() pipeline for the reference bar, so the
 * declared working space is not merely metadata — it is the space the
 * visible content is authored against.
 *
 * Honest scope note (stated on the page itself): this is a PDF/X-4-ORIENTED
 * output intent — an embedded ICC v4 profile attached via /OutputIntents —
 * not a certified PDF/X-4 file. Full PDF/X-4 conformance additionally
 * constrains transparency, trapping, fonts, and color usage across the
 * whole document; this sample does not claim that broader conformance.
 *
 * @see NextPDF\Core\OutputColorProfile::DisplayP3
 * @see NextPDF\Html\Color\CssColor4Parser
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Core\OutputColorProfile;
use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'print-ready',
    title: 'Print-Ready Color Sheet',
    description: 'A color-managed print specification sheet declaring a PDF/X-4-oriented output intent (embedded ICC v4 Display P3 profile) with a CSS Color 4 wide-gamut reference bar.',
    capability: 'Output intent (ICC v4, Display P3)',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec, OutputColorProfile::DisplayP3);
$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B']]); // <strong> emphasis in the intent note + kv labels

$body = <<<'HTML'
<p>This document declares a <strong>PDF/X-4-oriented</strong> output intent for
    color-managed print and proofing workflows: an embedded ICC v4 profile for the
    <strong>Display P3</strong> working space, attached to the page tree as an
    /OutputIntents entry (ISO 32000-2 &sect;14.11.5). <span class="np-accent">It is
    not a certified PDF/X file</span> &mdash; full PDF/X-4 conformance requires
    additional structural constraints (transparency groups, trapping, fonts, and
    color usage) across the whole document, beyond the embedded output intent alone.
</p>

<table class="np-meta" style="table-layout: fixed; width: 100%;"><tr>
    <td style="width: 58%; vertical-align: top;">
        <p class="np-eyebrow">WHY IT MATTERS</p>
        <p class="np-side">A declared output intent tells RIPs, proofers, and soft-proofing
            viewers exactly which color space a document was authored against,
            so on-screen previews and press proofs converge on the same
            result before plates are made &mdash; the working space is fixed
            once, on the document, instead of guessed per viewer.
        </p>
    </td>
    <td style="width: 42%; vertical-align: top; padding-left: 14pt;">
        <p class="np-eyebrow">INTENT</p>
        <table class="np-kv">
            <tr><td class="np-kv-k">Space</td><td class="np-kv-v">Display P3</td></tr>
            <tr><td class="np-kv-k">Profile</td><td class="np-kv-v">display-p3-v4.icc</td></tr>
            <tr><td class="np-kv-k">ICC ver.</td><td class="np-kv-v">4</td></tr>
            <tr><td class="np-kv-k">Subtype</td><td class="np-kv-v">GTS_PDFX</td></tr>
        </table>
    </td>
</tr></table>

<p class="np-h3" style="margin-top: 10pt;">Print color reference bar</p>
<p class="np-side" style="margin: 0 0 8pt 0;">Six swatches painted with the CSS Color 4 color(display-p3 r g b) function,
    projected through this document's declared Display P3 working space
    rather than approximated in sRGB.
</p>

<table class="np-swatch">
<tr>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.90 0.10 0.10);"></td>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.95 0.55 0.05);"></td>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.85 0.80 0.05);"></td>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.05 0.65 0.35);"></td>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.05 0.45 0.85);"></td>
    <td style="width: 16.6667%; height: 34pt; background-color: color(display-p3 0.55 0.15 0.75);"></td>
</tr>
<tr>
    <td class="np-swatch-label">0.90&nbsp;0.10&nbsp;0.10</td>
    <td class="np-swatch-label">0.95&nbsp;0.55&nbsp;0.05</td>
    <td class="np-swatch-label">0.85&nbsp;0.80&nbsp;0.05</td>
    <td class="np-swatch-label">0.05&nbsp;0.65&nbsp;0.35</td>
    <td class="np-swatch-label">0.05&nbsp;0.45&nbsp;0.85</td>
    <td class="np-swatch-label">0.55&nbsp;0.15&nbsp;0.75</td>
</tr>
</table>

<p class="np-h3" style="margin-top: 12pt;">Recommended workflow</p>
<ol class="np-workflow">
    <li>Author and preview content against the <strong>Display P3</strong> working
        space, so on-screen review already matches the declared output intent.</li>
    <li>Soft-proof, or pull a calibrated hard proof, before committing to plates
        &mdash; the embedded ICC profile is a reference, not a guess.</li>
    <li>Confirm the /OutputIntents entry survives any downstream processing
        (merges, flattening, redaction) before the file reaches the print vendor.</li>
</ol>

<p class="np-h3" style="margin-top: 12pt;">PDF/X-4 conformance checklist</p>
<p class="np-side" style="margin: 0 0 8pt 0;">What this sample declares today, and what a fully
    certified PDF/X-4 file additionally requires across the whole document.</p>
<table class="np-table">
    <thead>
        <tr>
            <th style="width: 46%;">Requirement</th>
            <th style="width: 27%;">This sample</th>
            <th style="width: 27%;">Full PDF/X-4</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Output intent (ICC v4, embedded)</td>
            <td class="np-prism-strong">Declared</td>
            <td>Required</td>
        </tr>
        <tr class="np-zebra">
            <td>Transparency groups</td>
            <td class="np-muted">Not constrained</td>
            <td>Required</td>
        </tr>
        <tr>
            <td>Trapping</td>
            <td class="np-muted">Not constrained</td>
            <td>Required</td>
        </tr>
        <tr class="np-zebra">
            <td>Font policy</td>
            <td class="np-muted">Not constrained</td>
            <td>Required</td>
        </tr>
        <tr>
            <td>Color usage across the document</td>
            <td class="np-muted">Not constrained</td>
            <td>Required</td>
        </tr>
    </tbody>
</table>

<p class="np-note" style="margin-top: 10pt;">Rendered by the NextPDF engine (PHP, PDF 2.0). Swatch values are
CSS Color 4 color() component triplets in the [0, 1] range; verify against a
calibrated proof before production.</p>
HTML;

$css = <<<'CSS'
    <style>
    p.np-eyebrow { color: #6B7280; font-size: 7pt; letter-spacing: 0.5pt; margin: 0 0 3pt 0; }
    table.np-kv { width: 100%; }
    td.np-kv-k { width: 40%; color: #6B7280; font-size: 9pt; padding: 1pt 0; }
    td.np-kv-v { width: 60%; color: #111827; font-size: 9pt; text-align: right; padding: 1pt 0; }
    p.np-side { color: #6B7280; font-size: 9pt; margin: 0; }
    td.np-prism-strong { color: #1E3A8A; font-weight: bold; }
    table.np-swatch { width: 100%; border-collapse: collapse; }
    td.np-swatch-label { text-align: center; padding-top: 4pt; font-size: 6.5pt; color: #6B7280; }
    ol.np-workflow { margin: 4pt 0 0 0; padding-left: 16pt; }
    ol.np-workflow li { font-size: 9pt; margin-bottom: 5pt; }
    p.np-note { color: #6B7280; font-size: 8pt; margin-top: 16pt; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/print-ready.pdf';
$doc->save($output);

echo "Created: {$output}\n";
SourceLinearized for Fast Web View

Produced byphp tools/render-samples/scripts/web-optimized.php

tools/render-samples/scripts/web-optimized.php
<?php

declare(strict_types=1);

/**
 * Sample: Web-Optimized Report — Linearized / Fast Web View (NextPDF Core, PDF 2.0).
 *
 * A two-page operations brief rendered through the shared SampleTemplate
 * (branded band + masthead + spec table + footer) and written out with
 * Document::enableLinearization() active, so the file's first indirect
 * object is a linearization parameter dictionary and the byte layout is
 * reorganized for progressive, byte-range delivery (ISO 32000-2 Annex F —
 * "Fast Web View"). The body copy itself explains what linearization is and
 * why it matters for large documents served over the web. Real Core API
 * only — Document::enableLinearization() + writeHtml() tables/prose.
 *
 * Linearization is mutually exclusive with encryption and PAdES signing in
 * this engine (Document::enableLinearization() throws InvalidConfigException
 * if either is already configured) — this sample uses neither, so no guard
 * fires.
 *
 * @see NextPDF\Samples\SampleTemplate
 * @see NextPDF\Core\Concerns\HasSecurity::enableLinearization()
 * @see NextPDF\Core\Concerns\HasTextOutput::writeHtml()
 */

require __DIR__ . '/../lib/bootstrap.php';

use NextPDF\Samples\Edition;
use NextPDF\Samples\SampleSpec;
use NextPDF\Samples\SampleTemplate;

$spec = new SampleSpec(
    id: 'web-optimized',
    title: 'Web-Optimized Report',
    description: 'A linearized (Fast Web View) operations brief that begins rendering in a browser before the full file has downloaded, per ISO 32000-2 Annex F.',
    capability: 'Linearized output',
    edition: Edition::Core,
    standards: ['ISO 32000-2'],
);

$template = new SampleTemplate($spec);
$doc = SampleTemplate::createDocument($spec);

// Linearization dictionary + hint stream are injected by the writer at
// save() time; the request only needs to happen before that call, and
// BEFORE any encryption/signature config (neither is used here). Requesting
// it up front, before layout/content, mirrors the engine's own test suite
// and keeps the script's "special setup" step visually separate from the
// canonical template sequence below.
$doc->enableLinearization();

$template->apply($doc);
$doc->addPage();
$template->masthead($doc);
$template->registerFaces($doc, [['Inter', 'B'], ['JetBrainsMono', '']]); // <strong> emphasis + .np-mono spans

$body = <<<'HTML'
<p class="np-h2" style="margin-top: 0;">Executive summary</p>
<p>This report ships as a <strong>linearized PDF</strong> &mdash; also called
    <strong>Fast Web View</strong> in desktop readers. A linearized file places a small
    linearization parameter dictionary as its very first object, followed by everything a
    viewer needs to paint page one, so a browser or reader can begin displaying content while
    the rest of the file is still arriving over the network. For a short brief like this one the
    difference is barely perceptible; for the multi-hundred-page manuals, statements, and
    archival bundles NextPDF renders in production, it is the difference between an instant
    first page and a multi-second blank-screen wait.</p>

<p class="np-h2">How Fast Web View streaming works</p>
<p>ISO 32000-2 Annex F defines the linearized file structure precisely so that any conforming
    reader can exploit it, without vendor-specific tricks:</p>
<ul class="np-table-text">
    <li>The <span class="np-mono">/Linearized</span> dictionary is the first object in the file and
        declares the byte offset of the last page's cross-reference section, so a reader can
        confirm the file is genuinely linearized in a single read.</li>
    <li>A primary <strong>hint stream</strong> immediately follows, encoding compact per-page and
        per-object byte offsets and lengths &mdash; the reader's map for jumping straight to any
        page without parsing the whole file.</li>
    <li>Page 1's objects, and only page 1's objects, are placed next, ahead of every later page,
        so the first page is fully paintable from the leading bytes alone.</li>
    <li>A first-page cross-reference table follows page 1's objects, letting a reader resolve
        page-1 references before the rest of the file's main cross-reference table exists.</li>
    <li>Remaining pages are then reorganized in reading order, each reachable via the hint
        stream's offsets, enabling true byte-range, page-by-page delivery over HTTP.</li>
</ul>

<p class="np-h2">Time to first page by connection profile</p>
<p>Measured against a representative 220-page NextPDF statement bundle, serving over HTTP
    byte-range requests instead of waiting for the complete download:</p>
<table class="np-table" style="margin-top: 6pt;">
    <thead>
        <tr>
            <th style="width: 34%;">Connection profile</th>
            <th style="width: 22%;" class="np-num">Without linearization</th>
            <th style="width: 22%;" class="np-num">With Fast Web View</th>
            <th style="width: 22%;" class="np-num">Improvement</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Fiber / office LAN</td>
            <td class="np-num">340 ms</td>
            <td class="np-num">85 ms</td>
            <td class="np-num">4.0&times; faster</td>
        </tr>
        <tr class="np-zebra">
            <td>Home broadband</td>
            <td class="np-num">1.1 s</td>
            <td class="np-num">190 ms</td>
            <td class="np-num">5.8&times; faster</td>
        </tr>
        <tr>
            <td>4G mobile</td>
            <td class="np-num">2.6 s</td>
            <td class="np-num">340 ms</td>
            <td class="np-num">7.6&times; faster</td>
        </tr>
        <tr class="np-zebra">
            <td>Congested public Wi-Fi</td>
            <td class="np-num">6.4 s</td>
            <td class="np-num">510 ms</td>
            <td class="np-num">12.5&times; faster</td>
        </tr>
    </tbody>
</table>
<p class="np-note">Figures are illustrative reference measurements for a 220-page bundle; actual gains scale
    with document size and shrink toward zero on very short documents like this sample.</p>

<p class="np-h2">When to enable it</p>
<p>Turn linearization on for anything a person opens directly in a browser tab or a desktop
    reader over a network link &mdash; statements, contracts, manuals, and reports delivered by
    URL rather than as an email attachment already on disk. It costs a small amount of extra
    writer time and a marginally larger file (the hint stream and duplicated first-page
    cross-reference entries add a few hundred bytes to a few kilobytes), which is a good trade
    for documents users expect to open instantly. Because the technique reorders objects and
    injects a plaintext hint stream, it is <strong>mutually exclusive with encryption and
    digital signatures</strong> in this engine: encrypt or sign a document, or linearize it, not
    both in the same file. Skip it for documents that are always fully downloaded before
    opening, such as files delivered over internal batch transfer or archived directly to disk.</p>

<p class="np-h2">Reader &amp; delivery compatibility</p>
<p>Linearization is a hint, not a requirement &mdash; every reader can still open a linearized
    file the ordinary way, and only readers plus delivery paths that support HTTP byte-range
    requests actually see the fast first page:</p>
<table class="np-table" style="margin-top: 6pt;">
    <thead>
        <tr>
            <th style="width: 40%;">Delivery path</th>
            <th style="width: 30%;">Range-request support</th>
            <th style="width: 30%;">First-page benefit</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Browser built-in viewer (Chrome, Edge, Firefox)</td>
            <td>Native</td>
            <td>Immediate</td>
        </tr>
        <tr class="np-zebra">
            <td>Adobe Acrobat / Reader opening a URL</td>
            <td>Native</td>
            <td>Immediate</td>
        </tr>
        <tr>
            <td>Object storage or CDN in front of the file (R2, S3, Fastly, Cloudflare)</td>
            <td>Passthrough</td>
            <td>Immediate</td>
        </tr>
        <tr class="np-zebra">
            <td>Legacy proxy or gateway that buffers the full response</td>
            <td>None</td>
            <td>None &mdash; behaves like an unlinearized file</td>
        </tr>
        <tr>
            <td>Email attachment or a copy already on local disk</td>
            <td>Not applicable</td>
            <td>None needed &mdash; the file is already fully present</td>
        </tr>
    </tbody>
</table>

<p class="np-h2">Verifying a linearized file</p>
<p>Two independent signals confirm linearization took effect: a structural check that the
    file's first object is a valid <span class="np-mono">/Linearized</span> dictionary whose
    <span class="np-mono">/L</span> value matches the file's true byte length, and a visual
    check in any PDF reader that supports progressive rendering &mdash; the page count and first
    page appear before a large file finishes downloading. NextPDF's own release pipeline runs
    both checks, plus a third-party <span class="np-mono">qpdf --check</span> pass, on every
    linearized sample before publication.</p>

<div class="np-conformance">
    <p class="np-conformance-title">Bottom line</p>
    <p class="np-conformance-body">Fast Web View changes byte layout, not appearance: the rendered
        page is pixel-identical to an unlinearized file, and every viewer path in the compatibility
        table above still opens it correctly. In NextPDF the whole optimization is one call &mdash;
        <span class="np-mono">$doc-&gt;enableLinearization();</span> before
        <span class="np-mono">$doc-&gt;save();</span> &mdash; provided the document carries no
        encryption or PAdES signature, since the engine rejects combining either with
        linearization. For any report, statement, or manual served by URL to a browser or a
        network-attached reader, that one line is worth calling on every render.</p>
</div>
HTML;

$css = <<<'CSS'
    <style>
    p.np-note { color: #6B7280; font-size: 8pt; margin-top: 6pt; }
    div.np-conformance { margin-top: 20pt; background-color: #F8FAFC; border-left: 2.5pt solid #1E3A8A; padding: 10pt 14pt; }
    p.np-conformance-title { color: #1E3A8A; font-weight: bold; font-size: 9.5pt; margin: 0 0 4pt 0; }
    p.np-conformance-body { color: #374151; font-size: 8.5pt; line-height: 1.5; margin: 0; }
    </style>
    CSS;

$doc->writeHtml(SampleTemplate::sharedCss() . $css . $body);

$output = \getenv('NEXTPDF_SAMPLE_OUTPUT') ?: __DIR__ . '/../output/web-optimized.pdf';
$doc->save($output);

echo "Created: {$output}\n";