Add tagged annotations in the structure tree (bug 1850797)
This commit is contained in:
parent
92f7653cfb
commit
d185db2b70
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -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
BIN
test/pdfs/tagged_stamp.pdf
Executable file
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user