Supported now: PDF/A-1b/1a, PDF/A-2b/2u/2a, PDF/A-3b/3u/3a

Pass a PdfAConformance value to render an archival-conformant PDF instead of the default output:

byte[] pdf = HtmlToPdf.Render(html, PdfAConformance.PdfA2b);

// or asynchronously
byte[] pdf = await HtmlToPdf.RenderAsync(html, PdfAConformance.PdfA3u);

Each render with a conformance level set:

  • Embeds a real ICC sRGB output intent, so color reproduces the same on any conformant viewer
  • Embeds spec-correct XMP metadata declaring the pdfaid:part/pdfaid:conformance identifiers
  • Embeds every font referenced in the document — including the standard 14 (Helvetica, Times, Courier, etc.), which render output normally leaves non-embedded since every PDF viewer has them built in. PDF/A requires full embedding regardless
  • Guarantees correct Unicode text extraction (the u conformance letter) — every embedded font always carries a real ToUnicode CMap
  • Rejects the combination with PdfEncryption by throwing InvalidOperationException — PDF/A forbids encryption outright, so a silently mislabeled PDF is never produced

PdfA3b/PdfA3u additionally permit embedding arbitrary file attachments (the mechanism ZUGFeRD/Factur-X e-invoicing builds on — see below).

PdfA1b/PdfA1a (PDF/A-1, based on PDF 1.4 — note ISO 19005-1 has no u level, only a/b, unlike parts 2/3) write a %PDF-1.4 header instead of 1.7, and — because PDF/A-1 forbids transparency outright — throw InvalidOperationException if the document uses CSS opacity below 1, a mix-blend-mode other than normal, or an image with an alpha channel, rather than silently dropping the effect or shipping a mislabeled non-conformant PDF. Use PdfA2b/2u/ 3b/3u for documents that need any of those.

The A conformance letter (PdfA1a, PdfA2a, PdfA3a) is PDF/A plus full accessibility tagging as a single named level — it requires tagged: true (see the PDF/UA-1 section below) and throws otherwise, rather than claim accessibility conformance with no structure tree:

byte[] pdf = HtmlToPdf.Render(html, PdfAConformance.PdfA2a, tagged: true);

Every entry point can reach it: the standalone HtmlToPdf.Render(html, PdfAConformance) overload shown above, PdfRenderOptions.Conformance (combinable with page size, margins, title, etc. in one call), the CLI's --pdfa <1b|1a|2b|2u|2a|3b|3u|3a> flag, and the REST API's options.conformance JSON field.

Supported now: ZUGFeRD / Factur-X (MINIMUM & EN 16931)

Pass a FacturXInvoice alongside PdfA3b or PdfA3u conformance to embed the invoice as a UN/CEFACT Cross Industry Invoice (CII) XML attachment — throws if conformance isn't PDF/A-3, since Factur-X's embedded-XML mechanism only exists there:

byte[] pdf = HtmlToPdf.Render(html, PdfAConformance.PdfA3b, new FacturXInvoice
{
    InvoiceNumber = "2026-042", IssueDate = DateTime.UtcNow, CurrencyCode = "EUR",
    SellerName = "Seller GmbH", BuyerName = "Buyer SARL",
    TaxBasisTotal = 100.00, TaxTotal = 20.00, GrandTotal = 120.00, DuePayableAmount = 120.00,
});

This embeds:

  • The CII invoice XML as a PDF/A-3 attachment (/EmbeddedFiles name tree, PDF 2.0 /AF associated-file array)
  • The Factur-X XMP extension schema (fx: namespace) declaring the attachment's document type, filename, version and conformance level, so Factur-X readers and PDF/A validators recognize it

The profile is derived from FacturXInvoice.LineItems rather than a separate enum. Leave it empty and you get the MINIMUM profile above — parties, dates and totals only, /AFRelationship /Data. Populate it and the writer instead produces EN 16931 (Comfort)-level output:

byte[] pdf = HtmlToPdf.Render(html, PdfAConformance.PdfA3b, new FacturXInvoice
{
    InvoiceNumber = "2026-042", IssueDate = DateTime.UtcNow, CurrencyCode = "EUR",
    SellerName = "Seller GmbH", BuyerName = "Buyer SARL",
    LineItems = { new FacturXLineItem
    {
        LineId = "1", ItemName = "Widget", NetUnitPrice = 10.00, BilledQuantity = 5,
        LineTotalAmount = 50.00, VatCategoryCode = "S", VatRatePercent = 20,
    } },
});

With line items present, the writer computes line-level tax per item, groups lines into a header-level tax breakdown by (VatCategoryCode, VatRatePercent), and computes the header monetary summation (TaxBasisTotalAmount/TaxTotalAmount/ GrandTotalAmount/DuePayableAmount) from the line items — any values set on the invoice's own header total properties are ignored rather than trusted, so the two can never disagree. The attachment relationship also switches to /AFRelationship /Alternative (required for German legal validity per the Factur-X spec), versus MINIMUM's /Data.

Scoping note: BASIC and EXTENDED are not modeled as separate profiles. Populating line items always targets EN 16931 — a strict superset of BASIC's requirements — but doesn't add EXTENDED-only fields (allowances/charges, multiple deliveries, additional references, etc.). Don't assume EXTENDED-level data is accepted or emitted.

Also reachable via PdfRenderOptions.Invoice, the CLI's --invoice <path-to-json> flag (JSON field names match FacturXInvoice's properties, including lineItems), and the REST API's options.invoice JSON object (with its own lineItems array).

Supported now: PDF/UA-1 (accessibility)

Pass tagged: true to produce a tagged PDF — a structure tree linked to the page content, so screen readers and other assistive technology can navigate it instead of reading raw painted glyphs in an arbitrary order:

byte[] pdf = HtmlToPdf.Render(html, tagged: true);

// combinable with PDF/A conformance
byte[] pdf = HtmlToPdf.Render(html, PdfAConformance.PdfA2b, tagged: true);

This builds a structure tree from the document's semantic HTML elements:

  • h1–h6 → headings, p → paragraphs
  • table/tr/th/td → table structure, with <th scope> mapped to the PDF /Scope attribute
  • ul/ol/li → list structure
  • img → a Figure element with its alt text
  • a → a Link element, cross-referenced to its clickable annotation via an OBJR entry and a /StructParent key (ISO 14289-1 7.18.1) — not just wrapped as ordinary marked content. A multi-word link (e.g. "Click here") splits into one box per word for line-breaking, same as any inline element; every word on the same line shares one Link structure element and one annotation sized to the full line, not just the first word
  • nav/header/footer/aside/main/article/section → landmark regions. PDF 1.7 (what PDF/UA-1 is based on) has no native semantic-landmark structure types, so these use custom type names (Nav, Header, etc.) backed by a /RoleMap fallback to the nearest standard type (Div or Sect) — the mechanism ISO 32000-1 §14.7.5 defines for exactly this case, so a reader that doesn't recognize the custom name still gets valid fallback semantics

Every other element (wrapper divs, inline spans, text-run fragments from line wrapping) attaches to its nearest tagged ancestor rather than being dropped, so no content goes untagged even though not every HTML element gets its own structure type. The catalog also gets /MarkInfo, /Lang (from <html lang>), /ViewerPreferences /DisplayDocTitle, and the XMP pdfuaid:part identification with a required dc:title (defaulted to "Untitled Document" rather than omitted if none is set).

Combinable with named page groups (page: <name> + @page <name>): every group's content attaches to the same single Document structure tree root, in group order.

Reachable via the overloads above, PdfRenderOptions.Tagged, the CLI's --tagged flag, and the REST API's options.tagged field.

Known limitations: no inline emphasis/strong mapping; position:fixed and margin-box content (running headers/footers) isn't tagged. A link that wraps across multiple lines gets one Link structure element and one annotation per line (a rectangle can't itself wrap) rather than one continuous element spanning the whole link.

Supported now: PDF/UA-2 (ISO 14289-2:2024)

Pass PdfUaVersion.Ua2 alongside tagged: true to target PDF/UA-2 instead of the default UA-1. PDF/UA-2 is based on WTPDF and PDF 2.0 (ISO 32000-2) rather than PDF 1.7, so it needs more than just a different XMP identification:

byte[] pdf = HtmlToPdf.Render(html, tagged: true, PdfUaVersion.Ua2);

This produces:

  • A %PDF-2.0 header instead of the usual 1.7
  • A declared PDF 2.0 structure namespace (/Namespaces on StructTreeRoot, http://iso.org/pdf2/ssn) that every structure element references via /NS — veraPDF's UA-2 rule 8.2.4-1 requires every element to belong to a declared namespace
  • XMP pdfuaid:part of 2 plus a pdfuaid:rev of 2024, identifying the referenced ISO 32000-2 edition

Landmark regions (nav/header/footer/aside/main/article/section) resolve straight to their standard fallback type (Div or Sect) under UA-2, rather than the custom type name + /RoleMap indirection UA-1 uses. PDF 2.0's namespace-scoped role mapping (/RoleMapNS) is a materially different and more involved mechanism than PDF 1.7's flat /RoleMap — resolving directly to a standard type sidesteps needing it, at the cost of losing the semantic landmark distinction specifically under UA-2 (the content is still tagged, just as a generic Div/Sect rather than a Nav/Header/etc.).

PdfUaVersion.Ua2 cannot be combined with PdfAConformance — there is no defined joint PDF/A + PDF/UA-2 standard (unlike PDF/UA-1's level-A conformance); combining throws. Reachable via the overloads above, PdfRenderOptions.UaVersion, the CLI's --ua-version 2 flag (requires --tagged), and the REST API's options.uaVersion field.

Known limitations: internal #id links jump via a page destination (/Dest), not a PDF 2.0 structure destination (/SD); table cells still rely on the same <th scope>-derived /Scope attribute UA-1 uses rather than PDF 2.0's /Headers mechanism or its algorithmic-determination alternative; /Info is kept alongside XMP rather than dropped, since sources on whether PDF/UA-2 actually forbids it (rather than just deprecating it in favor of XMP) disagreed and removing it is a large, hard-to-reverse-safely change across every existing PDF-writing path — kept as the conservative choice pending clearer verification.

On the roadmap

Not yet implemented. Listed here so the gap is explicit rather than silently assumed — BLUEPRINT.md has the full dependency-ordered plan.

  • ZUGFeRD/Factur-X BASIC and EXTENDED as distinct profiles — EN 16931 (Comfort) is implemented (see above) and is a superset of BASIC's requirements, but EXTENDED-only fields (allowances/charges, multiple deliveries, additional references) are not modeled
  • PDF/UA-2 structure destinations, /Headers table cells, and full /RoleMapNS — see the known limitations above
  • WTPDF's Level 1 "for reuse" conformance (a weaker tier than PDF/UA-2's accessibility conformance, both defined by the same PDF Association WTPDF 1.0 specification) — not modeled; EggPdf's UA-2 support targets the accessibility tier only