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:conformanceidentifiers - 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
uconformance letter) — every embedded font always carries a realToUnicodeCMap - Rejects the combination with
PdfEncryptionby throwingInvalidOperationException— 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 (
/EmbeddedFilesname tree, PDF 2.0/AFassociated-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→ paragraphstable/tr/th/td→ table structure, with<th scope>mapped to the PDF/Scopeattributeul/ol/li→ list structureimg→ a Figure element with itsalttexta→ a Link element, cross-referenced to its clickable annotation via anOBJRentry and a/StructParentkey (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 wordnav/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/RoleMapfallback to the nearest standard type (DivorSect) — 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.0header instead of the usual 1.7 - A declared PDF 2.0 structure namespace (
/Namespaceson 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:partof2plus apdfuaid:revof2024, 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,
/Headerstable 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