fix: ensure PDF form appearance streams have required /Subtype /Form entry (#2328)

When flattening PDF forms, some appearance streams lack the required
/Subtype /Form dictionary entry needed when used as XObjects. This
causes
corruption in Adobe Reader which fails to render these flattened fields.

Per PDF spec, Form XObject streams require:
- /Subtype /Form (required)
- /FormType 1 (optional)

The normalizeAppearanceStream function ensures these entries exist
before
adding appearance streams as XObjects to the page content stream.

Fixes rendering issues where flattened fields don't display in PDF
viewers.
This commit is contained in:
Lucas Smith 2025-12-16 16:00:11 +11:00 committed by GitHub
parent c5c87e3fd1
commit f129968968
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4,8 +4,10 @@ import {
PDFDict,
type PDFDocument,
PDFName,
PDFNumber,
PDFRadioGroup,
PDFRef,
PDFStream,
drawObject,
popGraphicsState,
pushGraphicsState,
@ -103,6 +105,36 @@ const getAppearanceRefForWidget = (field: PDFField, widget: PDFWidgetAnnotation)
}
};
/**
* Ensures that an appearance stream has the required dictionary entries to be
* used as a Form XObject. Some PDFs have appearance streams that are missing
* the /Subtype /Form entry, which causes Adobe Reader to fail to render them.
*
* Per PDF spec, a Form XObject stream requires:
* - /Subtype /Form (required)
* - /BBox (required, but should already exist for appearance streams)
* - /FormType 1 (optional, defaults to 1)
*/
const normalizeAppearanceStream = (document: PDFDocument, appearanceRef: PDFRef) => {
const appearanceStream = document.context.lookup(appearanceRef);
if (!(appearanceStream instanceof PDFStream)) {
return;
}
const dict = appearanceStream.dict;
// Ensure /Subtype /Form is set (required for XObject Form)
if (!dict.has(PDFName.of('Subtype'))) {
dict.set(PDFName.of('Subtype'), PDFName.of('Form'));
}
// Ensure /FormType is set (optional, but good practice)
if (!dict.has(PDFName.of('FormType'))) {
dict.set(PDFName.of('FormType'), PDFNumber.of(1));
}
};
const flattenWidget = (document: PDFDocument, field: PDFField, widget: PDFWidgetAnnotation) => {
try {
const page = getPageForWidget(document, widget);
@ -117,6 +149,9 @@ const flattenWidget = (document: PDFDocument, field: PDFField, widget: PDFWidget
return;
}
// Ensure the appearance stream has required XObject Form dictionary entries
normalizeAppearanceStream(document, appearanceRef);
const xObjectKey = page.node.newXObject('FlatWidget', appearanceRef);
const rectangle = widget.getRectangle();