Skip to content

Commit d185db2

Browse files
committed
Add tagged annotations in the structure tree (bug 1850797)
1 parent 92f7653 commit d185db2

10 files changed

+150
-45
lines changed

src/core/annotation.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ class AnnotationFactory {
7777
* @param {PDFManager} pdfManager
7878
* @param {Object} idFactory
7979
* @param {boolean} collectFields
80+
* @param {Object} [pageRef]
8081
* @returns {Promise} A promise that is resolved with an {Annotation}
8182
* instance.
8283
*/
83-
static create(xref, ref, pdfManager, idFactory, collectFields) {
84+
static create(xref, ref, pdfManager, idFactory, collectFields, pageRef) {
8485
return Promise.all([
8586
pdfManager.ensureCatalog("acroForm"),
8687
// Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used
@@ -91,18 +92,29 @@ class AnnotationFactory {
9192
pdfManager.ensureCatalog("attachments"),
9293
pdfManager.ensureDoc("xfaDatasets"),
9394
collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1,
94-
]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) =>
95-
pdfManager.ensure(this, "_create", [
96-
xref,
97-
ref,
98-
pdfManager,
99-
idFactory,
95+
pageRef ? pdfManager.ensureCatalog("structTreeRoot") : null,
96+
]).then(
97+
([
10098
acroForm,
99+
baseUrl,
101100
attachments,
102101
xfaDatasets,
103-
collectFields,
104102
pageIndex,
105-
])
103+
structTreeRoot,
104+
]) =>
105+
pdfManager.ensure(this, "_create", [
106+
xref,
107+
ref,
108+
pdfManager,
109+
idFactory,
110+
acroForm,
111+
attachments,
112+
xfaDatasets,
113+
collectFields,
114+
pageIndex,
115+
structTreeRoot,
116+
pageRef,
117+
])
106118
);
107119
}
108120

@@ -118,7 +130,9 @@ class AnnotationFactory {
118130
attachments = null,
119131
xfaDatasets,
120132
collectFields,
121-
pageIndex = -1
133+
pageIndex = -1,
134+
structTreeRoot = null,
135+
pageRef = null
122136
) {
123137
const dict = xref.fetchIfRef(ref);
124138
if (!(dict instanceof Dict)) {
@@ -150,6 +164,8 @@ class AnnotationFactory {
150164
!collectFields && acroFormDict.get("NeedAppearances") === true,
151165
pageIndex,
152166
evaluatorOptions: pdfManager.evaluatorOptions,
167+
structTreeRoot,
168+
pageRef,
153169
};
154170

155171
switch (subtype) {
@@ -594,6 +610,13 @@ class Annotation {
594610
const isLocked = !!(this.flags & AnnotationFlag.LOCKED);
595611
const isContentLocked = !!(this.flags & AnnotationFlag.LOCKEDCONTENTS);
596612

613+
if (params.structTreeRoot) {
614+
let structParent = dict.get("StructParent");
615+
structParent =
616+
Number.isInteger(structParent) && structParent >= 0 ? structParent : -1;
617+
params.structTreeRoot.addAnnotationIdToPage(params.pageRef, structParent);
618+
}
619+
597620
// Expose public properties using a data object.
598621
this.data = {
599622
annotationFlags: this.flags,

src/core/document.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,9 @@ class Page {
651651
if (!structTreeRoot) {
652652
return null;
653653
}
654+
// Ensure that the structTree will contain the page's annotations.
655+
await this._parsedAnnotations;
656+
654657
const structTree = await this.pdfManager.ensure(this, "_parseStructTree", [
655658
structTreeRoot,
656659
]);
@@ -662,7 +665,7 @@ class Page {
662665
*/
663666
_parseStructTree(structTreeRoot) {
664667
const tree = new StructTreePage(structTreeRoot, this.pageDict);
665-
tree.parse();
668+
tree.parse(this.ref);
666669
return tree;
667670
}
668671

@@ -740,7 +743,8 @@ class Page {
740743
annotationRef,
741744
this.pdfManager,
742745
this._localIdFactory,
743-
/* collectFields */ false
746+
/* collectFields */ false,
747+
this.ref
744748
).catch(function (reason) {
745749
warn(`_parsedAnnotations: "${reason}".`);
746750
return null;
@@ -1719,7 +1723,8 @@ class PDFDocument {
17191723
fieldRef,
17201724
this.pdfManager,
17211725
this._localIdFactory,
1722-
/* collectFields */ true
1726+
/* collectFields */ true,
1727+
/* pageRef */ null
17231728
)
17241729
.then(annotation => annotation?.getFieldObject())
17251730
.catch(function (reason) {

src/core/struct_tree.js

+66-22
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,48 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { Dict, isName, Name, Ref } from "./primitives.js";
17-
import { stringToPDFString, warn } from "../shared/util.js";
16+
import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js";
17+
import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
1818
import { NumberTree } from "./name_number_tree.js";
1919

2020
const MAX_DEPTH = 40;
2121

2222
const StructElementType = {
23-
PAGE_CONTENT: "PAGE_CONTENT",
24-
STREAM_CONTENT: "STREAM_CONTENT",
25-
OBJECT: "OBJECT",
26-
ELEMENT: "ELEMENT",
23+
PAGE_CONTENT: 1,
24+
STREAM_CONTENT: 2,
25+
OBJECT: 3,
26+
ANNOTATION: 4,
27+
ELEMENT: 5,
2728
};
2829

2930
class StructTreeRoot {
3031
constructor(rootDict) {
3132
this.dict = rootDict;
3233
this.roleMap = new Map();
34+
this.structParentIds = null;
3335
}
3436

3537
init() {
3638
this.readRoleMap();
3739
}
3840

41+
#addIdToPage(pageRef, id, type) {
42+
if (!(pageRef instanceof Ref) || id < 0) {
43+
return;
44+
}
45+
this.structParentIds ||= new RefSetCache();
46+
let ids = this.structParentIds.get(pageRef);
47+
if (!ids) {
48+
ids = [];
49+
this.structParentIds.put(pageRef, ids);
50+
}
51+
ids.push([id, type]);
52+
}
53+
54+
addAnnotationIdToPage(pageRef, id) {
55+
this.#addIdToPage(pageRef, id, StructElementType.ANNOTATION);
56+
}
57+
3958
readRoleMap() {
4059
const roleMapDict = this.dict.get("RoleMap");
4160
if (!(roleMapDict instanceof Dict)) {
@@ -129,12 +148,10 @@ class StructElementNode {
129148
if (this.tree.pageDict.objId !== pageObjId) {
130149
return null;
131150
}
151+
const kidRef = kidDict.getRaw("Stm");
132152
return new StructElement({
133153
type: StructElementType.STREAM_CONTENT,
134-
refObjId:
135-
kidDict.getRaw("Stm") instanceof Ref
136-
? kidDict.getRaw("Stm").toString()
137-
: null,
154+
refObjId: kidRef instanceof Ref ? kidRef.toString() : null,
138155
pageObjId,
139156
mcid: kidDict.get("MCID"),
140157
});
@@ -144,12 +161,10 @@ class StructElementNode {
144161
if (this.tree.pageDict.objId !== pageObjId) {
145162
return null;
146163
}
164+
const kidRef = kidDict.getRaw("Obj");
147165
return new StructElement({
148166
type: StructElementType.OBJECT,
149-
refObjId:
150-
kidDict.getRaw("Obj") instanceof Ref
151-
? kidDict.getRaw("Obj").toString()
152-
: null,
167+
refObjId: kidRef instanceof Ref ? kidRef.toString() : null,
153168
pageObjId,
154169
});
155170
}
@@ -186,7 +201,7 @@ class StructTreePage {
186201
this.nodes = [];
187202
}
188203

189-
parse() {
204+
parse(pageRef) {
190205
if (!this.root || !this.rootDict) {
191206
return;
192207
}
@@ -196,18 +211,42 @@ class StructTreePage {
196211
return;
197212
}
198213
const id = this.pageDict.get("StructParents");
199-
if (!Number.isInteger(id)) {
214+
const ids =
215+
pageRef instanceof Ref && this.root.structParentIds?.get(pageRef);
216+
if (!Number.isInteger(id) && !ids) {
200217
return;
201218
}
219+
220+
const map = new Map();
202221
const numberTree = new NumberTree(parentTree, this.rootDict.xref);
203-
const parentArray = numberTree.get(id);
204-
if (!Array.isArray(parentArray)) {
222+
223+
if (Number.isInteger(id)) {
224+
const parentArray = numberTree.get(id);
225+
if (Array.isArray(parentArray)) {
226+
for (const ref of parentArray) {
227+
if (ref instanceof Ref) {
228+
this.addNode(this.rootDict.xref.fetch(ref), map);
229+
}
230+
}
231+
}
232+
}
233+
234+
if (!ids) {
205235
return;
206236
}
207-
const map = new Map();
208-
for (const ref of parentArray) {
209-
if (ref instanceof Ref) {
210-
this.addNode(this.rootDict.xref.fetch(ref), map);
237+
for (const [elemId, type] of ids) {
238+
const obj = numberTree.get(elemId);
239+
if (obj) {
240+
const elem = this.addNode(this.rootDict.xref.fetchIfRef(obj), map);
241+
if (
242+
elem?.kids?.length === 1 &&
243+
elem.kids[0].type === StructElementType.OBJECT
244+
) {
245+
// The node in the struct tree is wrapping an object (annotation
246+
// or xobject), so we need to update the type of the node to match
247+
// the type of the object.
248+
elem.kids[0].type = type;
249+
}
211250
}
212251
}
213252
}
@@ -322,6 +361,11 @@ class StructTreePage {
322361
type: "object",
323362
id: kid.refObjId,
324363
});
364+
} else if (kid.type === StructElementType.ANNOTATION) {
365+
obj.children.push({
366+
type: "annotation",
367+
id: `${AnnotationPrefix}${kid.refObjId}`,
368+
});
325369
}
326370
}
327371
}

src/display/annotation_layer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import {
2222
AnnotationBorderStyleType,
2323
AnnotationEditorType,
24+
AnnotationPrefix,
2425
AnnotationType,
2526
FeatureTest,
2627
LINE_FACTOR,
@@ -30,7 +31,6 @@ import {
3031
warn,
3132
} from "../shared/util.js";
3233
import {
33-
AnnotationPrefix,
3434
DOMSVGFactory,
3535
getFilenameFromUrl,
3636
PDFDateString,

src/display/display_utils.js

-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import {
3131

3232
const SVG_NS = "http://www.w3.org/2000/svg";
3333

34-
const AnnotationPrefix = "pdfjs_internal_id_";
35-
3634
class PixelsPerInch {
3735
static CSS = 96.0;
3836

@@ -1005,7 +1003,6 @@ function setLayerDimensions(
10051003
}
10061004

10071005
export {
1008-
AnnotationPrefix,
10091006
deprecated,
10101007
DOMCanvasFactory,
10111008
DOMCMapReaderFactory,

src/shared/util.js

+3
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,8 @@ function getUuid() {
10471047
return bytesToString(buf);
10481048
}
10491049

1050+
const AnnotationPrefix = "pdfjs_internal_id_";
1051+
10501052
export {
10511053
AbortException,
10521054
AnnotationActionEventType,
@@ -1057,6 +1059,7 @@ export {
10571059
AnnotationFieldFlag,
10581060
AnnotationFlag,
10591061
AnnotationMode,
1062+
AnnotationPrefix,
10601063
AnnotationReplyType,
10611064
AnnotationType,
10621065
assert,

test/integration/accessibility_spec.js

+31
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,35 @@ describe("accessibility", () => {
139139
);
140140
});
141141
});
142+
143+
describe("Stamp annotation accessibility", () => {
144+
let pages;
145+
146+
beforeAll(async () => {
147+
pages = await loadAndWait("tagged_stamp.pdf", ".annotationLayer");
148+
});
149+
150+
afterAll(async () => {
151+
await closePages(pages);
152+
});
153+
154+
it("must check that the stamp annotation is linked to the struct tree", async () => {
155+
await Promise.all(
156+
pages.map(async ([browserName, page]) => {
157+
await page.waitForSelector(".structTree");
158+
159+
const isLinkedToStampAnnotation = await page.$eval(
160+
".structTree [role='figure']",
161+
el =>
162+
document
163+
.getElementById(el.getAttribute("aria-owns"))
164+
.classList.contains("stampAnnotation")
165+
);
166+
expect(isLinkedToStampAnnotation)
167+
.withContext(`In ${browserName}`)
168+
.toEqual(true);
169+
})
170+
);
171+
});
172+
});
142173
});

test/pdfs/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,4 @@
610610
!annotation_hidden_noview.pdf
611611
!widget_hidden_print.pdf
612612
!empty_protected.pdf
613+
!tagged_stamp.pdf

test/pdfs/tagged_stamp.pdf

21.1 KB
Binary file not shown.

web/struct_tree_layer_builder.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,15 @@ class StructTreeLayerBuilder {
100100
}
101101

102102
#setAttributes(structElement, htmlElement) {
103-
if (structElement.alt !== undefined) {
104-
htmlElement.setAttribute("aria-label", structElement.alt);
103+
const { alt, id, lang } = structElement;
104+
if (alt !== undefined) {
105+
htmlElement.setAttribute("aria-label", alt);
105106
}
106-
if (structElement.id !== undefined) {
107-
htmlElement.setAttribute("aria-owns", structElement.id);
107+
if (id !== undefined) {
108+
htmlElement.setAttribute("aria-owns", id);
108109
}
109-
if (structElement.lang !== undefined) {
110-
htmlElement.setAttribute("lang", structElement.lang);
110+
if (lang !== undefined) {
111+
htmlElement.setAttribute("lang", lang);
111112
}
112113
}
113114

0 commit comments

Comments
 (0)