Guides

PDF/A from HTML: what it is, when you need it, how to produce it

Auditors, registries and archives ask for PDF/A. What the standard requires, why an ordinary browser PDF fails it, and how to convert.

M Mikel Rougstone · 31 August 2026 · 3 min read
PDF/A from HTML: what it is, when you need it, how to produce it

PDF/A is the archival profile of PDF: a file that will open identically in twenty years because everything it needs is inside it — fonts embedded, colours defined, no JavaScript, no encryption, no external references. If someone has asked you for it, they have a retention obligation, and an ordinary browser PDF will fail their validator.

Who asks for it, and why

  • Public-sector tenders and filings. Many procurement portals reject non-conformant PDFs outright.
  • Courts. Several jurisdictions mandate PDF/A for electronic filing.
  • Regulated industries. Pharmaceutical submissions, financial records, medical documentation.
  • Archives and libraries. Anything intended to outlive the software that made it.

The common thread is a retention period longer than the lifetime of the application that produced the document.

The conformance levels

LevelGuaranteesUse when
PDF/A-1bVisual appearance preservedLegacy requirements; strictest, oldest
PDF/A-1a1b plus tagged structure (accessibility)Accessibility is required
PDF/A-2b1b plus JPEG2000, transparency, layersMost common ask today
PDF/A-2u2b plus all text mapped to UnicodeText must be reliably extractable
PDF/A-32 plus arbitrary embedded filesHybrid e-invoicing (Factur-X, ZUGFeRD)

When a requirement just says "PDF/A", it almost always means 2b.

Why a browser PDF is not PDF/A

Chromium's page.pdf() produces a valid, compact PDF that is not conformant on three counts:

1. No output intent. The file does not declare which colour space "red" means. 2. Missing XMP metadata announcing the conformance level. 3. Font subsetting without the required metadata in some cases.

Viewers open it happily; a validator rejects it. That gap is why "we generate PDFs already" is not an answer to a PDF/A requirement.

Producing it

The practical route is to render normally, then convert with Ghostscript, which embeds an ICC profile, writes the output intent and the XMP declaration, and flattens anything the profile forbids:

gs -dPDFA=2 -dBATCH -dNOPAUSE -dNOOUTERSAVE \
   --permit-file-read=/usr/share/color/icc/ghostscript/ \
   -sColorConversionStrategy=RGB -sDEVICE=pdfwrite \
   -dPDFACompatibilityPolicy=1 \
   -sOutputFile=out.pdf pdfa_def.ps in.pdf

pdfa_def.ps is a short PostScript file naming the ICC profile. Two details cost us an afternoon: Ghostscript needs explicit permission to read the profile directory (--permit-file-read), and -dPDFACompatibilityPolicy=1 makes it fail loudly rather than silently emitting a non-conformant file.

Through PDFGeny it is a flag:

curl -X POST https://pdfgeny.com/api/v1/render \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"html":"<h1>Annual report</h1>","pdfa":true}' --output archive.pdf

Validate; do not assume

Ghostscript converts. It does not certify. Run a sample through veraPDF — the open-source reference validator used by archives — before promising conformance to a client:

verapdf --flavour 2b archive.pdf

Common failures even after conversion: transparency in PDF/A-1, embedded video or audio, encryption, and fonts whose licence bits forbid embedding.

You can also check quickly whether the markers landed:

grep -c GTS_PDFA1 archive.pdf     # output intent present
grep -c "pdfaid:part" archive.pdf # conformance declared in XMP

What you give up

  • File size grows. Embedded colour profiles and full font programs add tens to hundreds of

kilobytes per document.

  • Interactivity disappears. Scripts, embedded media and external links to files are removed or

flattened. That is the point: an archival file must not depend on anything outside itself.

  • Some visual effects change. Transparency may be flattened in stricter levels, which can alter

overlapping elements.

A practical policy

Generate ordinary PDFs for day-to-day delivery — they are smaller and faster — and PDF/A for the archival copy, at the moment the document becomes a record:

delivery = render(data)                    # emailed to the customer
archive  = render(data, pdfa=True)         # stored for the retention period

For invoices specifically, retention runs six to ten years across most of Europe; see what an invoice PDF must contain for the field-level requirements that go with it.

M

Mikel Rougstone

Founder, PDFGeny

I build and run PDFGeny — the API, the rendering fleet and the template catalog. Most of what I write here comes from something that broke in production first.