2013-06-19 01:05:55 +09:00
|
|
|
/* Copyright 2012 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.
|
|
|
|
*/
|
|
|
|
|
2021-12-15 07:59:17 +09:00
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
2022-12-05 01:42:24 +09:00
|
|
|
/** @typedef {import("../src/display/api").TextContent} TextContent */
|
2021-12-15 07:59:17 +09:00
|
|
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
2022-07-29 00:59:03 +09:00
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
Fix Viewer API definitions and include in CI
The Viewer API definitions do not compile because of missing imports and
anonymous objects are typed as `Object`. These issues were not caught
during CI because the test project was not compiling anything from the
Viewer API.
As an example of the first problem:
```
/**
* @implements MyInterface
*/
export class MyClass {
...
}
```
will generate a broken definition that doesn’t import MyInterface:
```
/**
* @implements MyInterface
*/
export class MyClass implements MyInterface {
...
}
```
This can be fixed by adding a typedef jsdoc to specify the import:
```
/** @typedef {import("./otherFile").MyInterface} MyInterface */
```
See https://github.com/jsdoc/jsdoc/issues/1537 and
https://github.com/microsoft/TypeScript/issues/22160 for more details.
As an example of the second problem:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} An Object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
*/
function getPageSizeInches({ view, userUnit, rotate }) {
...
}
```
generates the broken definition:
```
function getPageSizeInches({ view, userUnit, rotate }: Object) {
...
}
```
The jsdoc should specify the type of each nested property:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} options An object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
* @param {number[]} options.view
* @param {number} options.userUnit
* @param {number} options.rotate
*/
```
2021-08-26 07:44:06 +09:00
|
|
|
|
2023-03-23 18:15:14 +09:00
|
|
|
import { normalizeUnicode, renderTextLayer, updateTextLayer } from "pdfjs-lib";
|
|
|
|
import { removeNullCharacters } from "./ui_utils.js";
|
2016-04-09 02:34:27 +09:00
|
|
|
|
2014-09-21 02:21:49 +09:00
|
|
|
/**
|
|
|
|
* @typedef {Object} TextLayerBuilderOptions
|
2021-08-19 09:02:29 +09:00
|
|
|
* @property {TextHighlighter} highlighter - Optional object that will handle
|
|
|
|
* highlighting text from the find controller.
|
2022-07-29 00:59:03 +09:00
|
|
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
2014-09-21 02:21:49 +09:00
|
|
|
*/
|
|
|
|
|
2013-06-19 01:05:55 +09:00
|
|
|
/**
|
2017-06-29 06:15:16 +09:00
|
|
|
* The text layer builder provides text selection functionality for the PDF.
|
|
|
|
* It does this by creating overlay divs over the PDF's text. These divs
|
2021-08-19 09:02:29 +09:00
|
|
|
* contain text that matches the PDF text they are overlaying.
|
2013-06-19 01:05:55 +09:00
|
|
|
*/
|
2017-06-29 06:15:16 +09:00
|
|
|
class TextLayerBuilder {
|
2023-04-22 20:07:07 +09:00
|
|
|
#enablePermissions = false;
|
|
|
|
|
2022-12-05 01:42:24 +09:00
|
|
|
#rotation = 0;
|
|
|
|
|
2022-11-22 01:15:39 +09:00
|
|
|
#scale = 0;
|
|
|
|
|
2022-12-05 01:42:24 +09:00
|
|
|
#textContentSource = null;
|
2022-11-22 01:15:39 +09:00
|
|
|
|
2017-06-29 06:15:16 +09:00
|
|
|
constructor({
|
2021-08-19 09:02:29 +09:00
|
|
|
highlighter = null,
|
2022-07-29 00:59:03 +09:00
|
|
|
accessibilityManager = null,
|
2023-04-22 20:07:07 +09:00
|
|
|
enablePermissions = false,
|
2017-06-29 06:15:16 +09:00
|
|
|
}) {
|
2017-04-17 21:46:53 +09:00
|
|
|
this.textContentItemsStr = [];
|
2014-12-18 05:12:51 +09:00
|
|
|
this.renderingDone = false;
|
2014-06-24 05:02:33 +09:00
|
|
|
this.textDivs = [];
|
2022-11-22 01:15:39 +09:00
|
|
|
this.textDivProperties = new WeakMap();
|
2015-11-11 00:45:03 +09:00
|
|
|
this.textLayerRenderTask = null;
|
2021-08-19 09:02:29 +09:00
|
|
|
this.highlighter = highlighter;
|
2022-07-29 00:59:03 +09:00
|
|
|
this.accessibilityManager = accessibilityManager;
|
2023-04-22 20:07:07 +09:00
|
|
|
this.#enablePermissions = enablePermissions === true;
|
2017-06-29 06:15:16 +09:00
|
|
|
|
2023-10-27 21:55:12 +09:00
|
|
|
/**
|
|
|
|
* Callback used to attach the textLayer to the DOM.
|
|
|
|
* @type {function}
|
|
|
|
*/
|
|
|
|
this.onAppend = null;
|
|
|
|
|
2022-11-22 01:15:39 +09:00
|
|
|
this.div = document.createElement("div");
|
|
|
|
this.div.className = "textLayer";
|
2014-06-24 05:02:33 +09:00
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
|
2022-01-19 02:09:12 +09:00
|
|
|
#finishRendering() {
|
2017-06-29 06:15:16 +09:00
|
|
|
this.renderingDone = true;
|
|
|
|
|
2022-01-19 02:09:12 +09:00
|
|
|
const endOfContent = document.createElement("div");
|
|
|
|
endOfContent.className = "endOfContent";
|
2022-11-22 01:15:39 +09:00
|
|
|
this.div.append(endOfContent);
|
2017-06-29 06:15:16 +09:00
|
|
|
|
2022-11-22 01:15:39 +09:00
|
|
|
this.#bindMouse();
|
|
|
|
}
|
|
|
|
|
|
|
|
get numTextDivs() {
|
|
|
|
return this.textDivs.length;
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
2015-11-04 05:20:34 +09:00
|
|
|
|
2017-06-29 06:15:16 +09:00
|
|
|
/**
|
|
|
|
* Renders the text layer.
|
2022-12-05 01:42:24 +09:00
|
|
|
* @param {PageViewport} viewport
|
2017-06-29 06:15:16 +09:00
|
|
|
*/
|
2022-11-22 01:15:39 +09:00
|
|
|
async render(viewport) {
|
2022-12-05 01:42:24 +09:00
|
|
|
if (!this.#textContentSource) {
|
|
|
|
throw new Error('No "textContentSource" parameter specified.');
|
2022-11-22 01:15:39 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
|
2022-12-12 22:24:27 +09:00
|
|
|
const { rotation } = viewport;
|
2022-11-22 01:15:39 +09:00
|
|
|
if (this.renderingDone) {
|
|
|
|
const mustRotate = rotation !== this.#rotation;
|
|
|
|
const mustRescale = scale !== this.#scale;
|
|
|
|
if (mustRotate || mustRescale) {
|
|
|
|
this.hide();
|
|
|
|
updateTextLayer({
|
|
|
|
container: this.div,
|
|
|
|
viewport,
|
|
|
|
textDivs: this.textDivs,
|
|
|
|
textDivProperties: this.textDivProperties,
|
|
|
|
mustRescale,
|
|
|
|
mustRotate,
|
|
|
|
});
|
|
|
|
this.#scale = scale;
|
|
|
|
this.#rotation = rotation;
|
|
|
|
}
|
2022-12-12 22:24:27 +09:00
|
|
|
this.show();
|
2017-06-29 06:15:16 +09:00
|
|
|
return;
|
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
|
2022-11-22 01:15:39 +09:00
|
|
|
this.cancel();
|
Export the XFA/StructTree-layers in the viewer components
While e.g. the `simpleviewer` and `singlepageviewer` examples work, since they're based on the `BaseViewer`-class, the standalone `pageviewer` example currently doesn't support either XFA- or StructTree-layers. This seems like an obvious oversight, which can be easily addressed simply by exporting the necessary functionality through `pdf_viewer.component.js`, similar to the existing Text/Annotation-layers.
While working on, and testing, these changes I happened to notice a number of smaller things that's also fixed in this patch:
- Ensure that `XfaLayerBuilder.render` always have a *consistent* return type, to prevent possible run-time failures in `PDFPageView`; PR 13908 follow-up.
- Change the order of the options in the `XfaLayerBuilder`-constructor to agree with the parameter order in the `DefaultXfaLayerFactory.createXfaLayerBuilder`-method.
- Add a missing `textHighlighterFactory`-option, in the JSDocs for the `PDFPageView`-class.
- A couple of small tweaks in the `TextLayerBuilder.render`-method: Re-use an existing Array rather than creating a new one, and replace an `if` with optional chaining instead.
*Please note:* For now XFA-support is currently disabled by default, similar to the regular viewer.
2021-08-29 01:09:39 +09:00
|
|
|
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
|
2022-07-29 00:59:03 +09:00
|
|
|
this.accessibilityManager?.setTextMapping(this.textDivs);
|
Export the XFA/StructTree-layers in the viewer components
While e.g. the `simpleviewer` and `singlepageviewer` examples work, since they're based on the `BaseViewer`-class, the standalone `pageviewer` example currently doesn't support either XFA- or StructTree-layers. This seems like an obvious oversight, which can be easily addressed simply by exporting the necessary functionality through `pdf_viewer.component.js`, similar to the existing Text/Annotation-layers.
While working on, and testing, these changes I happened to notice a number of smaller things that's also fixed in this patch:
- Ensure that `XfaLayerBuilder.render` always have a *consistent* return type, to prevent possible run-time failures in `PDFPageView`; PR 13908 follow-up.
- Change the order of the options in the `XfaLayerBuilder`-constructor to agree with the parameter order in the `DefaultXfaLayerFactory.createXfaLayerBuilder`-method.
- Add a missing `textHighlighterFactory`-option, in the JSDocs for the `PDFPageView`-class.
- A couple of small tweaks in the `TextLayerBuilder.render`-method: Re-use an existing Array rather than creating a new one, and replace an `if` with optional chaining instead.
*Please note:* For now XFA-support is currently disabled by default, similar to the regular viewer.
2021-08-29 01:09:39 +09:00
|
|
|
|
2017-06-29 06:15:16 +09:00
|
|
|
this.textLayerRenderTask = renderTextLayer({
|
2022-12-05 01:42:24 +09:00
|
|
|
textContentSource: this.#textContentSource,
|
2022-11-22 01:15:39 +09:00
|
|
|
container: this.div,
|
|
|
|
viewport,
|
2017-06-29 06:15:16 +09:00
|
|
|
textDivs: this.textDivs,
|
2022-11-22 01:15:39 +09:00
|
|
|
textDivProperties: this.textDivProperties,
|
2017-06-29 06:15:16 +09:00
|
|
|
textContentItemsStr: this.textContentItemsStr,
|
|
|
|
});
|
2022-11-22 01:15:39 +09:00
|
|
|
|
|
|
|
await this.textLayerRenderTask.promise;
|
|
|
|
this.#finishRendering();
|
|
|
|
this.#scale = scale;
|
2022-12-12 22:24:27 +09:00
|
|
|
this.#rotation = rotation;
|
2023-10-27 21:55:12 +09:00
|
|
|
// Ensure that the textLayer is appended to the DOM *before* handling
|
|
|
|
// e.g. a pending search operation.
|
|
|
|
this.onAppend(this.div);
|
|
|
|
this.highlighter?.enable();
|
2022-12-17 05:34:12 +09:00
|
|
|
this.accessibilityManager?.enable();
|
2022-11-22 01:15:39 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
hide() {
|
2023-10-27 21:55:12 +09:00
|
|
|
if (!this.div.hidden && this.renderingDone) {
|
2022-12-12 22:24:27 +09:00
|
|
|
// We turn off the highlighter in order to avoid to scroll into view an
|
|
|
|
// element of the text layer which could be hidden.
|
|
|
|
this.highlighter?.disable();
|
|
|
|
this.div.hidden = true;
|
|
|
|
}
|
2022-11-22 01:15:39 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
2022-12-12 22:24:27 +09:00
|
|
|
if (this.div.hidden && this.renderingDone) {
|
|
|
|
this.div.hidden = false;
|
|
|
|
this.highlighter?.enable();
|
|
|
|
}
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
2014-06-24 05:02:33 +09:00
|
|
|
|
2017-06-29 06:15:16 +09:00
|
|
|
/**
|
|
|
|
* Cancel rendering of the text layer.
|
|
|
|
*/
|
|
|
|
cancel() {
|
|
|
|
if (this.textLayerRenderTask) {
|
|
|
|
this.textLayerRenderTask.cancel();
|
|
|
|
this.textLayerRenderTask = null;
|
|
|
|
}
|
2021-08-19 09:02:29 +09:00
|
|
|
this.highlighter?.disable();
|
2022-07-29 00:59:03 +09:00
|
|
|
this.accessibilityManager?.disable();
|
2022-11-22 01:15:39 +09:00
|
|
|
this.textContentItemsStr.length = 0;
|
|
|
|
this.textDivs.length = 0;
|
|
|
|
this.textDivProperties = new WeakMap();
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
2014-06-24 05:02:33 +09:00
|
|
|
|
2022-12-05 01:42:24 +09:00
|
|
|
/**
|
|
|
|
* @param {ReadableStream | TextContent} source
|
|
|
|
*/
|
|
|
|
setTextContentSource(source) {
|
2017-06-29 06:15:16 +09:00
|
|
|
this.cancel();
|
2022-12-05 01:42:24 +09:00
|
|
|
this.#textContentSource = source;
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
|
2017-06-29 06:15:16 +09:00
|
|
|
/**
|
|
|
|
* Improves text selection by adding an additional div where the mouse was
|
|
|
|
* clicked. This reduces flickering of the content if the mouse is slowly
|
|
|
|
* dragged up or down.
|
|
|
|
*/
|
2022-01-19 02:09:12 +09:00
|
|
|
#bindMouse() {
|
2022-11-22 01:15:39 +09:00
|
|
|
const { div } = this;
|
2017-06-29 06:15:16 +09:00
|
|
|
|
|
|
|
div.addEventListener("mousedown", evt => {
|
2019-12-23 02:18:29 +09:00
|
|
|
const end = div.querySelector(".endOfContent");
|
2017-06-29 06:15:16 +09:00
|
|
|
if (!end) {
|
|
|
|
return;
|
2016-05-26 22:24:58 +09:00
|
|
|
}
|
2020-01-08 21:57:31 +09:00
|
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
2015-11-04 05:20:34 +09:00
|
|
|
// On non-Firefox browsers, the selection will feel better if the height
|
2017-06-29 06:15:16 +09:00
|
|
|
// of the `endOfContent` div is adjusted to start at mouse click
|
|
|
|
// location. This avoids flickering when the selection moves up.
|
|
|
|
// However it does not work when selection is started on empty space.
|
|
|
|
let adjustTop = evt.target !== div;
|
2016-10-15 00:57:53 +09:00
|
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
2022-01-19 02:09:12 +09:00
|
|
|
adjustTop &&=
|
|
|
|
getComputedStyle(end).getPropertyValue("-moz-user-select") !==
|
|
|
|
"none";
|
2016-10-15 00:57:53 +09:00
|
|
|
}
|
2015-11-04 05:20:34 +09:00
|
|
|
if (adjustTop) {
|
2019-12-23 02:18:29 +09:00
|
|
|
const divBounds = div.getBoundingClientRect();
|
|
|
|
const r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
|
2015-11-04 05:20:34 +09:00
|
|
|
end.style.top = (r * 100).toFixed(2) + "%";
|
|
|
|
}
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
|
|
|
end.classList.add("active");
|
|
|
|
});
|
|
|
|
|
|
|
|
div.addEventListener("mouseup", () => {
|
2019-12-23 02:18:29 +09:00
|
|
|
const end = div.querySelector(".endOfContent");
|
2017-06-29 06:15:16 +09:00
|
|
|
if (!end) {
|
|
|
|
return;
|
|
|
|
}
|
2020-01-08 21:57:31 +09:00
|
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
2017-06-29 06:15:16 +09:00
|
|
|
end.style.top = "";
|
|
|
|
}
|
|
|
|
end.classList.remove("active");
|
|
|
|
});
|
2023-03-23 18:15:14 +09:00
|
|
|
|
|
|
|
div.addEventListener("copy", event => {
|
2023-04-22 20:07:07 +09:00
|
|
|
if (!this.#enablePermissions) {
|
|
|
|
const selection = document.getSelection();
|
|
|
|
event.clipboardData.setData(
|
|
|
|
"text/plain",
|
|
|
|
removeNullCharacters(normalizeUnicode(selection.toString()))
|
|
|
|
);
|
|
|
|
}
|
2023-03-23 18:15:14 +09:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
});
|
2017-06-29 06:15:16 +09:00
|
|
|
}
|
|
|
|
}
|
2014-12-18 05:47:14 +09:00
|
|
|
|
2021-12-15 21:54:29 +09:00
|
|
|
export { TextLayerBuilder };
|