diff --git a/src/core/annotation.js b/src/core/annotation.js index 7268127632d53..ba22374530d75 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -428,7 +428,7 @@ class AnnotationFactory { } return { - annotations: await Promise.all(promises), + annotations: (await Promise.all(promises)).flat(), }; } @@ -1798,7 +1798,29 @@ class MarkupAnnotation extends Annotation { data: annotationDict, }); - return { ref: annotationRef }; + const retRef = { ref: annotationRef }; + if (annotation.popup) { + const popup = annotation.popup; + if (popup.deleted) { + annotationDict.delete("Popup"); + annotationDict.delete("Contents"); + annotationDict.delete("RC"); + return retRef; + } + const popupRef = (popup.ref ||= xref.getNewTemporaryRef()); + popup.parent = annotationRef; + const popupDict = PopupAnnotation.createNewDict(popup, xref); + changes.put(popupRef, { data: popupDict }); + annotationDict.setIfDefined( + "Contents", + stringToAsciiOrUTF16BE(popup.contents) + ); + annotationDict.set("Popup", popupRef); + + return [retRef, { ref: popupRef }]; + } + + return retRef; } static async createNewPrintAnnotation( @@ -3880,6 +3902,22 @@ class PopupAnnotation extends Annotation { this.data.open = !!dict.get("Open"); } + + static createNewDict(annotation, xref, _params) { + const { oldAnnotation, rect, parent } = annotation; + const popup = oldAnnotation || new Dict(xref); + popup.setIfNotExists("Type", Name.get("Annot")); + popup.setIfNotExists("Subtype", Name.get("Popup")); + popup.setIfNotExists("Open", false); + popup.setIfArray("Rect", rect); + popup.set("Parent", parent); + + return popup; + } + + static async createNewAppearanceStream(annotation, xref, params) { + return null; + } } class FreeTextAnnotation extends MarkupAnnotation { @@ -3947,18 +3985,27 @@ class FreeTextAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { color, fontSize, oldAnnotation, rect, rotation, user, value } = - annotation; + const { + color, + date, + fontSize, + oldAnnotation, + rect, + rotation, + user, + value, + } = annotation; const freetext = oldAnnotation || new Dict(xref); freetext.setIfNotExists("Type", Name.get("Annot")); freetext.setIfNotExists("Subtype", Name.get("FreeText")); + freetext.set( + oldAnnotation ? "M" : "CreationDate", + `D:${getModificationDate(date)}` + ); if (oldAnnotation) { - freetext.set("M", `D:${getModificationDate()}`); // TODO: We should try to generate a new RC from the content we've. // For now we can just remove it to avoid any issues. freetext.delete("RC"); - } else { - freetext.set("CreationDate", `D:${getModificationDate()}`); } freetext.setIfArray("Rect", rect); const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`; @@ -4492,6 +4539,7 @@ class InkAnnotation extends MarkupAnnotation { const { oldAnnotation, color, + date, opacity, paths, outlines, @@ -4503,7 +4551,10 @@ class InkAnnotation extends MarkupAnnotation { const ink = oldAnnotation || new Dict(xref); ink.setIfNotExists("Type", Name.get("Annot")); ink.setIfNotExists("Subtype", Name.get("Ink")); - ink.set(oldAnnotation ? "M" : "CreationDate", `D:${getModificationDate()}`); + ink.set( + oldAnnotation ? "M" : "CreationDate", + `D:${getModificationDate(date)}` + ); ink.setIfArray("Rect", rect); ink.setIfArray("InkList", outlines?.points || paths?.points); ink.setIfNotExists("F", 4); @@ -4730,13 +4781,23 @@ class HighlightAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { color, oldAnnotation, opacity, rect, rotation, user, quadPoints } = - annotation; - const date = `D:${getModificationDate()}`; + const { + color, + date, + oldAnnotation, + opacity, + rect, + rotation, + user, + quadPoints, + } = annotation; const highlight = oldAnnotation || new Dict(xref); highlight.setIfNotExists("Type", Name.get("Annot")); highlight.setIfNotExists("Subtype", Name.get("Highlight")); - highlight.set(oldAnnotation ? "M" : "CreationDate", date); + highlight.set( + oldAnnotation ? "M" : "CreationDate", + `D:${getModificationDate(date)}` + ); highlight.setIfArray("Rect", rect); highlight.setIfNotExists("F", 4); highlight.setIfNotExists("Border", [0, 0, 0]); @@ -5046,12 +5107,14 @@ class StampAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { oldAnnotation, rect, rotation, user } = annotation; - const date = `D:${getModificationDate(annotation.date)}`; + const { date, oldAnnotation, rect, rotation, user } = annotation; const stamp = oldAnnotation || new Dict(xref); stamp.setIfNotExists("Type", Name.get("Annot")); stamp.setIfNotExists("Subtype", Name.get("Stamp")); - stamp.set(oldAnnotation ? "M" : "CreationDate", date); + stamp.set( + oldAnnotation ? "M" : "CreationDate", + `D:${getModificationDate(date)}` + ); stamp.setIfArray("Rect", rect); stamp.setIfNotExists("F", 4); stamp.setIfNotExists("Border", [0, 0, 0]); diff --git a/src/core/document.js b/src/core/document.js index 5234f5ed44ed6..7138cbbf8cdb7 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -310,6 +310,12 @@ class Page { } continue; } + if (annotation.popup?.deleted) { + const popupRef = Ref.fromString(annotation.popupRef); + if (popupRef) { + deletedAnnotations.put(popupRef, popupRef); + } + } existingAnnotations?.put(ref); annotation.ref = ref; promises.push( diff --git a/src/shared/util.js b/src/shared/util.js index 2f5e057a8768a..593b6eca7a3fb 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1092,6 +1092,9 @@ function isArrayEqual(arr1, arr2) { } function getModificationDate(date = new Date()) { + if (!(date instanceof Date)) { + date = new Date(date); + } const buffer = [ date.getUTCFullYear().toString(), (date.getUTCMonth() + 1).toString().padStart(2, "0"), diff --git a/test/test_manifest.json b/test/test_manifest.json index e3e74f753461e..4fa04f526c6a9 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -12171,5 +12171,47 @@ "md5": "9fa985242476c642464d94893528e40f", "rounds": 1, "type": "eq" + }, + { + "id": "highlights-popup", + "file": "pdfs/highlights.pdf", + "md5": "55c12c918f3e2253b39b42075cb38205", + "rounds": 1, + "type": "eq", + "lastPage": 1, + "save": true, + "annotations": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "popup": { + "contents": "Hello PDF.js World" + }, + "pageIndex": 0, + "date": "2013-11-12T14:15:16Z", + "id": "612R" + } + } + }, + { + "id": "annotation-caret-ink-popup-deleted", + "file": "pdfs/annotation-caret-ink.pdf", + "md5": "6218ca235580d1975474c979e0128c2d", + "rounds": 1, + "type": "eq", + "lastPage": 1, + "save": true, + "annotations": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 15, + "popup": { + "deleted": true + }, + "pageIndex": 0, + "id": "25R", + "popupRef": "27R" + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 679f526b8f2ab..46617e5bac117 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4971,6 +4971,57 @@ describe("annotation", function () { OPS.endAnnotation, ]); }); + + it("should update an existing Highlight annotation", async function () { + const highlightDict = new Dict(); + highlightDict.set("Type", Name.get("Annot")); + highlightDict.set("Subtype", Name.get("Highlight")); + highlightDict.set("Rotate", 0); + highlightDict.set("CreationDate", "D:20190423"); + + const highlightRef = Ref.get(143, 0); + const xref = (partialEvaluator.xref = new XRefMock([ + { ref: highlightRef, data: highlightDict }, + ])); + const changes = new RefSetCache(); + + const task = new WorkerTask("test Highlight update"); + await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.HIGHLIGHT, + rotation: 90, + popup: { + contents: "Hello PDF.js World !", + }, + id: "143R", + ref: highlightRef, + oldAnnotation: highlightDict, + }, + ], + null, + changes + ); + + const data = await writeChanges(changes, xref); + + const popup = data[0]; + expect(popup.data).toEqual( + "1 0 obj\n" + + "<< /Type /Annot /Subtype /Popup /Open false /Parent 143 0 R>>\n" + + "endobj\n" + ); + + const base = data[1].data.replaceAll(/\(D:\d+\)/g, "(date)"); + expect(base).toEqual( + "143 0 obj\n" + + "<< /Type /Annot /Subtype /Highlight /Rotate 90 /CreationDate (date) /M (date) " + + "/F 4 /Contents (Hello PDF.js World !) /Popup 1 0 R>>\n" + + "endobj\n" + ); + }); }); describe("UnderlineAnnotation", function () { diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index 9d96e118e11a1..ac3b643212ea6 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -247,6 +247,7 @@ describe("util", function () { it("should get a correctly formatted date", function () { const date = new Date(Date.UTC(3141, 5, 9, 2, 6, 53)); expect(getModificationDate(date)).toEqual("31410609020653"); + expect(getModificationDate(date.toString())).toEqual("31410609020653"); }); });