Skip to content

v0.16.0 - PDF/A conformance

Latest

Choose a tag to compare

@danmolitor danmolitor released this 01 Sep 18:23
· 9 commits to main since this release

Forme now produces PDF/A-2 conforming archival documents — levels 2b, 2u,
and 2a
— verified by veraPDF in CI, and composable with PDF/UA-1 so a single
file can be both archival and accessible (the configuration
government/education/healthcare archives actually require).

⚠️ Correction — PDF/A output was never conformant before this release

If you used pdfa: "2b" (or "2a") in 0.15.0 or earlier, those files are not
valid PDF/A
— they carry an OutputIntent whose ICC profile was invalid, so no
conformant PDF/A reader would have accepted them as archival.

The embedded sRGB profile (srgb2014.icc) was never an ICC profile. A curl
that fetched it around 0.9.0 returned a Cloudflare "Just a moment…" HTML
challenge page instead of the binary; it was embedded unchecked and shipped in
every build since. Nothing caught it because nothing validated the output. It's
now replaced with a real, generated sRGB profile and validated in CI — and a
test asserts the embedded bytes are a valid ICC profile so this can't recur.

How to check: any file produced with pdfa before 0.16.0 will fail veraPDF's
PDF/A profile — verapdf -f 2b your-file.pdf reports FAIL. Re-render those
files with 0.16.0 (same input, same options) and they'll pass.

PDF/A-2b / 2u / 2a — verified

The same nine-document corpus the PDF/UA gate uses (five shipped templates + four
HTML fixtures) passes veraPDF's PDF/A-2b and PDF/A-2a profiles. Getting there:

  • A real sRGB OutputIntent — a generated, license-clean sRGB profile (~588
    bytes, ICC v4.3, Little CMS / MIT) embedded in the engine, replacing the
    invalid one above.
  • PDF/A XMP metadata — pdfaid part/level, plus the pdfaExtension description
    for the PDF/UA identification schema so PDF/A and PDF/UA compose.
  • A deterministic trailer /ID — derived from a content hash, not a
    timestamp or random bytes. Identical input still produces byte-identical
    output, so Forme's determinism guarantee (native == WASM, reproducible builds)
    holds in PDF/A mode too — a property archival pipelines care about.
  • /F (Print) on link annotations, and the font-embedding check now accepts
    the pdfUa metric-compatible substitution — so pdfA + pdfUa no longer throws.
  • No width-consistency issue: the AFM-widths-by-construction font design
    (with its carve-out) satisfies PDF/A's width check — geometry stays identical
    across standard and archival output.

Archival and accessible

PDF/A composes with PDF/UA-1. Set both:

import { standardFonts } from '@formepdf/fonts-standard';

<Document pdfa="2a" pdfUa lang="en-US" fonts={standardFonts()}></Document>

Every corpus file is CI-gated against PDF/A-2b, PDF/A-2a, and PDF/UA-1
together
(scripts/verify-pdfa.mjs) — the claim is enforced, not observed.

API

  • JSX: <Document pdfa="2b" | "2u" | "2a"> (new: "2u").
  • HTML: pdfA in renderHtml options; --pdf-a <level> on the forme-html CLI.
  • Needs an embeddable font (@formepdf/fonts-standard). If none is registered, a
    PDF/A render fails by name with the remedy rather than emitting a file that
    falsely claims conformance.

Full changelog

engine ·
@formepdf/html ·
@formepdf/react