diff --git a/src/core/annotation.js b/src/core/annotation.js index 365503cfe8b20..3a7c9ccdf7ed4 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -42,6 +42,7 @@ import { collectActions, escapeString, getInheritableProperty, + getParentToUpdate, getRotationMatrix, isNumberArray, lookupMatrix, @@ -2108,6 +2109,24 @@ class WidgetAnnotation extends Annotation { amendSavedDict(annotationStorage, dict) {} + setValue(dict, value, xref, changes) { + const { dict: parentDict, ref: parentRef } = getParentToUpdate( + dict, + this.ref, + xref + ); + if (!parentDict) { + dict.set("V", value); + } else if (!changes.has(parentRef)) { + const newParentDict = parentDict.clone(); + newParentDict.set("V", value); + changes.put(parentRef, { data: newParentDict }); + return newParentDict; + } + + return null; + } + async save(evaluator, task, annotationStorage, changes) { const storageEntry = annotationStorage?.get(this.data.id); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); @@ -2191,13 +2210,15 @@ class WidgetAnnotation extends Annotation { value, }; - dict.set( - "V", + const newParentDict = this.setValue( + dict, Array.isArray(value) ? value.map(stringToAsciiOrUTF16BE) - : stringToAsciiOrUTF16BE(value) + : stringToAsciiOrUTF16BE(value), + xref, + changes ); - this.amendSavedDict(annotationStorage, dict); + this.amendSavedDict(annotationStorage, newParentDict || dict); const maybeMK = this._getMKDict(rotation); if (maybeMK) { @@ -3111,7 +3132,8 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { }; const name = Name.get(value ? this.data.exportValue : "Off"); - dict.set("V", name); + this.setValue(dict, name, evaluator.xref, changes); + dict.set("AS", name); dict.set("M", `D:${getModificationDate()}`); if (flags !== undefined) { @@ -3170,24 +3192,8 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { }; const name = Name.get(value ? this.data.buttonValue : "Off"); - if (value) { - if (this.parent instanceof Ref) { - const parent = evaluator.xref.fetch(this.parent).clone(); - parent.set("V", name); - changes.put(this.parent, { - data: parent, - xfa: null, - needAppearances: false, - }); - } else if (this.parent instanceof Dict) { - this.parent.set("V", name); - } - } - - if (!this.parent) { - // If there is no parent then we must set the value in the field. - dict.set("V", name); + this.setValue(dict, name, evaluator.xref, changes); } dict.set("AS", name); diff --git a/src/core/core_utils.js b/src/core/core_utils.js index cd892ed6ff6c3..2951bf0425bab 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -146,6 +146,36 @@ function getInheritableProperty({ return values; } +/** + * Get the parent dictionary to update when a property is set. + * + * @param {Dict} dict - Dictionary from where to start the traversal. + * @param {Ref} ref - The reference to the dictionary. + * @param {XRef} xref - The `XRef` instance. + */ +function getParentToUpdate(dict, ref, xref) { + const visited = new RefSet(); + const firstDict = dict; + const result = { dict: null, ref: null }; + + while (dict instanceof Dict && !visited.has(ref)) { + visited.put(ref); + if (dict.has("T")) { + break; + } + ref = dict.getRaw("Parent"); + if (!(ref instanceof Ref)) { + return result; + } + dict = xref.fetch(ref); + } + if (dict instanceof Dict && dict !== firstDict) { + result.dict = dict; + result.ref = ref; + } + return result; +} + // prettier-ignore const ROMAN_NUMBER_MAP = [ "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM", @@ -672,6 +702,7 @@ export { getInheritableProperty, getLookupTableFactory, getNewAnnotationsMap, + getParentToUpdate, getRotationMatrix, getSizeInBytes, isAscii, diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 68452d8fed84a..7c840704d859d 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -2145,6 +2145,81 @@ describe("annotation", function () { ); }); + it("should save the text in two fields with the same name", async function () { + const textWidget1Ref = Ref.get(123, 0); + const textWidget2Ref = Ref.get(124, 0); + + const parentRef = Ref.get(125, 0); + textWidgetDict.set("Parent", parentRef); + const parentDict = new Dict(); + parentDict.set("Kids", [textWidget1Ref, textWidget2Ref]); + parentDict.set("T", "foo"); + const textWidget2Dict = textWidgetDict.clone(); + + const xref = new XRefMock([ + { ref: textWidget1Ref, data: textWidgetDict }, + { ref: textWidget2Ref, data: textWidget2Dict }, + { ref: parentRef, data: parentDict }, + helvRefObj, + ]); + partialEvaluator.xref = xref; + const task = new WorkerTask("test save"); + + const annotation1 = await AnnotationFactory.create( + xref, + textWidget1Ref, + annotationGlobalsMock, + idFactoryMock + ); + const annotation2 = await AnnotationFactory.create( + xref, + textWidget2Ref, + annotationGlobalsMock, + idFactoryMock + ); + const annotationStorage = new Map(); + annotationStorage.set(annotation1.data.id, { value: "hello world" }); + annotationStorage.set(annotation2.data.id, { value: "hello world" }); + const changes = new RefSetCache(); + + await annotation1.save( + partialEvaluator, + task, + annotationStorage, + changes + ); + await annotation2.save( + partialEvaluator, + task, + annotationStorage, + changes + ); + const data = await writeChanges(changes, xref); + expect(data.length).toEqual(5); + const [, , data1, data2, parentData] = data; + expect(data1.ref).toEqual(textWidget1Ref); + expect(data2.ref).toEqual(textWidget2Ref); + expect(parentData.ref).toEqual(parentRef); + + data1.data = data1.data.replace(/\(D:\d+\)/, "(date)"); + data2.data = data2.data.replace(/\(D:\d+\)/, "(date)"); + expect(data1.data).toEqual( + "123 0 obj\n" + + "<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " + + "<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " + + "/Parent 125 0 R /AP << /N 4 0 R>> /M (date)>>\nendobj\n" + ); + expect(data2.data).toEqual( + "124 0 obj\n" + + "<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " + + "<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " + + "/Parent 125 0 R /AP << /N 5 0 R>> /M (date)>>\nendobj\n" + ); + expect(parentData.data).toEqual( + "125 0 obj\n<< /Kids [123 0 R 124 0 R] /T (foo) /V (hello world)>>\nendobj\n" + ); + }); + it("should save rotated text", async function () { const textWidgetRef = Ref.get(123, 0); const xref = new XRefMock([ @@ -3080,6 +3155,7 @@ describe("annotation", function () { const parentDict = new Dict(); parentDict.set("V", Name.get("Off")); parentDict.set("Kids", [buttonWidgetRef]); + parentDict.set("T", "RadioGroup"); buttonWidgetDict.set("Parent", parentRef); const xref = new XRefMock([ @@ -3116,7 +3192,7 @@ describe("annotation", function () { ); expect(parentData.ref).toEqual(Ref.get(456, 0)); expect(parentData.data).toEqual( - "456 0 obj\n<< /V /Checked /Kids [123 0 R]>>\nendobj\n" + "456 0 obj\n<< /V /Checked /Kids [123 0 R] /T (RadioGroup)>>\nendobj\n" ); annotationStorage.set(annotation.data.id, { value: false }); @@ -3142,6 +3218,7 @@ describe("annotation", function () { const parentDict = new Dict(); parentDict.set("Kids", [buttonWidgetRef]); + parentDict.set("T", "RadioGroup"); buttonWidgetDict.set("Parent", parentRef); const xref = new XRefMock([ @@ -3178,7 +3255,7 @@ describe("annotation", function () { ); expect(parentData.ref).toEqual(Ref.get(456, 0)); expect(parentData.data).toEqual( - "456 0 obj\n<< /Kids [123 0 R] /V /Checked>>\nendobj\n" + "456 0 obj\n<< /Kids [123 0 R] /T (RadioGroup) /V /Checked>>\nendobj\n" ); });