- adds PDF/A support required for fracturex / zugferd - #145
Conversation
Well, I let an AI do the review. This way we're even ;) Thanks for this — solid foundation for PDF/A (ID, OutputIntent + sRGB, uncompressed XMP, header/version handling for part 1). The writer change to honor context.header looks correct and necessary. A few points before merge: Blocking
convertToPDFA passes the metadata string into context.stream(), which uses typedArrayFor (charCodeAt → low byte). That: corrupts begin="\uFEFF" (becomes 0xFF instead of UTF-8 EF BB BF)
#27 is about loading broken/encrypted PDFs, not generating PDF/A. Upstream #1183 (or a dedicated “add PDF/A” issue) would be the right reference.
XMP is built once at call time. Later setTitle / setAuthor / setModificationDate / etc. update the Info dict but not /Metadata, which violates PDF/A consistency. Prefer regenerating XMP in prepareForSave when pdfAConformance is set (or document clearly + provide a refresh helper). Non-blocking / follow-ups |
68e120c to
8a0ceff
Compare
|
Great times we live in ;) I told my AI what your AI found, let it churn a bit, validated the output again and let it add a Test for the unicode issue. Now I ping it back to you and your AI. |
|
Thanks for the latest change. I noticed some minor problems remaining while reviewing but I'll handle that manually before the release. This PR will be included in the upcoming release today. |
|
Version 2.8.0 released |
Disclaimer
I let Claude Opus write this and only reviewed it. I ran some of the integration tests, which looked as goofy as before. I did not run e.g. RN integration tests.
I let it generate a mock zugferd invoice and validated it agains this online checker, as well as verapdf as local checker.
This itself does not bring in fracturX / zugferd. It just adds the PDF/A stuff which is required by those.
trying to keep this PR focused, and therefore this is not added here, although it might be the biggest use case of PDF/A. I might add a second PR adding those once this one is merged.
What?
Adds first-class PDF/A support to pdf-lib via a new public method,
PDFDocument.convertToPDFA(), plus the small building blocks it needs. This resolves the upstream request Hopding#1183, where the community has repeatedly asked for a built-in way to produce PDF/A / ZUGFeRD / Factur-X documents instead of hand-assembling the structures in userland.New public API:
PDFDocument.convertToPDFA(options?: ConvertToPDFAOptions): voidConvertToPDFAOptions—conformance(default'3B'),iccProfile(defaults to a bundled sRGB profile),outputConditionIdentifier,colorComponents.PDFAConformanceLeveltype +parseConformance()helper.getDefaultSRGBProfile()— returns the bundled sRGB profile bytes.buildPDFAMetadata()— builds the XMP packet (exported for advanced use / testing).What
convertToPDFA()does:/IDto the trailer (reuses an existing one if present).OutputIntent(GTS_PDFA1) referencing an embedded ICC profile. A standards-compliant sRGB IEC61966-2.1 (ICC v2) profile is bundled, so it works out of the box; callers can pass their own./Metadatastream declaring thepdfaidpart/conformance, kept consistent with the document information dictionary.Why?
PDF/A is the standard for long-term archiving and is the container format required by e-invoicing standards such as ZUGFeRD / Factur-X (PDF/A-3) — a recurring, high-demand ask (Hopding#1183, Hopding#230, Hopding#229). Until now users had to manually stitch together the
/ID,OutputIntent, ICC profile, and XMP metadata, which is error-prone and hard to get past a validator. This makes the common case a single call while leaving the escape hatches (custom ICC profile, conformance level) open.How?
The bulk of the logic lives in a new
src/api/pdfa/module (conformance parsing, XMP builder, bundled sRGB profile), andPDFDocument.convertToPDFA()orchestrates it using existing primitives (context.stream,context.register,catalog.set,getTitle()/getAuthor()/… for XMP consistency). No parsing/serialization internals were changed for the feature itself.One supporting change was required in the writers.
PDFWriterandPDFStreamWriterhardcoded the output header toPDFHeader.forVersion(1, 7), ignoringcontext.header— even thoughPDFParsersetscontext.headerfrom the source file. I changed both to honorcontext.header. The default is still1.7(that's whatPDFContextinitializes), so newly-created documents are unaffected; the observable change is that loaded documents now preserve their original header version on save instead of being silently promoted to 1.7, and PDF/A-1 can emit a1.4header. See "Anything Else?" for the compatibility note.Alternatives considered:
context.headeris the correct, minimal fix and makes the existing (parser-populated) field actually meaningful on the write path.convertToPDFAthrows a descriptive error for A-levels.Testing?
tests/api/pdfa.spec.ts(16 tests) covering conformance parsing, the XMP builder (including XML escaping and field mirroring), the bundled ICC profile, the output intent / metadata //ID/ header-version structures, the "no object streams for part 1" rule, custom-profile options, and the encrypted-document and unsupported-conformance error paths. Full suite: 710 passing.1B,2B,3B, and3Uand validated each with veraPDF 1.30.2: all reportisCompliant="true"with 0 failed checks.factur-x.xml(via existingattach()withAFRelationship.Alternative) plus the Factur-X XMP extension schema, and validated it with the Mustang / EU-Rechnung online validator: PDF part conformant, recognized as ZUGFeRD 2 (BASIC).yarn typecheck,yarn lint(0 errors), andyarn build(cjs/es/esm/umd + downlevel-dts) all pass. The Node integration app (apps/node) compiles against the built library.New Dependencies?
No.
(The default sRGB ICC profile is bundled as an inlined base64 constant in
src/api/pdfa/srgbProfile.ts— no runtime dependency is added. It is the freely-redistributable sRGB IEC61966-2.1 profile, © 1998 Hewlett-Packard, the same profile bundled by Ghostscript, Apache PDFBox, and Little CMS. It adds ~3 KB (binary) / ~4 KB (base64 source) to the bundle.Screenshots
N/A — the feature adds archival/metadata structures and does not change how existing content is rendered.
Suggested Reading?
Yes — in addition to the PDF spec sections listed in CONTRIBUTING.md, the relevant references for this change are ISO 19005 (PDF/A parts 1–3), the XMP specification (for the metadata packet and PDF/A extension-schema mechanism), and the ICC / OutputIntent handling in ISO 32000.
Anything Else?
Behavior change in the writers. Making
PDFWriter/PDFStreamWriterhonorcontext.headerchanges the emitted header for loaded documents from a forced1.7to their original version. All unit tests pass, as I said the node integration test also worked fine, I dont think it matters, but you should know.Content compliance is the caller's responsibility.
convertToPDFA()performs the structural changes but cannot make arbitrary content compliant — most importantly, drawn text must use an embedded font (the 14 standard fonts are not PDF/A compliant), and PDF/A also forbids encryption, transparency (part 1), JavaScript, etc. This is documented in the method's doc comment, which points users to veraPDF for validation.Checklist