[Editor] Add the ability to get all the editable annotations in a pdf document

We want to be able to show all the comments in a pdf even if the pages where they are
haven't been rendered.
And it'll help to fix the issue #18915.
This commit is contained in:
Calixte Denizet 2025-08-14 17:45:08 +02:00
parent dd560ee453
commit 9e5ee1e5a7
7 changed files with 183 additions and 4 deletions

View File

@ -122,6 +122,7 @@ class AnnotationFactory {
* @param {Object} idFactory
* @param {boolean} [collectFields]
* @param {Object} [orphanFields]
* @param {Array<string>} [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,

View File

@ -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())

View File

@ -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");
});

View File

@ -906,6 +906,16 @@ class PDFDocumentProxy {
return this._transport.getAttachments();
}
/**
* @param {Set<number>} types - The annotation types to retrieve.
* @param {Set<number>} pageIndexesToSkip
* @returns {Promise<Array<Object>>} A promise that is resolved with a list of
* annotations data.
*/
getAnnotationsByType(types, pageIndexesToSkip) {
return this._transport.getAnnotationsByType(types, pageIndexesToSkip);
}
/**
* @returns {Promise<Object | null>} 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");
}

View File

@ -740,3 +740,4 @@
!dates_save.pdf
!print_protection.pdf
!tracemonkey_with_annotations.pdf
!tracemonkey_with_editable_annotations.pdf

Binary file not shown.

View File

@ -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 () {