Avoid mutating source widget parents

This commit is contained in:
Calixte Denizet 2026-07-13 12:00:43 +02:00
parent 61acf93172
commit aa812b531a
2 changed files with 42 additions and 7 deletions

View File

@ -263,7 +263,7 @@ class PDFEditor {
) { ) {
if (obj instanceof Ref) { if (obj instanceof Ref) {
const { const {
currentDocument: { oldRefMapping }, currentDocument: { fieldToParent, oldRefMapping },
} = this; } = this;
const existingRef = oldRefMapping.get(obj); const existingRef = oldRefMapping.get(obj);
if (existingRef) { if (existingRef) {
@ -299,9 +299,17 @@ class PDFEditor {
} }
} }
let cloneSource = true;
if (fieldToParent.has(oldRef) && obj instanceof Dict) {
// Avoid following a widget's field hierarchy while cloning the page,
// without mutating the source dictionary cached by its XRef.
obj = this.cloneDict(obj);
obj.delete("Parent");
cloneSource = false;
}
this.xref[newRef.num] = await this.#collectDependencies( this.xref[newRef.num] = await this.#collectDependencies(
obj, obj,
true, cloneSource,
xref, xref,
resourceStreamPath resourceStreamPath
); );
@ -1223,11 +1231,8 @@ class PDFEditor {
"Sig" "Sig"
); );
const parentRef = annotationDict.getRaw("Parent") || null; const parentRef = annotationDict.getRaw("Parent") || null;
// We remove the parent to avoid visiting it when cloning the // The parent will be omitted from the annotation clone to avoid
// annotation. // visiting it, then restored by #mergeAcroForms.
// It'll be fixed later in #mergeAcroForms when merging the
// AcroForms.
annotationDict.delete("Parent");
fieldToParent.put(annotationRef, parentRef); fieldToParent.put(annotationRef, parentRef);
} }

View File

@ -7671,6 +7671,36 @@ small scripts as well as for`);
return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0]; return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0];
}; };
it("does not mutate source widget parents", async function () {
const pdfData = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/AcroForm 6 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Annots [4 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 20 10] " +
"/Parent 5 0 R >>\nendobj\n",
"5 0 obj\n<< /FT /Tx /T (group) /Kids [4 0 R] >>\nendobj\n",
"6 0 obj\n<< /Fields [] /DA (/Helv 10 Tf) >>\nendobj\n",
]);
let loadingTask = getDocument({ data: pdfData });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
const sourceAnnotations = await (
await pdfDoc.getPage(1)
).getAnnotations();
expect(sourceAnnotations[0].fieldName).toEqual("group");
expect(sourceAnnotations[0].fieldType).toEqual("Tx");
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
await loadingTask.destroy();
});
it("rebuilds a missing AcroForm Fields array", async function () { it("rebuilds a missing AcroForm Fields array", async function () {
const data = assemblePdf([ const data = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n", "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",