Merge pull request #16886 from calixteman/struct_tree_annotation
Add tagged annotations in the structure tree (bug 1850797)
This commit is contained in:
		
						commit
						20b0be973c
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user