mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-20 15:18:08 +02:00
Merge pull request #13171 from brendandahl/struct-tree
[api-minor] Add support for basic structure tree for accessibility.
This commit is contained in:
commit
03c8c89002
22 changed files with 911 additions and 14 deletions
|
@ -41,6 +41,7 @@ import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
|||
import { NullL10n } from "./l10n_utils.js";
|
||||
import { PDFPageView } from "./pdf_page_view.js";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
||||
import { TextLayerBuilder } from "./text_layer_builder.js";
|
||||
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||
|
||||
|
@ -545,6 +546,7 @@ class BaseViewer {
|
|||
textLayerMode: this.textLayerMode,
|
||||
annotationLayerFactory: this,
|
||||
xfaLayerFactory,
|
||||
structTreeLayerFactory: this,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
renderInteractiveForms: this.renderInteractiveForms,
|
||||
renderer: this.renderer,
|
||||
|
@ -1328,6 +1330,16 @@ class BaseViewer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PDFPage} pdfPage
|
||||
* @returns {StructTreeLayerBuilder}
|
||||
*/
|
||||
createStructTreeLayerBuilder(pdfPage) {
|
||||
return new StructTreeLayerBuilder({
|
||||
pdfPage,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean} Whether all pages of the PDF document have identical
|
||||
* widths and heights.
|
||||
|
|
|
@ -216,6 +216,17 @@ class IPDFXfaLayerFactory {
|
|||
createXfaLayerBuilder(pageDiv, pdfPage) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
class IPDFStructTreeLayerFactory {
|
||||
/**
|
||||
* @param {PDFPage} pdfPage
|
||||
* @returns {StructTreeLayerBuilder}
|
||||
*/
|
||||
createStructTreeLayerBuilder(pdfPage) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
|
@ -254,6 +265,7 @@ export {
|
|||
IPDFAnnotationLayerFactory,
|
||||
IPDFHistory,
|
||||
IPDFLinkService,
|
||||
IPDFStructTreeLayerFactory,
|
||||
IPDFTextLayerFactory,
|
||||
IPDFXfaLayerFactory,
|
||||
IRenderableView,
|
||||
|
|
|
@ -49,6 +49,7 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js";
|
|||
* The default value is `TextLayerMode.ENABLE`.
|
||||
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
|
||||
* @property {IPDFXfaLayerFactory} xfaLayerFactory
|
||||
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
|
||||
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||
* for annotation icons. Include trailing slash.
|
||||
* @property {boolean} renderInteractiveForms - Turns on rendering of
|
||||
|
@ -102,6 +103,7 @@ class PDFPageView {
|
|||
this.textLayerFactory = options.textLayerFactory;
|
||||
this.annotationLayerFactory = options.annotationLayerFactory;
|
||||
this.xfaLayerFactory = options.xfaLayerFactory;
|
||||
this.structTreeLayerFactory = options.structTreeLayerFactory;
|
||||
this.renderer = options.renderer || RendererType.CANVAS;
|
||||
this.enableWebGL = options.enableWebGL || false;
|
||||
this.l10n = options.l10n || NullL10n;
|
||||
|
@ -116,6 +118,7 @@ class PDFPageView {
|
|||
this.textLayer = null;
|
||||
this.zoomLayer = null;
|
||||
this.xfaLayer = null;
|
||||
this.structTreeLayer = null;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "page";
|
||||
|
@ -354,6 +357,10 @@ class PDFPageView {
|
|||
this.annotationLayer.cancel();
|
||||
this.annotationLayer = null;
|
||||
}
|
||||
if (this._onTextLayerRendered) {
|
||||
this.eventBus._off("textlayerrendered", this._onTextLayerRendered);
|
||||
this._onTextLayerRendered = null;
|
||||
}
|
||||
}
|
||||
|
||||
cssTransform(target, redrawAnnotations = false) {
|
||||
|
@ -556,11 +563,12 @@ class PDFPageView {
|
|||
this.paintTask = paintTask;
|
||||
|
||||
const resultPromise = paintTask.promise.then(
|
||||
function () {
|
||||
return finishPaintTask(null).then(function () {
|
||||
() => {
|
||||
return finishPaintTask(null).then(() => {
|
||||
if (textLayer) {
|
||||
const readableStream = pdfPage.streamTextContent({
|
||||
normalizeWhitespace: true,
|
||||
includeMarkedContent: true,
|
||||
});
|
||||
textLayer.setTextContentStream(readableStream);
|
||||
textLayer.render();
|
||||
|
@ -599,6 +607,29 @@ class PDFPageView {
|
|||
this._renderXfaLayer();
|
||||
}
|
||||
|
||||
// The structure tree is currently only supported when the text layer is
|
||||
// enabled and a canvas is used for rendering.
|
||||
if (this.structTreeLayerFactory && this.textLayer && this.canvas) {
|
||||
// The structure tree must be generated after the text layer for the
|
||||
// aria-owns to work.
|
||||
this._onTextLayerRendered = event => {
|
||||
if (event.pageNumber !== this.id) {
|
||||
return;
|
||||
}
|
||||
this.eventBus._off("textlayerrendered", this._onTextLayerRendered);
|
||||
this._onTextLayerRendered = null;
|
||||
this.pdfPage.getStructTree().then(tree => {
|
||||
const treeDom = this.structTreeLayer.render(tree);
|
||||
treeDom.classList.add("structTree");
|
||||
this.canvas.appendChild(treeDom);
|
||||
});
|
||||
};
|
||||
this.eventBus._on("textlayerrendered", this._onTextLayerRendered);
|
||||
this.structTreeLayer = this.structTreeLayerFactory.createStructTreeLayerBuilder(
|
||||
pdfPage
|
||||
);
|
||||
}
|
||||
|
||||
div.setAttribute("data-loaded", true);
|
||||
|
||||
this.eventBus.dispatch("pagerender", {
|
||||
|
|
149
web/struct_tree_layer_builder.js
Normal file
149
web/struct_tree_layer_builder.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/* Copyright 2021 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const PDF_ROLE_TO_HTML_ROLE = {
|
||||
// Document level structure types
|
||||
Document: null, // There's a "document" role, but it doesn't make sense here.
|
||||
DocumentFragment: null,
|
||||
// Grouping level structure types
|
||||
Part: "group",
|
||||
Sect: "group", // XXX: There's a "section" role, but it's abstract.
|
||||
Div: "group",
|
||||
Aside: "note",
|
||||
NonStruct: "none",
|
||||
// Block level structure types
|
||||
P: null,
|
||||
// H<n>,
|
||||
H: "heading",
|
||||
Title: null,
|
||||
FENote: "note",
|
||||
// Sub-block level structure type
|
||||
Sub: "group",
|
||||
// General inline level structure types
|
||||
Lbl: null,
|
||||
Span: null,
|
||||
Em: null,
|
||||
Strong: null,
|
||||
Link: "link",
|
||||
Annot: "note",
|
||||
Form: "form",
|
||||
// Ruby and Warichu structure types
|
||||
Ruby: null,
|
||||
RB: null,
|
||||
RT: null,
|
||||
RP: null,
|
||||
Warichu: null,
|
||||
WT: null,
|
||||
WP: null,
|
||||
// List standard structure types
|
||||
L: "list",
|
||||
LI: "listitem",
|
||||
LBody: null,
|
||||
// Table standard structure types
|
||||
Table: "table",
|
||||
TR: "row",
|
||||
TH: "columnheader",
|
||||
TD: "cell",
|
||||
THead: "columnheader",
|
||||
TBody: null,
|
||||
TFoot: null,
|
||||
// Standard structure type Caption
|
||||
Caption: null,
|
||||
// Standard structure type Figure
|
||||
Figure: "figure",
|
||||
// Standard structure type Formula
|
||||
Formula: null,
|
||||
// standard structure type Artifact
|
||||
Artifact: null,
|
||||
};
|
||||
|
||||
const HEADING_PATTERN = /^H(\d+)$/;
|
||||
|
||||
/**
|
||||
* @typedef {Object} StructTreeLayerBuilderOptions
|
||||
* @property {PDFPage} pdfPage
|
||||
*/
|
||||
|
||||
class StructTreeLayerBuilder {
|
||||
/**
|
||||
* @param {StructTreeLayerBuilderOptions} options
|
||||
*/
|
||||
constructor({ pdfPage }) {
|
||||
this.pdfPage = pdfPage;
|
||||
}
|
||||
|
||||
render(structTree) {
|
||||
return this._walk(structTree);
|
||||
}
|
||||
|
||||
_setAttributes(structElement, htmlElement) {
|
||||
if (structElement.alt !== undefined) {
|
||||
htmlElement.setAttribute("aria-label", structElement.alt);
|
||||
}
|
||||
if (structElement.id !== undefined) {
|
||||
htmlElement.setAttribute("aria-owns", structElement.id);
|
||||
}
|
||||
}
|
||||
|
||||
_walk(node) {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const element = document.createElement("span");
|
||||
if ("role" in node) {
|
||||
const { role } = node;
|
||||
const match = role.match(HEADING_PATTERN);
|
||||
if (match) {
|
||||
element.setAttribute("role", "heading");
|
||||
element.setAttribute("aria-level", match[1]);
|
||||
} else if (PDF_ROLE_TO_HTML_ROLE[role]) {
|
||||
element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]);
|
||||
}
|
||||
}
|
||||
|
||||
this._setAttributes(node, element);
|
||||
|
||||
if (node.children) {
|
||||
if (node.children.length === 1 && "id" in node.children[0]) {
|
||||
// Often there is only one content node so just set the values on the
|
||||
// parent node to avoid creating an extra span.
|
||||
this._setAttributes(node.children[0], element);
|
||||
} else {
|
||||
for (const kid of node.children) {
|
||||
element.appendChild(this._walk(kid));
|
||||
}
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements IPDFStructTreeLayerFactory
|
||||
*/
|
||||
class DefaultStructTreeLayerFactory {
|
||||
/**
|
||||
* @param {PDFPage} pdfPage
|
||||
* @returns {StructTreeLayerBuilder}
|
||||
*/
|
||||
createStructTreeLayerBuilder(pdfPage) {
|
||||
return new StructTreeLayerBuilder({
|
||||
pdfPage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { DefaultStructTreeLayerFactory, StructTreeLayerBuilder };
|
|
@ -24,7 +24,7 @@
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
.textLayer > span {
|
||||
.textLayer span {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
|
|
|
@ -175,7 +175,7 @@ select {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
.pdfViewer.enablePermissions .textLayer > span {
|
||||
.pdfViewer.enablePermissions .textLayer span {
|
||||
user-select: none !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
@ -195,12 +195,12 @@ select {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.pdfPresentationMode:fullscreen .textLayer > span {
|
||||
.pdfPresentationMode:fullscreen .textLayer span {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.pdfPresentationMode.pdfPresentationModeControls > *,
|
||||
.pdfPresentationMode.pdfPresentationModeControls .textLayer > span {
|
||||
.pdfPresentationMode.pdfPresentationModeControls .textLayer span {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
@ -1653,19 +1653,19 @@ html[dir="rtl"] #documentPropertiesOverlay .row > * {
|
|||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .textLayer > span {
|
||||
#viewer.textLayer-visible .textLayer span {
|
||||
background-color: rgba(255, 255, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 1);
|
||||
border: solid 1px rgba(255, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#viewer.textLayer-hover .textLayer > span:hover {
|
||||
#viewer.textLayer-hover .textLayer span:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
#viewer.textLayer-shadow .textLayer > span {
|
||||
#viewer.textLayer-shadow .textLayer span {
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue