mirror of
https://github.com/documenso/documenso
synced 2026-04-21 13:27:18 +00:00
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.
170 lines
4.4 KiB
TypeScript
170 lines
4.4 KiB
TypeScript
import type { PDFField, PDFWidgetAnnotation } from '@cantoo/pdf-lib';
|
|
import {
|
|
PDFCheckBox,
|
|
PDFDict,
|
|
type PDFDocument,
|
|
PDFName,
|
|
PDFNumber,
|
|
PDFRadioGroup,
|
|
PDFRef,
|
|
PDFStream,
|
|
drawObject,
|
|
popGraphicsState,
|
|
pushGraphicsState,
|
|
rotateInPlace,
|
|
translate,
|
|
} from '@cantoo/pdf-lib';
|
|
import fontkit from '@pdf-lib/fontkit';
|
|
|
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
|
|
|
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
|
const context = document.context;
|
|
const catalog = context.lookup(context.trailerInfo.Root);
|
|
if (catalog instanceof PDFDict) {
|
|
catalog.delete(PDFName.of('OCProperties'));
|
|
}
|
|
};
|
|
|
|
export const flattenForm = async (document: PDFDocument) => {
|
|
removeOptionalContentGroups(document);
|
|
|
|
const form = document.getForm();
|
|
|
|
const fontNoto = await fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
|
async (res) => res.arrayBuffer(),
|
|
);
|
|
|
|
document.registerFontkit(fontkit);
|
|
|
|
const font = await document.embedFont(fontNoto);
|
|
|
|
form.updateFieldAppearances(font);
|
|
|
|
for (const field of form.getFields()) {
|
|
for (const widget of field.acroField.getWidgets()) {
|
|
flattenWidget(document, field, widget);
|
|
}
|
|
|
|
try {
|
|
form.removeField(field);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getPageForWidget = (document: PDFDocument, widget: PDFWidgetAnnotation) => {
|
|
const pageRef = widget.P();
|
|
|
|
let page = document.getPages().find((page) => page.ref === pageRef);
|
|
|
|
if (!page) {
|
|
const widgetRef = document.context.getObjectRef(widget.dict);
|
|
|
|
if (!widgetRef) {
|
|
return null;
|
|
}
|
|
|
|
page = document.findPageForAnnotationRef(widgetRef);
|
|
|
|
if (!page) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return page;
|
|
};
|
|
|
|
const getAppearanceRefForWidget = (field: PDFField, widget: PDFWidgetAnnotation) => {
|
|
try {
|
|
const normalAppearance = widget.getNormalAppearance();
|
|
let normalAppearanceRef: PDFRef | null = null;
|
|
|
|
if (normalAppearance instanceof PDFRef) {
|
|
normalAppearanceRef = normalAppearance;
|
|
}
|
|
|
|
if (
|
|
normalAppearance instanceof PDFDict &&
|
|
(field instanceof PDFCheckBox || field instanceof PDFRadioGroup)
|
|
) {
|
|
const value = field.acroField.getValue();
|
|
const ref = normalAppearance.get(value) ?? normalAppearance.get(PDFName.of('Off'));
|
|
|
|
if (ref instanceof PDFRef) {
|
|
normalAppearanceRef = ref;
|
|
}
|
|
}
|
|
|
|
return normalAppearanceRef;
|
|
} catch (error) {
|
|
console.error(error);
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
if (!page) {
|
|
return;
|
|
}
|
|
|
|
const appearanceRef = getAppearanceRefForWidget(field, widget);
|
|
|
|
if (!appearanceRef) {
|
|
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();
|
|
const operators = [
|
|
pushGraphicsState(),
|
|
translate(rectangle.x, rectangle.y),
|
|
...rotateInPlace({ ...rectangle, rotation: 0 }),
|
|
drawObject(xObjectKey),
|
|
popGraphicsState(),
|
|
].filter((op) => !!op);
|
|
|
|
page.pushOperators(...operators);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|