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 {PDFManager} pdfManager
|
||||||
* @param {Object} idFactory
|
* @param {Object} idFactory
|
||||||
* @param {boolean} collectFields
|
* @param {boolean} collectFields
|
||||||
|
* @param {Object} [pageRef]
|
||||||
* @returns {Promise} A promise that is resolved with an {Annotation}
|
* @returns {Promise} A promise that is resolved with an {Annotation}
|
||||||
* instance.
|
* instance.
|
||||||
*/
|
*/
|
||||||
static create(xref, ref, pdfManager, idFactory, collectFields) {
|
static create(xref, ref, pdfManager, idFactory, collectFields, pageRef) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
pdfManager.ensureCatalog("acroForm"),
|
pdfManager.ensureCatalog("acroForm"),
|
||||||
// Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used
|
// Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used
|
||||||
@ -91,18 +92,29 @@ class AnnotationFactory {
|
|||||||
pdfManager.ensureCatalog("attachments"),
|
pdfManager.ensureCatalog("attachments"),
|
||||||
pdfManager.ensureDoc("xfaDatasets"),
|
pdfManager.ensureDoc("xfaDatasets"),
|
||||||
collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1,
|
collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1,
|
||||||
]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) =>
|
pageRef ? pdfManager.ensureCatalog("structTreeRoot") : null,
|
||||||
pdfManager.ensure(this, "_create", [
|
]).then(
|
||||||
xref,
|
([
|
||||||
ref,
|
|
||||||
pdfManager,
|
|
||||||
idFactory,
|
|
||||||
acroForm,
|
acroForm,
|
||||||
|
baseUrl,
|
||||||
attachments,
|
attachments,
|
||||||
xfaDatasets,
|
xfaDatasets,
|
||||||
collectFields,
|
|
||||||
pageIndex,
|
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,
|
attachments = null,
|
||||||
xfaDatasets,
|
xfaDatasets,
|
||||||
collectFields,
|
collectFields,
|
||||||
pageIndex = -1
|
pageIndex = -1,
|
||||||
|
structTreeRoot = null,
|
||||||
|
pageRef = null
|
||||||
) {
|
) {
|
||||||
const dict = xref.fetchIfRef(ref);
|
const dict = xref.fetchIfRef(ref);
|
||||||
if (!(dict instanceof Dict)) {
|
if (!(dict instanceof Dict)) {
|
||||||
@ -150,6 +164,8 @@ class AnnotationFactory {
|
|||||||
!collectFields && acroFormDict.get("NeedAppearances") === true,
|
!collectFields && acroFormDict.get("NeedAppearances") === true,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
evaluatorOptions: pdfManager.evaluatorOptions,
|
evaluatorOptions: pdfManager.evaluatorOptions,
|
||||||
|
structTreeRoot,
|
||||||
|
pageRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (subtype) {
|
switch (subtype) {
|
||||||
@ -594,6 +610,13 @@ class Annotation {
|
|||||||
const isLocked = !!(this.flags & AnnotationFlag.LOCKED);
|
const isLocked = !!(this.flags & AnnotationFlag.LOCKED);
|
||||||
const isContentLocked = !!(this.flags & AnnotationFlag.LOCKEDCONTENTS);
|
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.
|
// Expose public properties using a data object.
|
||||||
this.data = {
|
this.data = {
|
||||||
annotationFlags: this.flags,
|
annotationFlags: this.flags,
|
||||||
|
@ -651,6 +651,9 @@ class Page {
|
|||||||
if (!structTreeRoot) {
|
if (!structTreeRoot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// Ensure that the structTree will contain the page's annotations.
|
||||||
|
await this._parsedAnnotations;
|
||||||
|
|
||||||
const structTree = await this.pdfManager.ensure(this, "_parseStructTree", [
|
const structTree = await this.pdfManager.ensure(this, "_parseStructTree", [
|
||||||
structTreeRoot,
|
structTreeRoot,
|
||||||
]);
|
]);
|
||||||
@ -662,7 +665,7 @@ class Page {
|
|||||||
*/
|
*/
|
||||||
_parseStructTree(structTreeRoot) {
|
_parseStructTree(structTreeRoot) {
|
||||||
const tree = new StructTreePage(structTreeRoot, this.pageDict);
|
const tree = new StructTreePage(structTreeRoot, this.pageDict);
|
||||||
tree.parse();
|
tree.parse(this.ref);
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,7 +743,8 @@ class Page {
|
|||||||
annotationRef,
|
annotationRef,
|
||||||
this.pdfManager,
|
this.pdfManager,
|
||||||
this._localIdFactory,
|
this._localIdFactory,
|
||||||
/* collectFields */ false
|
/* collectFields */ false,
|
||||||
|
this.ref
|
||||||
).catch(function (reason) {
|
).catch(function (reason) {
|
||||||
warn(`_parsedAnnotations: "${reason}".`);
|
warn(`_parsedAnnotations: "${reason}".`);
|
||||||
return null;
|
return null;
|
||||||
@ -1719,7 +1723,8 @@ class PDFDocument {
|
|||||||
fieldRef,
|
fieldRef,
|
||||||
this.pdfManager,
|
this.pdfManager,
|
||||||
this._localIdFactory,
|
this._localIdFactory,
|
||||||
/* collectFields */ true
|
/* collectFields */ true,
|
||||||
|
/* pageRef */ null
|
||||||
)
|
)
|
||||||
.then(annotation => annotation?.getFieldObject())
|
.then(annotation => annotation?.getFieldObject())
|
||||||
.catch(function (reason) {
|
.catch(function (reason) {
|
||||||
|
@ -13,29 +13,48 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dict, isName, Name, Ref } from "./primitives.js";
|
import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js";
|
||||||
import { stringToPDFString, warn } from "../shared/util.js";
|
import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
|
||||||
import { NumberTree } from "./name_number_tree.js";
|
import { NumberTree } from "./name_number_tree.js";
|
||||||
|
|
||||||
const MAX_DEPTH = 40;
|
const MAX_DEPTH = 40;
|
||||||
|
|
||||||
const StructElementType = {
|
const StructElementType = {
|
||||||
PAGE_CONTENT: "PAGE_CONTENT",
|
PAGE_CONTENT: 1,
|
||||||
STREAM_CONTENT: "STREAM_CONTENT",
|
STREAM_CONTENT: 2,
|
||||||
OBJECT: "OBJECT",
|
OBJECT: 3,
|
||||||
ELEMENT: "ELEMENT",
|
ANNOTATION: 4,
|
||||||
|
ELEMENT: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
class StructTreeRoot {
|
class StructTreeRoot {
|
||||||
constructor(rootDict) {
|
constructor(rootDict) {
|
||||||
this.dict = rootDict;
|
this.dict = rootDict;
|
||||||
this.roleMap = new Map();
|
this.roleMap = new Map();
|
||||||
|
this.structParentIds = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.readRoleMap();
|
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() {
|
readRoleMap() {
|
||||||
const roleMapDict = this.dict.get("RoleMap");
|
const roleMapDict = this.dict.get("RoleMap");
|
||||||
if (!(roleMapDict instanceof Dict)) {
|
if (!(roleMapDict instanceof Dict)) {
|
||||||
@ -129,12 +148,10 @@ class StructElementNode {
|
|||||||
if (this.tree.pageDict.objId !== pageObjId) {
|
if (this.tree.pageDict.objId !== pageObjId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const kidRef = kidDict.getRaw("Stm");
|
||||||
return new StructElement({
|
return new StructElement({
|
||||||
type: StructElementType.STREAM_CONTENT,
|
type: StructElementType.STREAM_CONTENT,
|
||||||
refObjId:
|
refObjId: kidRef instanceof Ref ? kidRef.toString() : null,
|
||||||
kidDict.getRaw("Stm") instanceof Ref
|
|
||||||
? kidDict.getRaw("Stm").toString()
|
|
||||||
: null,
|
|
||||||
pageObjId,
|
pageObjId,
|
||||||
mcid: kidDict.get("MCID"),
|
mcid: kidDict.get("MCID"),
|
||||||
});
|
});
|
||||||
@ -144,12 +161,10 @@ class StructElementNode {
|
|||||||
if (this.tree.pageDict.objId !== pageObjId) {
|
if (this.tree.pageDict.objId !== pageObjId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const kidRef = kidDict.getRaw("Obj");
|
||||||
return new StructElement({
|
return new StructElement({
|
||||||
type: StructElementType.OBJECT,
|
type: StructElementType.OBJECT,
|
||||||
refObjId:
|
refObjId: kidRef instanceof Ref ? kidRef.toString() : null,
|
||||||
kidDict.getRaw("Obj") instanceof Ref
|
|
||||||
? kidDict.getRaw("Obj").toString()
|
|
||||||
: null,
|
|
||||||
pageObjId,
|
pageObjId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -186,7 +201,7 @@ class StructTreePage {
|
|||||||
this.nodes = [];
|
this.nodes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
parse() {
|
parse(pageRef) {
|
||||||
if (!this.root || !this.rootDict) {
|
if (!this.root || !this.rootDict) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -196,18 +211,42 @@ class StructTreePage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = this.pageDict.get("StructParents");
|
const id = this.pageDict.get("StructParents");
|
||||||
if (!Number.isInteger(id)) {
|
const ids =
|
||||||
return;
|
pageRef instanceof Ref && this.root.structParentIds?.get(pageRef);
|
||||||
}
|
if (!Number.isInteger(id) && !ids) {
|
||||||
const numberTree = new NumberTree(parentTree, this.rootDict.xref);
|
|
||||||
const parentArray = numberTree.get(id);
|
|
||||||
if (!Array.isArray(parentArray)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
for (const ref of parentArray) {
|
const numberTree = new NumberTree(parentTree, this.rootDict.xref);
|
||||||
if (ref instanceof Ref) {
|
|
||||||
this.addNode(this.rootDict.xref.fetch(ref), map);
|
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",
|
type: "object",
|
||||||
id: kid.refObjId,
|
id: kid.refObjId,
|
||||||
});
|
});
|
||||||
|
} else if (kid.type === StructElementType.ANNOTATION) {
|
||||||
|
obj.children.push({
|
||||||
|
type: "annotation",
|
||||||
|
id: `${AnnotationPrefix}${kid.refObjId}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
import {
|
import {
|
||||||
AnnotationBorderStyleType,
|
AnnotationBorderStyleType,
|
||||||
AnnotationEditorType,
|
AnnotationEditorType,
|
||||||
|
AnnotationPrefix,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
FeatureTest,
|
FeatureTest,
|
||||||
LINE_FACTOR,
|
LINE_FACTOR,
|
||||||
@ -30,7 +31,6 @@ import {
|
|||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import {
|
import {
|
||||||
AnnotationPrefix,
|
|
||||||
DOMSVGFactory,
|
DOMSVGFactory,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
PDFDateString,
|
PDFDateString,
|
||||||
|
@ -31,8 +31,6 @@ import {
|
|||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
const AnnotationPrefix = "pdfjs_internal_id_";
|
|
||||||
|
|
||||||
class PixelsPerInch {
|
class PixelsPerInch {
|
||||||
static CSS = 96.0;
|
static CSS = 96.0;
|
||||||
|
|
||||||
@ -1005,7 +1003,6 @@ function setLayerDimensions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AnnotationPrefix,
|
|
||||||
deprecated,
|
deprecated,
|
||||||
DOMCanvasFactory,
|
DOMCanvasFactory,
|
||||||
DOMCMapReaderFactory,
|
DOMCMapReaderFactory,
|
||||||
|
@ -1047,6 +1047,8 @@ function getUuid() {
|
|||||||
return bytesToString(buf);
|
return bytesToString(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AnnotationPrefix = "pdfjs_internal_id_";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AbortException,
|
AbortException,
|
||||||
AnnotationActionEventType,
|
AnnotationActionEventType,
|
||||||
@ -1057,6 +1059,7 @@ export {
|
|||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
|
AnnotationPrefix,
|
||||||
AnnotationReplyType,
|
AnnotationReplyType,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
assert,
|
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
|
!annotation_hidden_noview.pdf
|
||||||
!widget_hidden_print.pdf
|
!widget_hidden_print.pdf
|
||||||
!empty_protected.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) {
|
#setAttributes(structElement, htmlElement) {
|
||||||
if (structElement.alt !== undefined) {
|
const { alt, id, lang } = structElement;
|
||||||
htmlElement.setAttribute("aria-label", structElement.alt);
|
if (alt !== undefined) {
|
||||||
|
htmlElement.setAttribute("aria-label", alt);
|
||||||
}
|
}
|
||||||
if (structElement.id !== undefined) {
|
if (id !== undefined) {
|
||||||
htmlElement.setAttribute("aria-owns", structElement.id);
|
htmlElement.setAttribute("aria-owns", id);
|
||||||
}
|
}
|
||||||
if (structElement.lang !== undefined) {
|
if (lang !== undefined) {
|
||||||
htmlElement.setAttribute("lang", structElement.lang);
|
htmlElement.setAttribute("lang", lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user