diff --git a/src/core/annotation.js b/src/core/annotation.js index fc0bb208c..2e8d35807 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -77,10 +77,11 @@ class AnnotationFactory { * @param {PDFManager} pdfManager * @param {Object} idFactory * @param {boolean} collectFields + * @param {Object} [pageRef] * @returns {Promise} A promise that is resolved with an {Annotation} * instance. */ - static create(xref, ref, pdfManager, idFactory, collectFields) { + static create(xref, ref, pdfManager, idFactory, collectFields, pageRef) { return Promise.all([ pdfManager.ensureCatalog("acroForm"), // Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used @@ -91,18 +92,29 @@ class AnnotationFactory { pdfManager.ensureCatalog("attachments"), pdfManager.ensureDoc("xfaDatasets"), collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1, - ]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) => - pdfManager.ensure(this, "_create", [ - xref, - ref, - pdfManager, - idFactory, + pageRef ? pdfManager.ensureCatalog("structTreeRoot") : null, + ]).then( + ([ acroForm, + baseUrl, attachments, xfaDatasets, - collectFields, pageIndex, - ]) + structTreeRoot, + ]) => + pdfManager.ensure(this, "_create", [ + xref, + ref, + pdfManager, + idFactory, + acroForm, + attachments, + xfaDatasets, + collectFields, + pageIndex, + structTreeRoot, + pageRef, + ]) ); } @@ -118,7 +130,9 @@ class AnnotationFactory { attachments = null, xfaDatasets, collectFields, - pageIndex = -1 + pageIndex = -1, + structTreeRoot = null, + pageRef = null ) { const dict = xref.fetchIfRef(ref); if (!(dict instanceof Dict)) { @@ -150,6 +164,8 @@ class AnnotationFactory { !collectFields && acroFormDict.get("NeedAppearances") === true, pageIndex, evaluatorOptions: pdfManager.evaluatorOptions, + structTreeRoot, + pageRef, }; switch (subtype) { @@ -594,6 +610,13 @@ class Annotation { const isLocked = !!(this.flags & AnnotationFlag.LOCKED); const isContentLocked = !!(this.flags & AnnotationFlag.LOCKEDCONTENTS); + if (params.structTreeRoot) { + let structParent = dict.get("StructParent"); + structParent = + Number.isInteger(structParent) && structParent >= 0 ? structParent : -1; + params.structTreeRoot.addAnnotationIdToPage(params.pageRef, structParent); + } + // Expose public properties using a data object. this.data = { annotationFlags: this.flags, diff --git a/src/core/document.js b/src/core/document.js index 66197ea13..741494023 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -651,6 +651,9 @@ class Page { if (!structTreeRoot) { return null; } + // Ensure that the structTree will contain the page's annotations. + await this._parsedAnnotations; + const structTree = await this.pdfManager.ensure(this, "_parseStructTree", [ structTreeRoot, ]); @@ -662,7 +665,7 @@ class Page { */ _parseStructTree(structTreeRoot) { const tree = new StructTreePage(structTreeRoot, this.pageDict); - tree.parse(); + tree.parse(this.ref); return tree; } @@ -740,7 +743,8 @@ class Page { annotationRef, this.pdfManager, this._localIdFactory, - /* collectFields */ false + /* collectFields */ false, + this.ref ).catch(function (reason) { warn(`_parsedAnnotations: "${reason}".`); return null; @@ -1719,7 +1723,8 @@ class PDFDocument { fieldRef, this.pdfManager, this._localIdFactory, - /* collectFields */ true + /* collectFields */ true, + /* pageRef */ null ) .then(annotation => annotation?.getFieldObject()) .catch(function (reason) { diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index 0f576dbcf..eeebd1194 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -13,29 +13,48 @@ * limitations under the License. */ -import { Dict, isName, Name, Ref } from "./primitives.js"; -import { stringToPDFString, warn } from "../shared/util.js"; +import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js"; +import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js"; import { NumberTree } from "./name_number_tree.js"; const MAX_DEPTH = 40; const StructElementType = { - PAGE_CONTENT: "PAGE_CONTENT", - STREAM_CONTENT: "STREAM_CONTENT", - OBJECT: "OBJECT", - ELEMENT: "ELEMENT", + PAGE_CONTENT: 1, + STREAM_CONTENT: 2, + OBJECT: 3, + ANNOTATION: 4, + ELEMENT: 5, }; class StructTreeRoot { constructor(rootDict) { this.dict = rootDict; this.roleMap = new Map(); + this.structParentIds = null; } init() { this.readRoleMap(); } + #addIdToPage(pageRef, id, type) { + if (!(pageRef instanceof Ref) || id < 0) { + return; + } + this.structParentIds ||= new RefSetCache(); + let ids = this.structParentIds.get(pageRef); + if (!ids) { + ids = []; + this.structParentIds.put(pageRef, ids); + } + ids.push([id, type]); + } + + addAnnotationIdToPage(pageRef, id) { + this.#addIdToPage(pageRef, id, StructElementType.ANNOTATION); + } + readRoleMap() { const roleMapDict = this.dict.get("RoleMap"); if (!(roleMapDict instanceof Dict)) { @@ -129,12 +148,10 @@ class StructElementNode { if (this.tree.pageDict.objId !== pageObjId) { return null; } + const kidRef = kidDict.getRaw("Stm"); return new StructElement({ type: StructElementType.STREAM_CONTENT, - refObjId: - kidDict.getRaw("Stm") instanceof Ref - ? kidDict.getRaw("Stm").toString() - : null, + refObjId: kidRef instanceof Ref ? kidRef.toString() : null, pageObjId, mcid: kidDict.get("MCID"), }); @@ -144,12 +161,10 @@ class StructElementNode { if (this.tree.pageDict.objId !== pageObjId) { return null; } + const kidRef = kidDict.getRaw("Obj"); return new StructElement({ type: StructElementType.OBJECT, - refObjId: - kidDict.getRaw("Obj") instanceof Ref - ? kidDict.getRaw("Obj").toString() - : null, + refObjId: kidRef instanceof Ref ? kidRef.toString() : null, pageObjId, }); } @@ -186,7 +201,7 @@ class StructTreePage { this.nodes = []; } - parse() { + parse(pageRef) { if (!this.root || !this.rootDict) { return; } @@ -196,18 +211,42 @@ class StructTreePage { return; } const id = this.pageDict.get("StructParents"); - if (!Number.isInteger(id)) { - return; - } - const numberTree = new NumberTree(parentTree, this.rootDict.xref); - const parentArray = numberTree.get(id); - if (!Array.isArray(parentArray)) { + const ids = + pageRef instanceof Ref && this.root.structParentIds?.get(pageRef); + if (!Number.isInteger(id) && !ids) { return; } + const map = new Map(); - for (const ref of parentArray) { - if (ref instanceof Ref) { - this.addNode(this.rootDict.xref.fetch(ref), map); + const numberTree = new NumberTree(parentTree, this.rootDict.xref); + + if (Number.isInteger(id)) { + const parentArray = numberTree.get(id); + if (Array.isArray(parentArray)) { + for (const ref of parentArray) { + if (ref instanceof Ref) { + this.addNode(this.rootDict.xref.fetch(ref), map); + } + } + } + } + + if (!ids) { + return; + } + for (const [elemId, type] of ids) { + const obj = numberTree.get(elemId); + if (obj) { + const elem = this.addNode(this.rootDict.xref.fetchIfRef(obj), map); + if ( + elem?.kids?.length === 1 && + elem.kids[0].type === StructElementType.OBJECT + ) { + // The node in the struct tree is wrapping an object (annotation + // or xobject), so we need to update the type of the node to match + // the type of the object. + elem.kids[0].type = type; + } } } } @@ -322,6 +361,11 @@ class StructTreePage { type: "object", id: kid.refObjId, }); + } else if (kid.type === StructElementType.ANNOTATION) { + obj.children.push({ + type: "annotation", + id: `${AnnotationPrefix}${kid.refObjId}`, + }); } } } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index ba9cfb997..90cf2edf4 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -21,6 +21,7 @@ import { AnnotationBorderStyleType, AnnotationEditorType, + AnnotationPrefix, AnnotationType, FeatureTest, LINE_FACTOR, @@ -30,7 +31,6 @@ import { warn, } from "../shared/util.js"; import { - AnnotationPrefix, DOMSVGFactory, getFilenameFromUrl, PDFDateString, diff --git a/src/display/display_utils.js b/src/display/display_utils.js index bac8faf0a..e77f94a0c 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -31,8 +31,6 @@ import { const SVG_NS = "http://www.w3.org/2000/svg"; -const AnnotationPrefix = "pdfjs_internal_id_"; - class PixelsPerInch { static CSS = 96.0; @@ -1005,7 +1003,6 @@ function setLayerDimensions( } export { - AnnotationPrefix, deprecated, DOMCanvasFactory, DOMCMapReaderFactory, diff --git a/src/shared/util.js b/src/shared/util.js index adce62c1d..9bd726e9a 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1047,6 +1047,8 @@ function getUuid() { return bytesToString(buf); } +const AnnotationPrefix = "pdfjs_internal_id_"; + export { AbortException, AnnotationActionEventType, @@ -1057,6 +1059,7 @@ export { AnnotationFieldFlag, AnnotationFlag, AnnotationMode, + AnnotationPrefix, AnnotationReplyType, AnnotationType, assert, diff --git a/test/integration/accessibility_spec.js b/test/integration/accessibility_spec.js index 0d25586b1..c161e5dd8 100644 --- a/test/integration/accessibility_spec.js +++ b/test/integration/accessibility_spec.js @@ -139,4 +139,35 @@ describe("accessibility", () => { ); }); }); + + describe("Stamp annotation accessibility", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tagged_stamp.pdf", ".annotationLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the stamp annotation is linked to the struct tree", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForSelector(".structTree"); + + const isLinkedToStampAnnotation = await page.$eval( + ".structTree [role='figure']", + el => + document + .getElementById(el.getAttribute("aria-owns")) + .classList.contains("stampAnnotation") + ); + expect(isLinkedToStampAnnotation) + .withContext(`In ${browserName}`) + .toEqual(true); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 266b80b83..43c66b754 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -610,3 +610,4 @@ !annotation_hidden_noview.pdf !widget_hidden_print.pdf !empty_protected.pdf +!tagged_stamp.pdf diff --git a/test/pdfs/tagged_stamp.pdf b/test/pdfs/tagged_stamp.pdf new file mode 100755 index 000000000..918fd5597 Binary files /dev/null and b/test/pdfs/tagged_stamp.pdf differ diff --git a/web/struct_tree_layer_builder.js b/web/struct_tree_layer_builder.js index 5d54475a7..997fac339 100644 --- a/web/struct_tree_layer_builder.js +++ b/web/struct_tree_layer_builder.js @@ -100,14 +100,15 @@ class StructTreeLayerBuilder { } #setAttributes(structElement, htmlElement) { - if (structElement.alt !== undefined) { - htmlElement.setAttribute("aria-label", structElement.alt); + const { alt, id, lang } = structElement; + if (alt !== undefined) { + htmlElement.setAttribute("aria-label", alt); } - if (structElement.id !== undefined) { - htmlElement.setAttribute("aria-owns", structElement.id); + if (id !== undefined) { + htmlElement.setAttribute("aria-owns", id); } - if (structElement.lang !== undefined) { - htmlElement.setAttribute("lang", structElement.lang); + if (lang !== undefined) { + htmlElement.setAttribute("lang", lang); } }