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 (, 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. // * 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'

One invoice, two audiences: this page is the human-readable layer of a hybrid Factur-X e-invoice, and the same invoice travels beside it as EN 16931 structured data (factur-x.xml) that accounts-payable platforms post without retyping a figure.

HTML; $body = <<<'HTML'
SELLERBILLED TO
PATEON / NextPDF Northwind Trading Co.
VAT US94105X042 · United States Buyer reference NW-AP-2026-0042 · United States
NumberINV-2026-0042 Issued2026-06-27
TypeCommercial invoice (380) Due2026-07-27
TermsNet 30 CurrencyUSD
# Description Qty Unit price Amount
1NextPDF Pro License — annual subscription 2$299.00$598.00
2NextPDF Enterprise License — annual subscription 1$999.00$999.00
3Priority Support — 12 months, 4-hour SLA 1$199.00$199.00
4Onboarding and integration workshop (remote) 1$450.00$450.00
Net total$2,246.00
VAT 8.5% (category S)$190.91
Amount due (USD)$2,436.91

EN 16931 machine-readable layer

Attachment factur-x.xml — UN/CEFACT Cross-Industry Invoice
Profile EN 16931 (Factur-X 1.08 / ZUGFeRD 2.4)
Specification (BT-24) urn:cen.eu:en16931:2017
Association AFRelationship /Alternative — an equivalent representation of this page
XMP declaration fx:DocumentType INVOICE · fx:ConformanceLevel EN 16931
Carrier PDF/A-3b (ISO 19005-3:2012), fonts embedded, sRGB output intent

Accounting systems extract and post the XML directly; auditors and people keep this archival page — one file, byte-for-byte the same invoice in both worlds.

Payment due within 30 days of the invoice date (Net 30) — payable by 2026-07-27. Reference INV-2026-0042 on all remittances. Demonstration data only. All amounts in USD.

HTML; $css = <<<'CSS' CSS; // Two calls by design: the writer starts each writeHtml() from the clean body // state, so the lead's 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 PdfAManager — is 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";