diff --git a/src/core/annotation.js b/src/core/annotation.js index 7937479d4..c47b79ffb 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -264,6 +264,9 @@ class AnnotationFactory { const promises = []; for (const annotation of annotations) { + if (annotation.deleted) { + continue; + } switch (annotation.annotationType) { case AnnotationEditorType.FREETEXT: if (!baseFontRef) { @@ -308,6 +311,9 @@ class AnnotationFactory { const { isOffscreenCanvasSupported } = evaluator.options; const promises = []; for (const annotation of annotations) { + if (annotation.deleted) { + continue; + } switch (annotation.annotationType) { case AnnotationEditorType.FREETEXT: promises.push( @@ -466,6 +472,7 @@ class Annotation { const MK = dict.get("MK"); this.setBorderAndBackgroundColors(MK); this.setRotation(MK); + this.ref = params.ref instanceof Ref ? params.ref : null; this._streams = []; if (this.appearance) { @@ -1467,7 +1474,7 @@ class MarkupAnnotation extends Annotation { } static async createNewAnnotation(xref, annotation, dependencies, params) { - const annotationRef = xref.getNewTemporaryRef(); + const annotationRef = annotation.ref || xref.getNewTemporaryRef(); const ap = await this.createNewAppearanceStream(annotation, xref, params); const buffer = []; let annotationDict; @@ -1497,11 +1504,17 @@ class MarkupAnnotation extends Annotation { const ap = await this.createNewAppearanceStream(annotation, xref, params); const annotationDict = this.createNewDict(annotation, xref, { ap }); - return new this.prototype.constructor({ + const newAnnotation = new this.prototype.constructor({ dict: annotationDict, xref, isOffscreenCanvasSupported: params.isOffscreenCanvasSupported, }); + + if (annotation.ref) { + newAnnotation.ref = newAnnotation.refToReplace = annotation.ref; + } + + return newAnnotation; } } @@ -1511,7 +1524,6 @@ class WidgetAnnotation extends Annotation { const { dict, xref } = params; const data = this.data; - this.ref = params.ref; this._needAppearances = params.needAppearances; data.annotationType = AnnotationType.WIDGET; diff --git a/src/core/document.js b/src/core/document.js index 93f31b795..d5789b543 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -41,7 +41,7 @@ import { XRefEntryException, XRefParseException, } from "./core_utils.js"; -import { Dict, isName, Name, Ref } from "./primitives.js"; +import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js"; import { getXfaFontDict, getXfaFontName } from "./xfa_fonts.js"; import { BaseStream } from "./base_stream.js"; import { calculateMD5 } from "./crypto.js"; @@ -258,6 +258,24 @@ class Page { ); } + #replaceIdByRef(annotations, deletedAnnotations) { + for (const annotation of annotations) { + if (annotation.id) { + const ref = Ref.fromString(annotation.id); + if (!ref) { + warn(`A non-linked annotation cannot be modified: ${annotation.id}`); + continue; + } + if (annotation.deleted) { + deletedAnnotations.put(ref); + continue; + } + annotation.ref = ref; + delete annotation.id; + } + } + } + async saveNewAnnotations(handler, task, annotations) { if (this.xfaFactory) { throw new Error("XFA: Cannot save new annotations."); @@ -276,8 +294,13 @@ class Page { options: this.evaluatorOptions, }); + const deletedAnnotations = new RefSet(); + this.#replaceIdByRef(annotations, deletedAnnotations); + const pageDict = this.pageDict; - const annotationsArray = this.annotations.slice(); + const annotationsArray = this.annotations.filter( + a => !(a instanceof Ref && deletedAnnotations.has(a)) + ); const newData = await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, @@ -401,11 +424,14 @@ class Page { const newAnnotationsByPage = !this.xfaFactory ? getNewAnnotationsMap(annotationStorage) : null; + let deletedAnnotations = null; let newAnnotationsPromise = Promise.resolve(null); if (newAnnotationsByPage) { const newAnnotations = newAnnotationsByPage.get(this.pageIndex); if (newAnnotations) { + deletedAnnotations = new RefSet(); + this.#replaceIdByRef(newAnnotations, deletedAnnotations); newAnnotationsPromise = AnnotationFactory.printNewAnnotations( partialEvaluator, task, @@ -446,6 +472,25 @@ class Page { newAnnotationsPromise, ]).then(function ([pageOpList, annotations, newAnnotations]) { if (newAnnotations) { + // Some annotations can already exist (if it has the refToReplace + // property). In this case, we replace the old annotation by the new + // one. + annotations = annotations.filter( + a => !(a.ref && deletedAnnotations.has(a.ref)) + ); + for (let i = 0, ii = newAnnotations.length; i < ii; i++) { + const newAnnotation = newAnnotations[i]; + if (newAnnotation.refToReplace) { + const j = annotations.findIndex( + a => a.ref && isRefsEqual(a.ref, newAnnotation.refToReplace) + ); + if (j >= 0) { + annotations.splice(j, 1, newAnnotation); + newAnnotations.splice(i--, 1); + ii--; + } + } + } annotations = annotations.concat(newAnnotations); } if ( diff --git a/src/core/primitives.js b/src/core/primitives.js index f19976c3a..30f81407b 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -279,6 +279,23 @@ class Ref { return `${this.num}R${this.gen}`; } + static fromString(str) { + const ref = RefCache[str]; + if (ref) { + return ref; + } + const m = /^(\d+)R(\d*)$/.exec(str); + if (!m || m[1] === "0") { + return null; + } + + // eslint-disable-next-line no-restricted-syntax + return (RefCache[str] = new Ref( + parseInt(m[1]), + !m[2] ? 0 : parseInt(m[2]) + )); + } + static get(num, gen) { const key = gen === 0 ? `${num}R` : `${num}R${gen}`; // eslint-disable-next-line no-restricted-syntax diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index dafc349b6..7d6b943dd 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -599,3 +599,4 @@ !bug1529502.pdf !issue16500.pdf !issue16538.pdf +!freetexts.pdf diff --git a/test/pdfs/freetexts.pdf b/test/pdfs/freetexts.pdf new file mode 100755 index 000000000..c4ff346c0 Binary files /dev/null and b/test/pdfs/freetexts.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 17e7c36bb..f78bae3ce 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -7720,5 +7720,91 @@ "md5": "35b691c3a343f4531bd287b001b67a77", "rounds": 1, "type": "eq" - } + }, + { + "id": "freetexts-editor-print", + "file": "pdfs/freetexts.pdf", + "md5": "da1310a25ab796c1201810070d5032a3", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 3, + "color": [0, 0, 255], + "fontSize": 21, + "value": "The content must have been changed", + "pageIndex": 0, + "rect": [ 92.58600000000003, 415.2426115258789, 449.1110015258789, 447.59261], + "rotation": 0, + "id": "36R" + } + } + }, + { + "id": "freetexts-editor-save", + "file": "pdfs/freetexts.pdf", + "md5": "da1310a25ab796c1201810070d5032a3", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "save": true, + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 3, + "color": [255, 0, 0], + "fontSize": 21, + "value": "The content must have been changed", + "pageIndex": 0, + "rect": [ 92.58600000000003, 415.2426115258789, 449.1110015258789, 447.59261], + "rotation": 0, + "id": "36R" + } + } + }, + { + "id": "freetexts-delete-editor-print", + "file": "pdfs/freetexts.pdf", + "md5": "da1310a25ab796c1201810070d5032a3", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "pageIndex": 0, + "deleted": true, + "id": "36R" + }, + "pdfjs_internal_editor_1": { + "pageIndex": 0, + "deleted": true, + "id": "53R" + } + } + }, + { + "id": "freetexts-delete-editor-save", + "file": "pdfs/freetexts.pdf", + "md5": "da1310a25ab796c1201810070d5032a3", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "save": true, + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "pageIndex": 0, + "deleted": true, + "id": "36R" + }, + "pdfjs_internal_editor_1": { + "pageIndex": 0, + "deleted": true, + "id": "53R" + } + } + } ]