pdf.js/web/text_layer_builder.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

238 lines
7.1 KiB
JavaScript
Raw Permalink Normal View History

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.
*/
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("../src/display/api").TextContent} TextContent */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
// 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
import { normalizeUnicode, renderTextLayer, updateTextLayer } from "pdfjs-lib";
import { removeNullCharacters } from "./ui_utils.js";
/**
* @typedef {Object} TextLayerBuilderOptions
* @property {TextHighlighter} highlighter - Optional object that will handle
* highlighting text from the find controller.
* @property {TextAccessibilityManager} [accessibilityManager]
*/
2013-06-19 01:05:55 +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
* contain text that matches the PDF text they are overlaying.
2013-06-19 01:05:55 +09:00
*/
class TextLayerBuilder {
#enablePermissions = false;
#rotation = 0;
#scale = 0;
#textContentSource = null;
constructor({
highlighter = null,
accessibilityManager = null,
enablePermissions = false,
}) {
this.textContentItemsStr = [];
this.renderingDone = false;
this.textDivs = [];
this.textDivProperties = new WeakMap();
this.textLayerRenderTask = null;
this.highlighter = highlighter;
this.accessibilityManager = accessibilityManager;
this.#enablePermissions = enablePermissions === true;
/**
* Callback used to attach the textLayer to the DOM.
* @type {function}
*/
this.onAppend = null;
this.div = document.createElement("div");
this.div.className = "textLayer";
}
2013-06-19 01:05:55 +09:00
#finishRendering() {
this.renderingDone = true;
const endOfContent = document.createElement("div");
endOfContent.className = "endOfContent";
this.div.append(endOfContent);
this.#bindMouse();
}
get numTextDivs() {
return this.textDivs.length;
}
/**
* Renders the text layer.
* @param {PageViewport} viewport
*/
async render(viewport) {
if (!this.#textContentSource) {
throw new Error('No "textContentSource" parameter specified.');
}
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
const { rotation } = viewport;
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;
}
this.show();
return;
}
2013-06-19 01:05:55 +09:00
this.cancel();
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
this.accessibilityManager?.setTextMapping(this.textDivs);
this.textLayerRenderTask = renderTextLayer({
textContentSource: this.#textContentSource,
container: this.div,
viewport,
textDivs: this.textDivs,
textDivProperties: this.textDivProperties,
textContentItemsStr: this.textContentItemsStr,
});
await this.textLayerRenderTask.promise;
this.#finishRendering();
this.#scale = scale;
this.#rotation = rotation;
// Ensure that the textLayer is appended to the DOM *before* handling
// e.g. a pending search operation.
this.onAppend(this.div);
this.highlighter?.enable();
this.accessibilityManager?.enable();
}
hide() {
if (!this.div.hidden && this.renderingDone) {
// 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;
}
}
show() {
if (this.div.hidden && this.renderingDone) {
this.div.hidden = false;
this.highlighter?.enable();
}
}
/**
* Cancel rendering of the text layer.
*/
cancel() {
if (this.textLayerRenderTask) {
this.textLayerRenderTask.cancel();
this.textLayerRenderTask = null;
}
this.highlighter?.disable();
this.accessibilityManager?.disable();
this.textContentItemsStr.length = 0;
this.textDivs.length = 0;
this.textDivProperties = new WeakMap();
}
/**
* @param {ReadableStream | TextContent} source
*/
setTextContentSource(source) {
this.cancel();
this.#textContentSource = source;
}
2013-06-19 01:05:55 +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.
*/
#bindMouse() {
const { div } = this;
div.addEventListener("mousedown", evt => {
2019-12-23 02:18:29 +09:00
const end = div.querySelector(".endOfContent");
if (!end) {
return;
}
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
// On non-Firefox browsers, the selection will feel better if the height
// 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;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
adjustTop &&=
getComputedStyle(end).getPropertyValue("-moz-user-select") !==
"none";
}
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);
end.style.top = (r * 100).toFixed(2) + "%";
}
}
end.classList.add("active");
});
div.addEventListener("mouseup", () => {
2019-12-23 02:18:29 +09:00
const end = div.querySelector(".endOfContent");
if (!end) {
return;
}
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
end.style.top = "";
}
end.classList.remove("active");
});
div.addEventListener("copy", event => {
if (!this.#enablePermissions) {
const selection = document.getSelection();
event.clipboardData.setData(
"text/plain",
removeNullCharacters(normalizeUnicode(selection.toString()))
);
}
event.preventDefault();
event.stopPropagation();
});
}
}
export { TextLayerBuilder };