Skip to content

Commit d473678

Browse files
Merge pull request #21577 from calixteman/fix/pdf-editor-acroform-resource-xref
Fix AcroForm appearance resources when merging pages
2 parents f472cb0 + 914545d commit d473678

2 files changed

Lines changed: 183 additions & 36 deletions

File tree

src/core/editor/pdf_editor.js

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2259,47 +2259,51 @@ class PDFEditor {
22592259
}
22602260
}
22612261
const resourcesValuesCache = new Map();
2262-
for (const field of drToFix) {
2263-
const ap = field.get("AP");
2264-
for (const [, value] of ap) {
2265-
if (!(value instanceof BaseStream)) {
2262+
const fixAppearanceResources = async stream => {
2263+
let resources = stream.dict.getRaw("Resources");
2264+
resources &&= this.xrefWrapper.fetchIfRef(resources);
2265+
if (!(resources instanceof Dict)) {
2266+
const newResourcesRef = await resourcesValuesCache.getOrInsertComputed(
2267+
acroFormDefaultResources,
2268+
() => this.#cloneObject(acroFormDefaultResources, xref)
2269+
);
2270+
stream.dict.set("Resources", newResourcesRef);
2271+
return;
2272+
}
2273+
for (const [
2274+
resKey,
2275+
resValue,
2276+
] of acroFormDefaultResources.getRawEntries()) {
2277+
if (resources.has(resKey)) {
22662278
continue;
22672279
}
2268-
let resources = value.dict.getRaw("Resources");
2269-
if (!resources) {
2270-
const newResourcesRef =
2271-
await resourcesValuesCache.getOrInsertComputed(
2272-
acroFormDefaultResources,
2273-
() => this.#cloneObject(acroFormDefaultResources, xref)
2274-
);
2275-
value.dict.set("Resources", newResourcesRef);
2276-
continue;
2280+
let newResValue = resValue;
2281+
if (resValue instanceof Ref) {
2282+
newResValue = await this.#collectDependencies(resValue, true, xref);
2283+
} else if (
2284+
resValue instanceof Dict ||
2285+
resValue instanceof BaseStream ||
2286+
Array.isArray(resValue)
2287+
) {
2288+
newResValue = await resourcesValuesCache.getOrInsertComputed(
2289+
resValue,
2290+
() => this.#cloneObject(resValue, xref)
2291+
);
22772292
}
2293+
resources.set(resKey, newResValue);
2294+
}
2295+
};
22782296

2279-
resources = xref.fetchIfRef(resources);
2280-
for (const [
2281-
resKey,
2282-
resValue,
2283-
] of acroFormDefaultResources.getRawEntries()) {
2284-
if (!resources.has(resKey)) {
2285-
let newResValue = resValue;
2286-
if (resValue instanceof Ref) {
2287-
newResValue = await this.#collectDependencies(
2288-
resValue,
2289-
true,
2290-
xref
2291-
);
2292-
} else if (
2293-
resValue instanceof Dict ||
2294-
resValue instanceof BaseStream ||
2295-
Array.isArray(resValue)
2296-
) {
2297-
newResValue = await resourcesValuesCache.getOrInsertComputed(
2298-
resValue,
2299-
() => this.#cloneObject(resValue, xref)
2300-
);
2297+
for (const field of drToFix) {
2298+
const ap = field.get("AP");
2299+
for (const [, value] of ap) {
2300+
if (value instanceof BaseStream) {
2301+
await fixAppearanceResources(value);
2302+
} else if (value instanceof Dict) {
2303+
for (const [, stream] of value) {
2304+
if (stream instanceof BaseStream) {
2305+
await fixAppearanceResources(stream);
23012306
}
2302-
resources.set(resKey, newResValue);
23032307
}
23042308
}
23052309
}

test/unit/api_spec.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7468,6 +7468,39 @@ small scripts as well as for`);
74687468
});
74697469

74707470
describe("AcroForm", function () {
7471+
const buildTextFieldPdf = (
7472+
fieldName,
7473+
fontKey = "Helv",
7474+
baseFont = "Helvetica"
7475+
) => {
7476+
const appearance = `BT /${fontKey} 10 Tf (x) Tj ET`;
7477+
return assemblePdf([
7478+
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
7479+
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
7480+
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] " +
7481+
"/Resources << >> /Annots [4 0 R] >>\nendobj\n",
7482+
"4 0 obj\n<< /Type /Annot /Subtype /Widget /FT /Tx " +
7483+
`/T (${fieldName}) /Rect [0 0 20 10] /AP << /N 5 0 R >> >>\nendobj\n`,
7484+
`5 0 obj\n<< /Subtype /Form /BBox [0 0 20 10] /Resources 7 0 R ` +
7485+
`/Length ${appearance.length} >>\n` +
7486+
`stream\n${appearance}\nendstream\nendobj\n`,
7487+
`6 0 obj\n<< /Fields [4 0 R] /DR << /Font << ` +
7488+
`/${fontKey} 8 0 R >> >> /DA (/${fontKey} 10 Tf) >>\nendobj\n`,
7489+
"7 0 obj\n<< >>\nendobj\n",
7490+
`8 0 obj\n<< /Type /Font /Subtype /Type1 ` +
7491+
`/BaseFont /${baseFont} >>\nendobj\n`,
7492+
]);
7493+
};
7494+
7495+
const getAppearanceFontName = async (pdfDoc, pageNumber) => {
7496+
const page = await pdfDoc.getPage(pageNumber);
7497+
const operatorList = await page.getOperatorList({
7498+
annotationMode: AnnotationMode.ENABLE,
7499+
});
7500+
const fontIndex = operatorList.fnArray.indexOf(OPS.setFont);
7501+
return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0];
7502+
};
7503+
74717504
it("extract page 2 and check AcroForm Fields T entries", async function () {
74727505
let loadingTask = getDocument(
74737506
buildGetDocumentParams("form_two_pages.pdf")
@@ -7623,6 +7656,116 @@ small scripts as well as for`);
76237656
expect(newPdfDoc.numPages).toEqual(2);
76247657
await newLoadingTask.destroy();
76257658
});
7659+
7660+
it("merges appearance resources from conflicting AcroForms", async function () {
7661+
let loadingTask = getDocument({
7662+
data: buildTextFieldPdf("first", "F1", "Helvetica"),
7663+
});
7664+
let pdfDoc = await loadingTask.promise;
7665+
const data = await pdfDoc.extractPages([
7666+
{ document: null },
7667+
{ document: buildTextFieldPdf("second", "F2", "Courier") },
7668+
]);
7669+
expect(data).not.toBeNull();
7670+
await loadingTask.destroy();
7671+
7672+
loadingTask = getDocument({ data });
7673+
pdfDoc = await loadingTask.promise;
7674+
expect(pdfDoc.numPages).toEqual(2);
7675+
expect(
7676+
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
7677+
).toEqual(["first", "second"]);
7678+
for (const pageNumber of [1, 2]) {
7679+
const fontName = await getAppearanceFontName(pdfDoc, pageNumber);
7680+
expect(fontName).not.toBeNull();
7681+
expect(fontName).not.toEqual("g_font_error");
7682+
}
7683+
await loadingTask.destroy();
7684+
});
7685+
7686+
it("replaces invalid appearance resources with default resources", async function () {
7687+
const appearance = "BT /Cour 10 Tf (x) Tj ET";
7688+
const broken = assemblePdf([
7689+
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
7690+
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
7691+
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] " +
7692+
"/Resources << >> /Annots [4 0 R] >>\nendobj\n",
7693+
"4 0 obj\n<< /Type /Annot /Subtype /Widget /FT /Tx /T (broken) " +
7694+
"/Rect [0 0 20 10] /AP << /N 5 0 R >> >>\nendobj\n",
7695+
"5 0 obj\n<< /Subtype /Form /BBox [0 0 20 10] " +
7696+
`/Resources /Nope /Length ${appearance.length} >>\n` +
7697+
`stream\n${appearance}\nendstream\nendobj\n`,
7698+
"6 0 obj\n<< /Fields [4 0 R] /DR << /Font << /Cour 7 0 R >> >> " +
7699+
"/DA (/Cour 10 Tf) >>\nendobj\n",
7700+
"7 0 obj\n<< /Type /Font /Subtype /Type1 " +
7701+
"/BaseFont /Courier >>\nendobj\n",
7702+
]);
7703+
7704+
let loadingTask = getDocument({ data: buildTextFieldPdf("main") });
7705+
let pdfDoc = await loadingTask.promise;
7706+
const data = await pdfDoc.extractPages([
7707+
{ document: null },
7708+
{ document: broken },
7709+
]);
7710+
expect(data).not.toBeNull();
7711+
await loadingTask.destroy();
7712+
7713+
loadingTask = getDocument({ data });
7714+
pdfDoc = await loadingTask.promise;
7715+
expect(pdfDoc.numPages).toEqual(2);
7716+
expect(
7717+
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
7718+
).toEqual(["broken", "main"]);
7719+
const fontName = await getAppearanceFontName(pdfDoc, 2);
7720+
expect(fontName).not.toBeNull();
7721+
expect(fontName).not.toEqual("g_font_error");
7722+
await loadingTask.destroy();
7723+
});
7724+
7725+
it("merges default resources into checkbox appearance streams", async function () {
7726+
const on = "/Tx BMC q BT /ZaDb 12 Tf (4) Tj ET Q EMC";
7727+
const withCheckbox = assemblePdf([
7728+
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 7 0 R >>\nendobj\n",
7729+
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
7730+
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] " +
7731+
"/Resources << >> /Annots [4 0 R] >>\nendobj\n",
7732+
"4 0 obj\n<< /Type /Annot /Subtype /Widget /FT /Btn /T (check) " +
7733+
"/Rect [0 0 20 20] /AS /On " +
7734+
"/AP << /N << /On 5 0 R /Off 6 0 R >> >> >>\nendobj\n",
7735+
`5 0 obj\n<< /Subtype /Form /BBox [0 0 20 20] /Resources 8 0 R ` +
7736+
`/Length ${on.length} >>\nstream\n${on}\nendstream\nendobj\n`,
7737+
"6 0 obj\n<< /Subtype /Form /BBox [0 0 20 20] /Resources 8 0 R " +
7738+
"/Length 1 >>\nstream\n \nendstream\nendobj\n",
7739+
"7 0 obj\n<< /Fields [4 0 R] /DR << " +
7740+
"/Font << /ZaDb 9 0 R >> " +
7741+
"/ColorSpace << /CsDefault /DeviceRGB >> >> " +
7742+
"/DA (/ZaDb 0 Tf) >>\nendobj\n",
7743+
"8 0 obj\n<< >>\nendobj\n",
7744+
"9 0 obj\n<< /Type /Font /Subtype /Type1 " +
7745+
"/BaseFont /ZapfDingbats >>\nendobj\n",
7746+
]);
7747+
7748+
let loadingTask = getDocument({ data: buildTextFieldPdf("main") });
7749+
let pdfDoc = await loadingTask.promise;
7750+
const data = await pdfDoc.extractPages([
7751+
{ document: null },
7752+
{ document: withCheckbox },
7753+
]);
7754+
expect(data).not.toBeNull();
7755+
expect(countMarker(data, "/CsDefault")).toEqual(1);
7756+
await loadingTask.destroy();
7757+
7758+
loadingTask = getDocument({ data });
7759+
pdfDoc = await loadingTask.promise;
7760+
expect(pdfDoc.numPages).toEqual(2);
7761+
expect(
7762+
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
7763+
).toEqual(["check", "main"]);
7764+
const fontName = await getAppearanceFontName(pdfDoc, 2);
7765+
expect(fontName).not.toBeNull();
7766+
expect(fontName).not.toEqual("g_font_error");
7767+
await loadingTask.destroy();
7768+
});
76267769
});
76277770

76287771
describe("Outlines", function () {

0 commit comments

Comments
 (0)