diff --git a/src/core/annotation.js b/src/core/annotation.js index 676918a40..4657daedf 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -122,6 +122,7 @@ class AnnotationFactory { * @param {Object} idFactory * @param {boolean} [collectFields] * @param {Object} [orphanFields] + * @param {Array} [collectByType] * @param {Object} [pageRef] * @returns {Promise} A promise that is resolved with an {Annotation} * instance. @@ -133,6 +134,7 @@ class AnnotationFactory { idFactory, collectFields, orphanFields, + collectByType, pageRef ) { const pageIndex = collectFields @@ -146,6 +148,7 @@ class AnnotationFactory { idFactory, collectFields, orphanFields, + collectByType, pageIndex, pageRef, ]); @@ -161,6 +164,7 @@ class AnnotationFactory { idFactory, collectFields = false, orphanFields = null, + collectByType = null, pageIndex = null, pageRef = null ) { @@ -169,14 +173,21 @@ class AnnotationFactory { return undefined; } - const { acroForm, pdfManager } = annotationGlobals; - const id = - ref instanceof Ref ? ref.toString() : `annot_${idFactory.createObjId()}`; - // Determine the annotation's subtype. let subtype = dict.get("Subtype"); subtype = subtype instanceof Name ? subtype.name : null; + if ( + collectByType && + !collectByType.has(AnnotationType[subtype.toUpperCase()]) + ) { + return null; + } + + const { acroForm, pdfManager } = annotationGlobals; + const id = + ref instanceof Ref ? ref.toString() : `annot_${idFactory.createObjId()}`; + // Return the right annotation object based on the subtype and field type. const parameters = { xref, diff --git a/src/core/document.js b/src/core/document.js index 7138cbbf8..0faa68c50 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -802,6 +802,7 @@ class Page { this._localIdFactory, /* collectFields */ false, orphanFields, + /* collectByType */ null, this.ref ).catch(function (reason) { warn(`_parsedAnnotations: "${reason}".`); @@ -849,6 +850,51 @@ class Page { ); return shadow(this, "jsActions", actions); } + + async collectAnnotationsByType( + handler, + task, + types, + promises, + annotationGlobals + ) { + const annots = await this.pdfManager.ensure(this, "annotations"); + const { pageIndex } = this; + for (const annotationRef of annots) { + promises.push( + AnnotationFactory.create( + this.xref, + annotationRef, + annotationGlobals, + this._localIdFactory, + /* collectFields */ false, + /* orphanFields */ null, + /* collectByType */ types, + this.ref + ) + .then(async annotation => { + if (!annotation) { + return null; + } + annotation.data.pageIndex = pageIndex; + if (annotation.hasTextContent && annotation.viewable) { + const partialEvaluator = this.#createPartialEvaluator(handler); + await annotation.extractTextContent(partialEvaluator, task, [ + -Infinity, + -Infinity, + Infinity, + Infinity, + ]); + } + return annotation.data; + }) + .catch(function (reason) { + warn(`collectAnnotationsByType: "${reason}".`); + return null; + }) + ); + } + } } const PDF_HEADER_SIGNATURE = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]); @@ -1881,6 +1927,7 @@ class PDFDocument { /* idFactory = */ null, /* collectFields */ true, orphanFields, + /* collectByType */ null, /* pageRef */ null ) .then(annotation => annotation?.getFieldObject()) diff --git a/src/core/worker.js b/src/core/worker.js index 3d1abbaf1..b94fb7068 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -447,6 +447,57 @@ class WorkerMessageHandler { .then(page => pdfManager.ensure(page, "jsActions")); }); + handler.on( + "GetAnnotationsByType", + async function ({ types, pageIndexesToSkip }) { + const [numPages, annotationGlobals] = await Promise.all([ + pdfManager.ensureDoc("numPages"), + pdfManager.ensureDoc("annotationGlobals"), + ]); + + if (!annotationGlobals) { + return null; + } + const pagePromises = []; + const annotationPromises = []; + let task = null; + try { + for (let i = 0, ii = numPages; i < ii; i++) { + if (pageIndexesToSkip?.has(i)) { + continue; + } + if (!task) { + task = new WorkerTask("GetAnnotationsByType"); + startWorkerTask(task); + } + pagePromises.push( + pdfManager.getPage(i).then(async page => { + if (!page) { + return []; + } + return ( + page.collectAnnotationsByType( + handler, + task, + types, + annotationPromises, + annotationGlobals + ) || [] + ); + }) + ); + } + await Promise.all(pagePromises); + const annotations = await Promise.all(annotationPromises); + return annotations.filter(a => !!a); + } finally { + if (task) { + finishWorkerTask(task); + } + } + } + ); + handler.on("GetOutline", function (data) { return pdfManager.ensureCatalog("documentOutline"); }); diff --git a/src/display/api.js b/src/display/api.js index 1db1dbcce..a384a819e 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -906,6 +906,16 @@ class PDFDocumentProxy { return this._transport.getAttachments(); } + /** + * @param {Set} types - The annotation types to retrieve. + * @param {Set} pageIndexesToSkip + * @returns {Promise>} A promise that is resolved with a list of + * annotations data. + */ + getAnnotationsByType(types, pageIndexesToSkip) { + return this._transport.getAnnotationsByType(types, pageIndexesToSkip); + } + /** * @returns {Promise} A promise that is resolved with * an {Object} with the JavaScript actions: @@ -2944,6 +2954,13 @@ class WorkerTransport { return this.messageHandler.sendWithPromise("GetAttachments", null); } + getAnnotationsByType(types, pageIndexesToSkip) { + return this.messageHandler.sendWithPromise("GetAnnotationsByType", { + types, + pageIndexesToSkip, + }); + } + getDocJSActions() { return this.#cacheSimpleMethod("GetDocJSActions"); } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 58549f5c8..f896c4299 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -740,3 +740,4 @@ !dates_save.pdf !print_protection.pdf !tracemonkey_with_annotations.pdf +!tracemonkey_with_editable_annotations.pdf diff --git a/test/pdfs/tracemonkey_with_editable_annotations.pdf b/test/pdfs/tracemonkey_with_editable_annotations.pdf new file mode 100755 index 000000000..037d9482c Binary files /dev/null and b/test/pdfs/tracemonkey_with_editable_annotations.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index e5fdf5d30..29d0ec910 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -3205,6 +3205,58 @@ describe("api", function () { }); }); }); + + describe("Get annotations by their types in the document", function () { + it("gets editable annotations", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("tracemonkey_with_editable_annotations.pdf") + ); + const pdfDoc = await loadingTask.promise; + + // Get all the editable annotations in the document. + let editableAnnotations = ( + await pdfDoc.getAnnotationsByType( + new Set([ + AnnotationType.FREETEXT, + AnnotationType.STAMP, + AnnotationType.INK, + AnnotationType.HIGHLIGHT, + ]), + null + ) + ).map(annotation => ({ + id: annotation.id, + subtype: annotation.subtype, + pageIndex: annotation.pageIndex, + })); + editableAnnotations.sort((a, b) => a.id.localeCompare(b.id)); + expect(editableAnnotations).toEqual([ + { id: "1000R", subtype: "FreeText", pageIndex: 12 }, + { id: "1001R", subtype: "Stamp", pageIndex: 12 }, + { id: "1011R", subtype: "Stamp", pageIndex: 13 }, + { id: "997R", subtype: "Ink", pageIndex: 13 }, + { id: "998R", subtype: "Highlight", pageIndex: 13 }, + ]); + + // Get all the editable annotations but the ones on page 12. + editableAnnotations = ( + await pdfDoc.getAnnotationsByType( + new Set([AnnotationType.STAMP, AnnotationType.HIGHLIGHT]), + new Set([12]) + ) + ).map(annotation => ({ + id: annotation.id, + subtype: annotation.subtype, + pageIndex: annotation.pageIndex, + })); + editableAnnotations.sort((a, b) => a.id.localeCompare(b.id)); + expect(editableAnnotations).toEqual([ + { id: "1011R", subtype: "Stamp", pageIndex: 13 }, + { id: "998R", subtype: "Highlight", pageIndex: 13 }, + ]); + await loadingTask.destroy(); + }); + }); }); describe("Page", function () {