Skip to content

fix: PDF/A-3 metadata and ICC profile validation errors#111

Merged
jslno merged 1 commit intojslno:mainfrom
chrizzellu:fix/pdfa3-metadata-and-icc
Feb 7, 2026
Merged

fix: PDF/A-3 metadata and ICC profile validation errors#111
jslno merged 1 commit intojslno:mainfrom
chrizzellu:fix/pdfa3-metadata-and-icc

Conversation

@chrizzellu
Copy link
Contributor

@chrizzellu chrizzellu commented Feb 6, 2026

Summary

Fixes PDF/A-3B validation errors reported by the official Factur-X validator at https://services.fnfe-mpe.org/. PDFs generated by embedInPdf now pass both PDF and XML validation. Relates to #110.

Root Cause

The XMP metadata stream contained xmlns:about="" (an empty XML namespace declaration), which is forbidden by the XML Namespace specification. This caused every strict XML parser — including the PDF/A validator — to reject the entire XMP document. The validator then:

  1. Could not read pdfaid:part=3, so it defaulted to testing as PDF/A-1b
  2. Reported all XMP metadata fields as null
  3. Failed EmbeddedFiles checks (forbidden in PDF/A-1, allowed in PDF/A-3)

This single typo (xmlns:about instead of rdf:about) was the root cause of all reported PDF validation errors.

Changes

1. Fix invalid xmlns:about=""rdf:about="" (formatter.ts)

The Factur-X rdf:Description block for fx:DocumentType etc. used @xmlns:about which generates xmlns:about="" — an empty namespace, invalid XML. Changed to @rdf:about which generates the correct rdf:about="" attribute.

This was the critical fix that made the entire XMP parseable.

2. Add dc:title and dc:description to XMP metadata (formatter.ts)

The Info dictionary fields Title and Subject (set via setTitle/setSubject) had no corresponding XMP entries, causing metadata mismatch errors. Now included with correct rdf:Alt / xml:lang="x-default" structure per XMP spec.

3. Fix dc:creator to use metadata.author (formatter.ts)

dc:creator was set to metadata.creator, but per PDF/XMP spec:

  • dc:creator must match Info /Author (= metadata.author)
  • xmp:CreatorTool must match Info /Creator (= metadata.creator)

Falls back to metadata.creator when author is not provided.

4. Encode XMP as UTF-8 bytes (formatter.ts)

The XMP string was passed directly to context.stream() with Length: xmp.length. JavaScript's .length counts UTF-16 code units, but the PDF stream contains UTF-8 bytes. The \uFEFF BOM (1 JS char → 3 UTF-8 bytes) caused a length mismatch that made validators fail to read the stream. Now uses TextEncoder().encode() for correct byte-level handling.

5. Add N: 3 to ICC profile stream (formatter.ts)

The sRGB ICC profile stream was missing the required N entry (number of color components), causing: "The N entry (value null) does not match the number of components in the embedded ICC profile (color space RGB)"

6. Load PDF with updateMetadata: false (create.ts)

PDFDocument.load() defaults to updateMetadata: true, which auto-sets Producer = "pdf-lib" and Creator = "pdf-lib" in the Info dictionary before embedInPdf can set the correct values. Now disabled to prevent stale metadata.

7. Save with useObjectStreams: false (create.ts)

Forces traditional cross-reference tables instead of xref streams for maximum PDF/A compatibility.

Validation Results

Before (from issue #110):

PDF: Not a PDF/A-3 (13 errors)
XML: valid

After:

PDF: flavour=3b, isCompliant=true, assertions=[] (0 errors)
XML: valid (32 rules fired, 0 failed, 8 XRechnung-specific notices)

Tested with Puppeteer-generated PDFs validated against the official Factur-X validator.

Test plan

  • Existing tests pass (2 pre-existing XSD validation failures in formatter.test.ts unrelated — require Java)
  • Generated PDFs validated at https://services.fnfe-mpe.org/ — PDF/A-3B compliant, XML valid
  • Tested with BASIC profile, real invoice data, EU reverse charge scenario

@vercel
Copy link
Contributor

vercel bot commented Feb 6, 2026

@chrizzellu is attempting to deploy a commit to the better-auth-extended Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 6, 2026

Open in StackBlitz

npm i https://pkg.pr.new/jslno/node-zugferd/@node-zugferd/api@111
npm i https://pkg.pr.new/jslno/node-zugferd@111

commit: 1c883fc

@chrizzellu chrizzellu force-pushed the fix/pdfa3-metadata-and-icc branch from 1f4ee52 to c476306 Compare February 6, 2026 22:18
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

@chrizzellu chrizzellu force-pushed the fix/pdfa3-metadata-and-icc branch 6 times, most recently from 8f4a912 to 5fc6277 Compare February 6, 2026 22:45
Fixes multiple PDF/A-3 validation errors reported by official Factur-X
validators. Relates to jslno#110.

## Changes

### 1. Add dc:title and dc:description to XMP metadata (formatter.ts)

The Info dictionary fields Title and Subject (set via pdf-lib's
setTitle/setSubject) had no corresponding XMP entries, causing
PDF/A validation errors:
- "Info /Title and XMP dc:title['x-default'] are not equivalent"
- "Info /Subject and XMP dc:description['x-default'] are not equivalent"

Now both fields are included in the XMP metadata template when
provided via metadata options. Uses rdf:Alt with xml:lang="x-default"
per XMP specification.

### 2. Fix dc:creator to use metadata.author (formatter.ts)

dc:creator in XMP was set to metadata.creator, but per the PDF/XMP
spec, dc:creator should match Info /Author (metadata.author), not
Info /Creator (metadata.creator). xmp:CreatorTool already correctly
uses metadata.creator. Falls back to metadata.creator when author
is not provided, for backward compatibility.

### 3. Add N entry to ICC profile stream dictionary (formatter.ts)

The sRGB ICC profile stream was missing the required N (number of
color components) entry, causing:
- "The N entry (value null) does not match the number of components
  in the embedded ICC profile (color space RGB)"

Added N: 3 to the profile stream dictionary (RGB = 3 components).

### 4. Save with useObjectStreams: false (create.ts)

PDF/A validators flag xref streams (from object streams) as
non-compliant. Saving with useObjectStreams: false forces
traditional cross-reference tables for maximum compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chrizzellu chrizzellu force-pushed the fix/pdfa3-metadata-and-icc branch from 5fc6277 to 1c883fc Compare February 6, 2026 23:01
@jslno jslno linked an issue Feb 7, 2026 that may be closed by this pull request
@jslno jslno merged commit 921c851 into jslno:main Feb 7, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

embedInPdf produces invalid PDF/A-3 - Multiple validation errors

2 participants