Add tagged annotations in the structure tree (bug 1850797)

This commit is contained in:
Calixte Denizet 2023-08-30 20:00:05 +02:00
parent 92f7653cfb
commit d185db2b70
10 changed files with 152 additions and 47 deletions

View File

@ -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,7 +92,16 @@ class AnnotationFactory {
pdfManager.ensureCatalog("attachments"),
pdfManager.ensureDoc("xfaDatasets"),
collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1,
]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) =>
pageRef ? pdfManager.ensureCatalog("structTreeRoot") : null,
]).then(
([
acroForm,
baseUrl,
attachments,
xfaDatasets,
pageIndex,
structTreeRoot,
]) =>
pdfManager.ensure(this, "_create", [
xref,
ref,
@ -102,6 +112,8 @@ class AnnotationFactory {
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,

View File

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

View File

@ -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,21 +211,45 @@ 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();
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;
}
}
}
}
addNode(dict, map, level = 0) {
if (level > MAX_DEPTH) {
@ -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}`,
});
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -610,3 +610,4 @@
!annotation_hidden_noview.pdf
!widget_hidden_print.pdf
!empty_protected.pdf
!tagged_stamp.pdf

BIN
test/pdfs/tagged_stamp.pdf Executable file

Binary file not shown.

View File

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