Skip to content

Commit 914545d

Browse files
committed
Fix AcroForm appearance resources when merging pages
Merging pages with conflicting AcroForm /DR must inline the resources each field's appearance relies on, since only one /DR survives in the output. That fixup: - resolved the appearance /Resources ref against the source document's xref instead of the merged one, so the lookup failed and merging threw (null.has(...)); - skipped checkbox/radio widgets, whose /AP /N is a sub-dictionary of appearance states rather than a single stream; - threw on a non-dictionary /Resources (e.g. a stray name) instead of falling back to the default resources.
1 parent c661ba1 commit 914545d

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
@@ -2264,47 +2264,51 @@ class PDFEditor {
22642264
}
22652265
}
22662266
const resourcesValuesCache = new Map();
2267-
for (const field of drToFix) {
2268-
const ap = field.get("AP");
2269-
for (const [, value] of ap) {
2270-
if (!(value instanceof BaseStream)) {
2267+
const fixAppearanceResources = async stream => {
2268+
let resources = stream.dict.getRaw("Resources");
2269+
resources &&= this.xrefWrapper.fetchIfRef(resources);
2270+
if (!(resources instanceof Dict)) {
2271+
const newResourcesRef = await resourcesValuesCache.getOrInsertComputed(
2272+
acroFormDefaultResources,
2273+
() => this.#cloneObject(acroFormDefaultResources, xref)
2274+
);
2275+
stream.dict.set("Resources", newResourcesRef);
2276+
return;
2277+
}
2278+
for (const [
2279+
resKey,
2280+
resValue,
2281+
] of acroFormDefaultResources.getRawEntries()) {
2282+
if (resources.has(resKey)) {
22712283
continue;
22722284
}
2273-
let resources = value.dict.getRaw("Resources");
2274-
if (!resources) {
2275-
const newResourcesRef =
2276-
await resourcesValuesCache.getOrInsertComputed(
2277-
acroFormDefaultResources,
2278-
() => this.#cloneObject(acroFormDefaultResources, xref)
2279-
);
2280-
value.dict.set("Resources", newResourcesRef);
2281-
continue;
2285+
let newResValue = resValue;
2286+
if (resValue instanceof Ref) {
2287+
newResValue = await this.#collectDependencies(resValue, true, xref);
2288+
} else if (
2289+
resValue instanceof Dict ||
2290+
resValue instanceof BaseStream ||
2291+
Array.isArray(resValue)
2292+
) {
2293+
newResValue = await resourcesValuesCache.getOrInsertComputed(
2294+
resValue,
2295+
() => this.#cloneObject(resValue, xref)
2296+
);
22822297
}
2298+
resources.set(resKey, newResValue);
2299+
}
2300+
};
22832301

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

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)