/* 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("./text_highlighter").TextHighlighter} TextHighlighter */ // eslint-disable-next-line max-len /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ import { renderTextLayer, updateTextLayer } from "pdfjs-lib"; /** * @typedef {Object} TextLayerBuilderOptions * @property {TextHighlighter} highlighter - Optional object that will handle * highlighting text from the find controller. * @property {TextAccessibilityManager} [accessibilityManager] * @property {boolean} [isOffscreenCanvasSupported] - Allows to use an * OffscreenCanvas if needed. */ /** * 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. */ class TextLayerBuilder { #scale = 0; #rotation = 0; constructor({ highlighter = null, accessibilityManager = null, isOffscreenCanvasSupported = true, }) { this.textContent = null; this.textContentItemsStr = []; this.textContentStream = null; this.renderingDone = false; this.textDivs = []; this.textDivProperties = new WeakMap(); this.textLayerRenderTask = null; this.highlighter = highlighter; this.accessibilityManager = accessibilityManager; this.isOffscreenCanvasSupported = isOffscreenCanvasSupported; this.div = document.createElement("div"); this.div.className = "textLayer"; } #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. */ async render(viewport) { if (!(this.textContent || this.textContentStream)) { throw new Error( `Neither "textContent" nor "textContentStream" specified.` ); } const scale = viewport.scale * (globalThis.devicePixelRatio || 1); if (this.renderingDone) { const { rotation } = viewport; 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, isOffscreenCanvasSupported: this.isOffscreenCanvasSupported, mustRescale, mustRotate, }); this.show(); this.#scale = scale; this.#rotation = rotation; } return; } this.cancel(); this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr); this.accessibilityManager?.setTextMapping(this.textDivs); this.textLayerRenderTask = renderTextLayer({ textContent: this.textContent, textContentStream: this.textContentStream, container: this.div, viewport, textDivs: this.textDivs, textDivProperties: this.textDivProperties, textContentItemsStr: this.textContentItemsStr, isOffscreenCanvasSupported: this.isOffscreenCanvasSupported, }); await this.textLayerRenderTask.promise; this.#finishRendering(); this.#scale = scale; this.accessibilityManager?.enable(); this.show(); } hide() { // 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() { 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(); } setTextContentStream(readableStream) { this.cancel(); this.textContentStream = readableStream; } setTextContent(textContent) { this.cancel(); this.textContent = textContent; } /** * 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 => { 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) { 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", () => { const end = div.querySelector(".endOfContent"); if (!end) { return; } if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { end.style.top = ""; } end.classList.remove("active"); }); } } export { TextLayerBuilder };