From 71479fdd21518ba43422c19b40228126e5519d42 Mon Sep 17 00:00:00 2001
From: Calixte Denizet <calixte.denizet@gmail.com>
Date: Thu, 15 Jun 2023 20:43:57 +0200
Subject: [PATCH] [Editor] Avoid to have duplicated entries in the Annot array
 when saving an existing and modified annotation

---
 src/core/document.js  | 13 ++++++---
 src/core/worker.js    |  5 ++++
 src/display/api.js    | 12 ++++++++
 test/unit/api_spec.js | 67 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 93 insertions(+), 4 deletions(-)

diff --git a/src/core/document.js b/src/core/document.js
index 1bbd1c0e8..f18334597 100644
--- a/src/core/document.js
+++ b/src/core/document.js
@@ -258,7 +258,7 @@ class Page {
     );
   }
 
-  #replaceIdByRef(annotations, deletedAnnotations) {
+  #replaceIdByRef(annotations, deletedAnnotations, existingAnnotations) {
     for (const annotation of annotations) {
       if (annotation.id) {
         const ref = Ref.fromString(annotation.id);
@@ -270,6 +270,7 @@ class Page {
           deletedAnnotations.put(ref);
           continue;
         }
+        existingAnnotations?.put(ref);
         annotation.ref = ref;
         delete annotation.id;
       }
@@ -295,7 +296,8 @@ class Page {
     });
 
     const deletedAnnotations = new RefSet();
-    this.#replaceIdByRef(annotations, deletedAnnotations);
+    const existingAnnotations = new RefSet();
+    this.#replaceIdByRef(annotations, deletedAnnotations, existingAnnotations);
 
     const pageDict = this.pageDict;
     const annotationsArray = this.annotations.filter(
@@ -308,7 +310,10 @@ class Page {
     );
 
     for (const { ref } of newData.annotations) {
-      annotationsArray.push(ref);
+      // Don't add an existing annotation ref to the annotations array.
+      if (ref instanceof Ref && !existingAnnotations.has(ref)) {
+        annotationsArray.push(ref);
+      }
     }
 
     const savedDict = pageDict.get("Annots");
@@ -431,7 +436,7 @@ class Page {
       const newAnnotations = newAnnotationsByPage.get(this.pageIndex);
       if (newAnnotations) {
         deletedAnnotations = new RefSet();
-        this.#replaceIdByRef(newAnnotations, deletedAnnotations);
+        this.#replaceIdByRef(newAnnotations, deletedAnnotations, null);
         newAnnotationsPromise = AnnotationFactory.printNewAnnotations(
           partialEvaluator,
           task,
diff --git a/src/core/worker.js b/src/core/worker.js
index d4b028156..00e96e92a 100644
--- a/src/core/worker.js
+++ b/src/core/worker.js
@@ -823,6 +823,11 @@ class WorkerMessageHandler {
           .ensureXRef("trailer")
           .then(trailer => trailer.get("Prev"));
       });
+      handler.on("GetAnnotArray", function (data) {
+        return pdfManager.getPage(data.pageIndex).then(function (page) {
+          return page.annotations.map(a => a.toString());
+        });
+      });
     }
 
     return workerHandlerName;
diff --git a/src/display/api.js b/src/display/api.js
index 3485b341e..f4589953c 100644
--- a/src/display/api.js
+++ b/src/display/api.js
@@ -780,6 +780,11 @@ class PDFDocumentProxy {
           return this._transport.getXRefPrevValue();
         },
       });
+      Object.defineProperty(this, "getAnnotArray", {
+        value: pageIndex => {
+          return this._transport.getAnnotArray(pageIndex);
+        },
+      });
     }
   }
 
@@ -2405,6 +2410,13 @@ class WorkerTransport {
           return this.messageHandler.sendWithPromise("GetXRefPrevValue", null);
         },
       });
+      Object.defineProperty(this, "getAnnotArray", {
+        value: pageIndex => {
+          return this.messageHandler.sendWithPromise("GetAnnotArray", {
+            pageIndex,
+          });
+        },
+      });
     }
   }
 
diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js
index e7d3028cf..98c5d5bc9 100644
--- a/test/unit/api_spec.js
+++ b/test/unit/api_spec.js
@@ -2008,6 +2008,73 @@ describe("api", function () {
       await loadingTask.destroy();
     });
 
+    it("edit and write an existing annotation, save the pdf and check that the Annot array doesn't contain dup entries", async function () {
+      let loadingTask = getDocument(buildGetDocumentParams("issue14438.pdf"));
+      let pdfDoc = await loadingTask.promise;
+      pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", {
+        annotationType: AnnotationEditorType.FREETEXT,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        fontSize: 10,
+        color: [0, 0, 0],
+        value: "Hello PDF.js World!",
+        pageIndex: 0,
+        id: "10R",
+      });
+      pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
+        annotationType: AnnotationEditorType.FREETEXT,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        fontSize: 10,
+        color: [0, 0, 0],
+        value: "Hello PDF.js World!",
+        pageIndex: 0,
+      });
+
+      const data = await pdfDoc.saveDocument();
+      await loadingTask.destroy();
+
+      loadingTask = getDocument(data);
+      pdfDoc = await loadingTask.promise;
+      const annotations = await pdfDoc.getAnnotArray(0);
+
+      expect(annotations).toEqual([
+        "4R",
+        "10R",
+        "17R",
+        "20R",
+        "21R",
+        "22R",
+        "25R",
+        "28R",
+        "29R",
+        "30R",
+        "33R",
+        "36R",
+        "37R",
+        "42R",
+        "43R",
+        "44R",
+        "47R",
+        "50R",
+        "51R",
+        "54R",
+        "55R",
+        "58R",
+        "59R",
+        "62R",
+        "63R",
+        "66R",
+        "69R",
+        "72R",
+        "75R",
+        "78R",
+        "140R",
+      ]);
+
+      await loadingTask.destroy();
+    });
+
     describe("Cross-origin", function () {
       let loadingTask;
       function _checkCanLoad(expectSuccess, filename, options) {