Refactor the text layer code in order to avoid to recompute it on each draw
The idea is just to resuse what we got on the first draw. Now, we only update the scaleX of the different spans and the other values are dependant of --scale-factor. Move some properties in the CSS in order to avoid any updates in JS.
This commit is contained in:
parent
fa54a58790
commit
eed9bf71c5
@ -16,6 +16,7 @@
|
||||
import {
|
||||
AbortException,
|
||||
createPromiseCapability,
|
||||
FeatureTest,
|
||||
Util,
|
||||
} from "../shared/util.js";
|
||||
|
||||
@ -27,16 +28,40 @@ import {
|
||||
* render (the object is returned by the page's `getTextContent` method).
|
||||
* @property {ReadableStream} [textContentStream] - Text content stream to
|
||||
* render (the stream is returned by the page's `streamTextContent` method).
|
||||
* @property {DocumentFragment | HTMLElement} container - The DOM node that
|
||||
* will contain the text runs.
|
||||
* @property {HTMLElement} container - The DOM node that will contain the text
|
||||
* runs.
|
||||
* @property {import("./display_utils").PageViewport} viewport - The target
|
||||
* viewport to properly layout the text runs.
|
||||
* @property {Array<HTMLElement>} [textDivs] - HTML elements that correspond to
|
||||
* the text items of the textContent input.
|
||||
* This is output and shall initially be set to an empty array.
|
||||
* @property {WeakMap<HTMLElement,Object>} [textDivProperties] - Some properties
|
||||
* weakly mapped to the HTML elements used to render the text.
|
||||
* @property {Array<string>} [textContentItemsStr] - Strings that correspond to
|
||||
* the `str` property of the text items of the textContent input.
|
||||
* This is output and shall initially be set to an empty array.
|
||||
* @property {boolean} [isOffscreenCanvasSupported] true if we can use
|
||||
* OffscreenCanvas to measure string widths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Text layer update parameters.
|
||||
*
|
||||
* @typedef {Object} TextLayerUpdateParameters
|
||||
* @property {HTMLElement} container - The DOM node that will contain the text
|
||||
* runs.
|
||||
* @property {import("./display_utils").PageViewport} viewport - The target
|
||||
* viewport to properly layout the text runs.
|
||||
* @property {Array<HTMLElement>} [textDivs] - HTML elements that correspond to
|
||||
* the text items of the textContent input.
|
||||
* This is output and shall initially be set to an empty array.
|
||||
* @property {WeakMap<HTMLElement,Object>} [textDivProperties] - Some properties
|
||||
* weakly mapped to the HTML elements used to render the text.
|
||||
* @property {boolean} [isOffscreenCanvasSupported] true if we can use
|
||||
* OffscreenCanvas to measure string widths.
|
||||
* @property {boolean} [mustRotate] true if the text layer must be rotated.
|
||||
* @property {boolean} [mustRescale] true if the text layer contents must be
|
||||
* rescaled.
|
||||
*/
|
||||
|
||||
const MAX_TEXT_DIVS_TO_RENDER = 100000;
|
||||
@ -44,13 +69,27 @@ const DEFAULT_FONT_SIZE = 30;
|
||||
const DEFAULT_FONT_ASCENT = 0.8;
|
||||
const ascentCache = new Map();
|
||||
|
||||
function getAscent(fontFamily, ctx) {
|
||||
function getCtx(size, isOffscreenCanvasSupported) {
|
||||
let ctx;
|
||||
if (isOffscreenCanvasSupported && FeatureTest.isOffscreenCanvasSupported) {
|
||||
ctx = new OffscreenCanvas(size, size).getContext("2d", { alpha: false });
|
||||
} else {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = canvas.height = size;
|
||||
ctx = canvas.getContext("2d", { alpha: false });
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function getAscent(fontFamily, isOffscreenCanvasSupported) {
|
||||
const cachedAscent = ascentCache.get(fontFamily);
|
||||
if (cachedAscent) {
|
||||
return cachedAscent;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
const ctx = getCtx(DEFAULT_FONT_SIZE, isOffscreenCanvasSupported);
|
||||
|
||||
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
|
||||
const metrics = ctx.measureText("");
|
||||
|
||||
@ -58,9 +97,10 @@ function getAscent(fontFamily, ctx) {
|
||||
let ascent = metrics.fontBoundingBoxAscent;
|
||||
let descent = Math.abs(metrics.fontBoundingBoxDescent);
|
||||
if (ascent) {
|
||||
ctx.restore();
|
||||
const ratio = ascent / (ascent + descent);
|
||||
ascentCache.set(fontFamily, ratio);
|
||||
|
||||
ctx.canvas.width = ctx.canvas.height = 0;
|
||||
return ratio;
|
||||
}
|
||||
|
||||
@ -99,7 +139,7 @@ function getAscent(fontFamily, ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
ctx.canvas.width = ctx.canvas.height = 0;
|
||||
|
||||
if (ascent) {
|
||||
const ratio = ascent / (ascent + descent);
|
||||
@ -111,7 +151,7 @@ function getAscent(fontFamily, ctx) {
|
||||
return DEFAULT_FONT_ASCENT;
|
||||
}
|
||||
|
||||
function appendText(task, geom, styles, ctx) {
|
||||
function appendText(task, geom, styles) {
|
||||
// Initialize all used properties to keep the caches monomorphic.
|
||||
const textDiv = document.createElement("span");
|
||||
const textDivProperties = {
|
||||
@ -123,14 +163,15 @@ function appendText(task, geom, styles, ctx) {
|
||||
};
|
||||
task._textDivs.push(textDiv);
|
||||
|
||||
const tx = Util.transform(task._viewport.transform, geom.transform);
|
||||
const tx = Util.transform(task._transform, geom.transform);
|
||||
let angle = Math.atan2(tx[1], tx[0]);
|
||||
const style = styles[geom.fontName];
|
||||
if (style.vertical) {
|
||||
angle += Math.PI / 2;
|
||||
}
|
||||
const fontHeight = Math.hypot(tx[2], tx[3]);
|
||||
const fontAscent = fontHeight * getAscent(style.fontFamily, ctx);
|
||||
const fontAscent =
|
||||
fontHeight * getAscent(style.fontFamily, task._isOffscreenCanvasSupported);
|
||||
|
||||
let left, top;
|
||||
if (angle === 0) {
|
||||
@ -140,12 +181,21 @@ function appendText(task, geom, styles, ctx) {
|
||||
left = tx[4] + fontAscent * Math.sin(angle);
|
||||
top = tx[5] - fontAscent * Math.cos(angle);
|
||||
}
|
||||
|
||||
const scaleFactorStr = "calc(var(--scale-factor)*";
|
||||
const divStyle = textDiv.style;
|
||||
// Setting the style properties individually, rather than all at once,
|
||||
// should be OK since the `textDiv` isn't appended to the document yet.
|
||||
textDiv.style.left = `${left}px`;
|
||||
textDiv.style.top = `${top}px`;
|
||||
textDiv.style.fontSize = `${fontHeight}px`;
|
||||
textDiv.style.fontFamily = style.fontFamily;
|
||||
if (task._container === task._rootContainer) {
|
||||
divStyle.left = `${((100 * left) / task._pageWidth).toFixed(2)}%`;
|
||||
divStyle.top = `${((100 * top) / task._pageHeight).toFixed(2)}%`;
|
||||
} else {
|
||||
// We're in a marked content span, hence we can't use percents.
|
||||
divStyle.left = `${scaleFactorStr}${left.toFixed(2)}px)`;
|
||||
divStyle.top = `${scaleFactorStr}${top.toFixed(2)}px)`;
|
||||
}
|
||||
divStyle.fontSize = `${scaleFactorStr}${fontHeight.toFixed(2)}px)`;
|
||||
divStyle.fontFamily = style.fontFamily;
|
||||
|
||||
textDivProperties.fontSize = fontHeight;
|
||||
|
||||
@ -183,11 +233,7 @@ function appendText(task, geom, styles, ctx) {
|
||||
}
|
||||
}
|
||||
if (shouldScaleText) {
|
||||
if (style.vertical) {
|
||||
textDivProperties.canvasWidth = geom.height * task._viewport.scale;
|
||||
} else {
|
||||
textDivProperties.canvasWidth = geom.width * task._viewport.scale;
|
||||
}
|
||||
textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width;
|
||||
}
|
||||
task._textDivProperties.set(textDiv, textDivProperties);
|
||||
if (task._textContentStream) {
|
||||
@ -195,6 +241,35 @@ function appendText(task, geom, styles, ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
function layout(params) {
|
||||
const { div, scale, properties, ctx, prevFontSize, prevFontFamily } = params;
|
||||
const { style } = div;
|
||||
let transform = "";
|
||||
if (properties.canvasWidth !== 0 && properties.hasText) {
|
||||
const { fontFamily } = style;
|
||||
const { canvasWidth, fontSize } = properties;
|
||||
|
||||
if (prevFontSize !== fontSize || prevFontFamily !== fontFamily) {
|
||||
ctx.font = `${fontSize * scale}px ${fontFamily}`;
|
||||
params.prevFontSize = fontSize;
|
||||
params.prevFontFamily = fontFamily;
|
||||
}
|
||||
|
||||
// Only measure the width for multi-char text divs, see `appendText`.
|
||||
const { width } = ctx.measureText(div.textContent);
|
||||
|
||||
if (width > 0) {
|
||||
transform = `scaleX(${(canvasWidth * scale) / width})`;
|
||||
}
|
||||
}
|
||||
if (properties.angle !== 0) {
|
||||
transform = `rotate(${properties.angle}deg) ${transform}`;
|
||||
}
|
||||
if (transform.length > 0) {
|
||||
style.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
function render(task) {
|
||||
if (task._canceled) {
|
||||
return;
|
||||
@ -228,40 +303,41 @@ class TextLayerRenderTask {
|
||||
container,
|
||||
viewport,
|
||||
textDivs,
|
||||
textDivProperties,
|
||||
textContentItemsStr,
|
||||
isOffscreenCanvasSupported,
|
||||
}) {
|
||||
this._textContent = textContent;
|
||||
this._textContentStream = textContentStream;
|
||||
this._container = container;
|
||||
this._document = container.ownerDocument;
|
||||
this._viewport = viewport;
|
||||
this._container = this._rootContainer = container;
|
||||
this._textDivs = textDivs || [];
|
||||
this._textContentItemsStr = textContentItemsStr || [];
|
||||
this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
|
||||
|
||||
this._reader = null;
|
||||
this._layoutTextLastFontSize = null;
|
||||
this._layoutTextLastFontFamily = null;
|
||||
this._layoutTextCtx = null;
|
||||
this._textDivProperties = new WeakMap();
|
||||
this._textDivProperties = textDivProperties || new WeakMap();
|
||||
this._renderingDone = false;
|
||||
this._canceled = false;
|
||||
this._capability = createPromiseCapability();
|
||||
this._devicePixelRatio = globalThis.devicePixelRatio || 1;
|
||||
this._layoutTextParams = {
|
||||
prevFontSize: null,
|
||||
prevFontFamily: null,
|
||||
div: null,
|
||||
scale: viewport.scale * (globalThis.devicePixelRatio || 1),
|
||||
properties: null,
|
||||
ctx: getCtx(0, isOffscreenCanvasSupported),
|
||||
};
|
||||
const [pageLLx, pageLLy, pageURx, pageURy] = viewport.viewBox;
|
||||
this._transform = [1, 0, 0, -1, -pageLLx, pageURy];
|
||||
this._pageWidth = pageURx - pageLLx;
|
||||
this._pageHeight = pageURy - pageLLy;
|
||||
|
||||
setTextLayerDimensions(container, viewport);
|
||||
|
||||
// Always clean-up the temporary canvas once rendering is no longer pending.
|
||||
this._capability.promise
|
||||
.finally(() => {
|
||||
// The `textDiv` properties are no longer needed.
|
||||
this._textDivProperties = null;
|
||||
|
||||
if (this._layoutTextCtx) {
|
||||
// Zeroing the width and height cause Firefox to release graphics
|
||||
// resources immediately, which can greatly reduce memory consumption.
|
||||
this._layoutTextCtx.canvas.width = 0;
|
||||
this._layoutTextCtx.canvas.height = 0;
|
||||
this._layoutTextCtx = null;
|
||||
}
|
||||
this._layoutTextParams = null;
|
||||
})
|
||||
.catch(() => {
|
||||
// Avoid "Uncaught promise" messages in the console.
|
||||
@ -289,7 +365,7 @@ class TextLayerRenderTask {
|
||||
});
|
||||
this._reader = null;
|
||||
}
|
||||
this._capability.reject(new Error("TextLayer task cancelled."));
|
||||
this._capability.reject(new AbortException("TextLayer task cancelled."));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -315,7 +391,7 @@ class TextLayerRenderTask {
|
||||
continue;
|
||||
}
|
||||
this._textContentItemsStr.push(item.str);
|
||||
appendText(this, item, styleCache, this._layoutTextCtx);
|
||||
appendText(this, item, styleCache);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,39 +399,10 @@ class TextLayerRenderTask {
|
||||
* @private
|
||||
*/
|
||||
_layoutText(textDiv) {
|
||||
const textDivProperties = this._textDivProperties.get(textDiv);
|
||||
|
||||
let transform = "";
|
||||
if (textDivProperties.canvasWidth !== 0 && textDivProperties.hasText) {
|
||||
const { fontFamily } = textDiv.style;
|
||||
const { fontSize } = textDivProperties;
|
||||
|
||||
// Only build font string and set to context if different from last.
|
||||
if (
|
||||
fontSize !== this._layoutTextLastFontSize ||
|
||||
fontFamily !== this._layoutTextLastFontFamily
|
||||
) {
|
||||
this._layoutTextCtx.font = `${
|
||||
fontSize * this._devicePixelRatio
|
||||
}px ${fontFamily}`;
|
||||
this._layoutTextLastFontSize = fontSize;
|
||||
this._layoutTextLastFontFamily = fontFamily;
|
||||
}
|
||||
// Only measure the width for multi-char text divs, see `appendText`.
|
||||
const { width } = this._layoutTextCtx.measureText(textDiv.textContent);
|
||||
|
||||
if (width > 0) {
|
||||
transform = `scaleX(${
|
||||
(this._devicePixelRatio * textDivProperties.canvasWidth) / width
|
||||
})`;
|
||||
}
|
||||
}
|
||||
if (textDivProperties.angle !== 0) {
|
||||
transform = `rotate(${textDivProperties.angle}deg) ${transform}`;
|
||||
}
|
||||
if (transform.length > 0) {
|
||||
textDiv.style.transform = transform;
|
||||
}
|
||||
const textDivProperties = (this._layoutTextParams.properties =
|
||||
this._textDivProperties.get(textDiv));
|
||||
this._layoutTextParams.div = textDiv;
|
||||
layout(this._layoutTextParams);
|
||||
|
||||
if (textDivProperties.hasText) {
|
||||
this._container.append(textDiv);
|
||||
@ -375,10 +422,6 @@ class TextLayerRenderTask {
|
||||
let styleCache = Object.create(null);
|
||||
|
||||
// The temporary canvas is used to measure text length in the DOM.
|
||||
const canvas = this._document.createElement("canvas");
|
||||
canvas.height = canvas.width = DEFAULT_FONT_SIZE;
|
||||
|
||||
this._layoutTextCtx = canvas.getContext("2d", { alpha: false });
|
||||
|
||||
if (this._textContent) {
|
||||
const textItems = this._textContent.items;
|
||||
@ -426,9 +469,67 @@ function renderTextLayer(renderParameters) {
|
||||
viewport: renderParameters.viewport,
|
||||
textDivs: renderParameters.textDivs,
|
||||
textContentItemsStr: renderParameters.textContentItemsStr,
|
||||
textDivProperties: renderParameters.textDivProperties,
|
||||
isOffscreenCanvasSupported: renderParameters.isOffscreenCanvasSupported,
|
||||
});
|
||||
task._render();
|
||||
return task;
|
||||
}
|
||||
|
||||
export { renderTextLayer, TextLayerRenderTask };
|
||||
/**
|
||||
* @param {TextLayerUpdateParameters} renderParameters
|
||||
* @returns {TextLayerRenderTask}
|
||||
*/
|
||||
function updateTextLayer({
|
||||
container,
|
||||
viewport,
|
||||
textDivs,
|
||||
textDivProperties,
|
||||
isOffscreenCanvasSupported,
|
||||
mustRotate = true,
|
||||
mustRescale = true,
|
||||
}) {
|
||||
if (mustRotate) {
|
||||
setTextLayerDimensions(container, { rotation: viewport.rotation });
|
||||
}
|
||||
|
||||
if (mustRescale) {
|
||||
const ctx = getCtx(0, isOffscreenCanvasSupported);
|
||||
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
|
||||
const params = {
|
||||
prevFontSize: null,
|
||||
prevFontFamily: null,
|
||||
div: null,
|
||||
scale,
|
||||
properties: null,
|
||||
ctx,
|
||||
};
|
||||
for (const div of textDivs) {
|
||||
params.properties = textDivProperties.get(div);
|
||||
params.div = div;
|
||||
layout(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDivElement} div
|
||||
* @param {import("./display_utils").PageViewport} viewport
|
||||
*/
|
||||
function setTextLayerDimensions(div, viewport) {
|
||||
if (!viewport.viewBox) {
|
||||
div.setAttribute("data-main-rotation", viewport.rotation);
|
||||
return;
|
||||
}
|
||||
|
||||
const [pageLLx, pageLLy, pageURx, pageURy] = viewport.viewBox;
|
||||
const pageWidth = pageURx - pageLLx;
|
||||
const pageHeight = pageURy - pageLLy;
|
||||
const { style } = div;
|
||||
|
||||
style.width = `calc(var(--scale-factor) * ${pageWidth}px)`;
|
||||
style.height = `calc(var(--scale-factor) * ${pageHeight}px)`;
|
||||
div.setAttribute("data-main-rotation", viewport.rotation);
|
||||
}
|
||||
|
||||
export { renderTextLayer, TextLayerRenderTask, updateTextLayer };
|
||||
|
@ -23,6 +23,7 @@
|
||||
/** @typedef {import("./display/text_layer").TextLayerRenderTask} TextLayerRenderTask */
|
||||
|
||||
import {
|
||||
AbortException,
|
||||
AnnotationEditorParamsType,
|
||||
AnnotationEditorType,
|
||||
AnnotationMode,
|
||||
@ -60,12 +61,12 @@ import {
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
} from "./display/display_utils.js";
|
||||
import { renderTextLayer, updateTextLayer } from "./display/text_layer.js";
|
||||
import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js";
|
||||
import { AnnotationEditorUIManager } from "./display/editor/tools.js";
|
||||
import { AnnotationLayer } from "./display/annotation_layer.js";
|
||||
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
||||
import { isNodeJS } from "./shared/is_node.js";
|
||||
import { renderTextLayer } from "./display/text_layer.js";
|
||||
import { SVGGraphics } from "./display/svg.js";
|
||||
import { XfaLayer } from "./display/xfa_layer.js";
|
||||
|
||||
@ -110,6 +111,7 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
|
||||
}
|
||||
|
||||
export {
|
||||
AbortException,
|
||||
AnnotationEditorLayer,
|
||||
AnnotationEditorParamsType,
|
||||
AnnotationEditorType,
|
||||
@ -143,6 +145,7 @@ export {
|
||||
SVGGraphics,
|
||||
UnexpectedResponseException,
|
||||
UNSUPPORTED_FEATURES,
|
||||
updateTextLayer,
|
||||
Util,
|
||||
VerbosityLevel,
|
||||
version,
|
||||
|
@ -168,7 +168,7 @@ class Rasterize {
|
||||
}
|
||||
|
||||
static get textStylePromise() {
|
||||
const styles = ["./text_layer_test.css"];
|
||||
const styles = [VIEWER_CSS, "./text_layer_test.css"];
|
||||
return shadow(this, "textStylePromise", loadStyles(styles));
|
||||
}
|
||||
|
||||
@ -256,8 +256,10 @@ class Rasterize {
|
||||
// Items are transformed to have 1px font size.
|
||||
svg.setAttribute("font-size", 1);
|
||||
|
||||
const [overrides] = await this.textStylePromise;
|
||||
style.textContent = overrides;
|
||||
const [common, overrides] = await this.textStylePromise;
|
||||
style.textContent =
|
||||
`${common}\n${overrides}\n` +
|
||||
`:root { --scale-factor: ${viewport.scale} }`;
|
||||
|
||||
// Rendering text layer as HTML.
|
||||
const task = renderTextLayer({
|
||||
@ -265,6 +267,7 @@ class Rasterize {
|
||||
container: div,
|
||||
viewport,
|
||||
});
|
||||
|
||||
await task.promise;
|
||||
svg.append(foreignObject);
|
||||
|
||||
|
@ -22,9 +22,11 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
line-height: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
.textLayer span,
|
||||
.textLayer br {
|
||||
color: black;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
transform-origin: 0% 0%;
|
||||
|
@ -24,7 +24,7 @@ import { isNodeJS } from "../../src/shared/is_node.js";
|
||||
describe("textLayer", function () {
|
||||
it("creates textLayer from ReadableStream", async function () {
|
||||
if (isNodeJS) {
|
||||
pending("document.createDocumentFragment is not supported in Node.js.");
|
||||
pending("document.createElement is not supported in Node.js.");
|
||||
}
|
||||
const loadingTask = getDocument(buildGetDocumentParams("basicapi.pdf"));
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
@ -34,7 +34,7 @@ describe("textLayer", function () {
|
||||
|
||||
const textLayerRenderTask = renderTextLayer({
|
||||
textContentStream: page.streamTextContent(),
|
||||
container: document.createDocumentFragment(),
|
||||
container: document.createElement("div"),
|
||||
viewport: page.getViewport(),
|
||||
textContentItemsStr,
|
||||
});
|
||||
|
@ -501,6 +501,7 @@ const PDFViewerApplication = {
|
||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
|
||||
isOffscreenCanvasSupported: AppOptions.get("isOffscreenCanvasSupported"),
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
enablePermissions: AppOptions.get("enablePermissions"),
|
||||
pageColors,
|
||||
|
@ -165,12 +165,9 @@ class DefaultStructTreeLayerFactory {
|
||||
class DefaultTextLayerFactory {
|
||||
/**
|
||||
* @typedef {Object} CreateTextLayerBuilderParameters
|
||||
* @property {HTMLDivElement} textLayerDiv
|
||||
* @property {number} pageIndex
|
||||
* @property {PageViewport} viewport
|
||||
* @property {EventBus} eventBus
|
||||
* @property {TextHighlighter} highlighter
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {boolean} [isOffscreenCanvasSupported]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -178,20 +175,14 @@ class DefaultTextLayerFactory {
|
||||
* @returns {TextLayerBuilder}
|
||||
*/
|
||||
createTextLayerBuilder({
|
||||
textLayerDiv,
|
||||
pageIndex,
|
||||
viewport,
|
||||
eventBus,
|
||||
highlighter,
|
||||
accessibilityManager = null,
|
||||
isOffscreenCanvasSupported = true,
|
||||
}) {
|
||||
return new TextLayerBuilder({
|
||||
textLayerDiv,
|
||||
pageIndex,
|
||||
viewport,
|
||||
eventBus,
|
||||
highlighter,
|
||||
accessibilityManager,
|
||||
isOffscreenCanvasSupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@
|
||||
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */
|
||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||
@ -168,12 +167,9 @@ class IRenderableView {
|
||||
class IPDFTextLayerFactory {
|
||||
/**
|
||||
* @typedef {Object} CreateTextLayerBuilderParameters
|
||||
* @property {HTMLDivElement} textLayerDiv
|
||||
* @property {number} pageIndex
|
||||
* @property {PageViewport} viewport
|
||||
* @property {EventBus} eventBus
|
||||
* @property {TextHighlighter} highlighter
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {boolean} [isOffscreenCanvasSupported]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -181,12 +177,9 @@ class IPDFTextLayerFactory {
|
||||
* @returns {TextLayerBuilder}
|
||||
*/
|
||||
createTextLayerBuilder({
|
||||
textLayerDiv,
|
||||
pageIndex,
|
||||
viewport,
|
||||
eventBus,
|
||||
highlighter,
|
||||
accessibilityManager,
|
||||
isOffscreenCanvasSupported,
|
||||
}) {}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@
|
||||
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
||||
|
||||
import {
|
||||
AbortException,
|
||||
AnnotationMode,
|
||||
createPromiseCapability,
|
||||
PixelsPerInch,
|
||||
@ -82,6 +83,8 @@ import { TextAccessibilityManager } from "./text_accessibility.js";
|
||||
* for annotation icons. Include trailing slash.
|
||||
* @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
|
||||
* value is `false`.
|
||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
||||
* OffscreenCanvas if needed.
|
||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||
* total pixels, i.e. width * height. Use -1 for no limit. The default value
|
||||
* is 4096 * 4096 (16 mega-pixels).
|
||||
@ -128,6 +131,8 @@ class PDFPageView {
|
||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
|
||||
this.isOffscreenCanvasSupported =
|
||||
options.isOffscreenCanvasSupported ?? true;
|
||||
this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
|
||||
this.pageColors = options.pageColors || null;
|
||||
|
||||
@ -174,8 +179,8 @@ class PDFPageView {
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "page";
|
||||
div.style.width = Math.floor(this.viewport.width) + "px";
|
||||
div.style.height = Math.floor(this.viewport.height) + "px";
|
||||
div.style.width = Math.round(this.viewport.width) + "px";
|
||||
div.style.height = Math.round(this.viewport.height) + "px";
|
||||
div.setAttribute("data-page-number", this.id);
|
||||
div.setAttribute("role", "region");
|
||||
this.l10n.get("page_landmark", { page: this.id }).then(msg => {
|
||||
@ -284,6 +289,37 @@ class PDFPageView {
|
||||
}
|
||||
}
|
||||
|
||||
async #renderTextLayer() {
|
||||
const { pdfPage, textLayer, viewport } = this;
|
||||
if (!textLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
if (!textLayer.renderingDone) {
|
||||
const readableStream = pdfPage.streamTextContent({
|
||||
includeMarkedContent: true,
|
||||
});
|
||||
textLayer.setTextContentStream(readableStream);
|
||||
}
|
||||
await textLayer.render(viewport);
|
||||
} catch (ex) {
|
||||
if (ex instanceof AbortException) {
|
||||
return;
|
||||
}
|
||||
console.error(`#renderTextLayer: "${ex}".`);
|
||||
error = ex;
|
||||
}
|
||||
|
||||
this.eventBus.dispatch("textlayerrendered", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
numTextDivs: textLayer.numTextDivs,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
async _buildXfaTextContentItems(textDivs) {
|
||||
const text = await this.pdfPage.getTextContent();
|
||||
const items = [];
|
||||
@ -320,17 +356,19 @@ class PDFPageView {
|
||||
keepAnnotationLayer = false,
|
||||
keepAnnotationEditorLayer = false,
|
||||
keepXfaLayer = false,
|
||||
keepTextLayer = false,
|
||||
} = {}) {
|
||||
this.cancelRendering({
|
||||
keepAnnotationLayer,
|
||||
keepAnnotationEditorLayer,
|
||||
keepXfaLayer,
|
||||
keepTextLayer,
|
||||
});
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
|
||||
const div = this.div;
|
||||
div.style.width = Math.floor(this.viewport.width) + "px";
|
||||
div.style.height = Math.floor(this.viewport.height) + "px";
|
||||
div.style.width = Math.round(this.viewport.width) + "px";
|
||||
div.style.height = Math.round(this.viewport.height) + "px";
|
||||
|
||||
const childNodes = div.childNodes,
|
||||
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
|
||||
@ -338,7 +376,8 @@ class PDFPageView {
|
||||
(keepAnnotationLayer && this.annotationLayer?.div) || null,
|
||||
annotationEditorLayerNode =
|
||||
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
|
||||
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
|
||||
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null,
|
||||
textLayerNode = (keepTextLayer && this.textLayer?.div) || null;
|
||||
for (let i = childNodes.length - 1; i >= 0; i--) {
|
||||
const node = childNodes[i];
|
||||
switch (node) {
|
||||
@ -346,6 +385,7 @@ class PDFPageView {
|
||||
case annotationLayerNode:
|
||||
case annotationEditorLayerNode:
|
||||
case xfaLayerNode:
|
||||
case textLayerNode:
|
||||
continue;
|
||||
}
|
||||
node.remove();
|
||||
@ -369,6 +409,10 @@ class PDFPageView {
|
||||
this.xfaLayer.hide();
|
||||
}
|
||||
|
||||
if (textLayerNode) {
|
||||
this.textLayer.hide();
|
||||
}
|
||||
|
||||
if (!zoomLayerNode) {
|
||||
if (this.canvas) {
|
||||
this.paintedViewportMap.delete(this.canvas);
|
||||
@ -450,6 +494,7 @@ class PDFPageView {
|
||||
redrawAnnotationLayer: true,
|
||||
redrawAnnotationEditorLayer: true,
|
||||
redrawXfaLayer: true,
|
||||
redrawTextLayer: true,
|
||||
});
|
||||
|
||||
this.eventBus.dispatch("pagerendered", {
|
||||
@ -484,6 +529,7 @@ class PDFPageView {
|
||||
redrawAnnotationLayer: true,
|
||||
redrawAnnotationEditorLayer: true,
|
||||
redrawXfaLayer: true,
|
||||
redrawTextLayer: true,
|
||||
});
|
||||
|
||||
this.eventBus.dispatch("pagerendered", {
|
||||
@ -508,6 +554,7 @@ class PDFPageView {
|
||||
keepAnnotationLayer: true,
|
||||
keepAnnotationEditorLayer: true,
|
||||
keepXfaLayer: true,
|
||||
keepTextLayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -519,6 +566,7 @@ class PDFPageView {
|
||||
keepAnnotationLayer = false,
|
||||
keepAnnotationEditorLayer = false,
|
||||
keepXfaLayer = false,
|
||||
keepTextLayer = false,
|
||||
} = {}) {
|
||||
if (this.paintTask) {
|
||||
this.paintTask.cancel();
|
||||
@ -526,7 +574,7 @@ class PDFPageView {
|
||||
}
|
||||
this.resume = null;
|
||||
|
||||
if (this.textLayer) {
|
||||
if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
|
||||
this.textLayer.cancel();
|
||||
this.textLayer = null;
|
||||
}
|
||||
@ -561,6 +609,7 @@ class PDFPageView {
|
||||
redrawAnnotationLayer = false,
|
||||
redrawAnnotationEditorLayer = false,
|
||||
redrawXfaLayer = false,
|
||||
redrawTextLayer = false,
|
||||
}) {
|
||||
// Scale target (canvas or svg), its wrapper and page container.
|
||||
const width = this.viewport.width;
|
||||
@ -587,49 +636,6 @@ class PDFPageView {
|
||||
}
|
||||
target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`;
|
||||
|
||||
if (this.textLayer) {
|
||||
// Rotating the text layer is more complicated since the divs inside the
|
||||
// the text layer are rotated.
|
||||
// TODO: This could probably be simplified by drawing the text layer in
|
||||
// one orientation and then rotating overall.
|
||||
const textLayerViewport = this.textLayer.viewport;
|
||||
const textRelativeRotation =
|
||||
this.viewport.rotation - textLayerViewport.rotation;
|
||||
const textAbsRotation = Math.abs(textRelativeRotation);
|
||||
let scale = width / textLayerViewport.width;
|
||||
if (textAbsRotation === 90 || textAbsRotation === 270) {
|
||||
scale = width / textLayerViewport.height;
|
||||
}
|
||||
const textLayerDiv = this.textLayer.textLayerDiv;
|
||||
let transX, transY;
|
||||
switch (textAbsRotation) {
|
||||
case 0:
|
||||
transX = transY = 0;
|
||||
break;
|
||||
case 90:
|
||||
transX = 0;
|
||||
transY = "-" + textLayerDiv.style.height;
|
||||
break;
|
||||
case 180:
|
||||
transX = "-" + textLayerDiv.style.width;
|
||||
transY = "-" + textLayerDiv.style.height;
|
||||
break;
|
||||
case 270:
|
||||
transX = "-" + textLayerDiv.style.width;
|
||||
transY = 0;
|
||||
break;
|
||||
default:
|
||||
console.error("Bad rotation value.");
|
||||
break;
|
||||
}
|
||||
|
||||
textLayerDiv.style.transform =
|
||||
`rotate(${textAbsRotation}deg) ` +
|
||||
`scale(${scale}) ` +
|
||||
`translate(${transX}, ${transY})`;
|
||||
textLayerDiv.style.transformOrigin = "0% 0%";
|
||||
}
|
||||
|
||||
if (redrawAnnotationLayer && this.annotationLayer) {
|
||||
this._renderAnnotationLayer();
|
||||
}
|
||||
@ -639,6 +645,9 @@ class PDFPageView {
|
||||
if (redrawXfaLayer && this.xfaLayer) {
|
||||
this._renderXfaLayer();
|
||||
}
|
||||
if (redrawTextLayer && this.textLayer) {
|
||||
this.#renderTextLayer();
|
||||
}
|
||||
}
|
||||
|
||||
get width() {
|
||||
@ -686,40 +695,33 @@ class PDFPageView {
|
||||
canvasWrapper.style.height = div.style.height;
|
||||
canvasWrapper.classList.add("canvasWrapper");
|
||||
|
||||
const lastDivBeforeTextDiv =
|
||||
this.annotationLayer?.div || this.annotationEditorLayer?.div;
|
||||
|
||||
if (lastDivBeforeTextDiv) {
|
||||
// The annotation layer needs to stay on top.
|
||||
lastDivBeforeTextDiv.before(canvasWrapper);
|
||||
if (this.textLayer) {
|
||||
this.textLayer.div.before(canvasWrapper);
|
||||
} else {
|
||||
div.append(canvasWrapper);
|
||||
}
|
||||
|
||||
let textLayer = null;
|
||||
if (this.textLayerMode !== TextLayerMode.DISABLE && this.textLayerFactory) {
|
||||
this._accessibilityManager ||= new TextAccessibilityManager();
|
||||
const textLayerDiv = document.createElement("div");
|
||||
textLayerDiv.className = "textLayer";
|
||||
textLayerDiv.style.width = canvasWrapper.style.width;
|
||||
textLayerDiv.style.height = canvasWrapper.style.height;
|
||||
const lastDivBeforeTextDiv =
|
||||
this.annotationLayer?.div || this.annotationEditorLayer?.div;
|
||||
if (lastDivBeforeTextDiv) {
|
||||
// The annotation layer needs to stay on top.
|
||||
lastDivBeforeTextDiv.before(textLayerDiv);
|
||||
lastDivBeforeTextDiv.before(canvasWrapper);
|
||||
} else {
|
||||
div.append(textLayerDiv);
|
||||
div.append(canvasWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
textLayer = this.textLayerFactory.createTextLayerBuilder({
|
||||
textLayerDiv,
|
||||
pageIndex: this.id - 1,
|
||||
viewport: this.viewport,
|
||||
eventBus: this.eventBus,
|
||||
if (
|
||||
!this.textLayer &&
|
||||
this.textLayerMode !== TextLayerMode.DISABLE &&
|
||||
this.textLayerFactory
|
||||
) {
|
||||
this._accessibilityManager ||= new TextAccessibilityManager();
|
||||
|
||||
this.textLayer = this.textLayerFactory.createTextLayerBuilder({
|
||||
highlighter: this.textHighlighter,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
||||
});
|
||||
canvasWrapper.after(this.textLayer.div);
|
||||
}
|
||||
this.textLayer = textLayer;
|
||||
|
||||
if (
|
||||
this.#annotationMode !== AnnotationMode.DISABLE &&
|
||||
@ -809,13 +811,7 @@ class PDFPageView {
|
||||
const resultPromise = paintTask.promise.then(
|
||||
() => {
|
||||
return finishPaintTask(null).then(() => {
|
||||
if (textLayer) {
|
||||
const readableStream = pdfPage.streamTextContent({
|
||||
includeMarkedContent: true,
|
||||
});
|
||||
textLayer.setTextContentStream(readableStream);
|
||||
textLayer.render();
|
||||
}
|
||||
this.#renderTextLayer();
|
||||
|
||||
if (this.annotationLayer) {
|
||||
this._renderAnnotationLayer().then(() => {
|
||||
@ -949,10 +945,12 @@ class PDFPageView {
|
||||
|
||||
const sfx = approximateFraction(outputScale.sx);
|
||||
const sfy = approximateFraction(outputScale.sy);
|
||||
|
||||
canvas.width = roundToDivide(viewport.width * outputScale.sx, sfx[0]);
|
||||
canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]);
|
||||
canvas.style.width = roundToDivide(viewport.width, sfx[1]) + "px";
|
||||
canvas.style.height = roundToDivide(viewport.height, sfy[1]) + "px";
|
||||
const { style } = canvas;
|
||||
style.width = roundToDivide(viewport.width, sfx[1]) + "px";
|
||||
style.height = roundToDivide(viewport.height, sfy[1]) + "px";
|
||||
|
||||
// Add the viewport so it's known what it was originally drawn with.
|
||||
this.paintedViewportMap.set(canvas, viewport);
|
||||
|
@ -128,6 +128,8 @@ function isValidAnnotationEditorMode(mode) {
|
||||
* landscape pages upon printing. The default is `false`.
|
||||
* @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
|
||||
* value is `false`.
|
||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
||||
* OffscreenCanvas if needed.
|
||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||
* total pixels, i.e. width * height. Use -1 for no limit. The default value
|
||||
* is 4096 * 4096 (16 mega-pixels).
|
||||
@ -287,6 +289,8 @@ class PDFViewer {
|
||||
this.renderer = options.renderer || RendererType.CANVAS;
|
||||
}
|
||||
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
|
||||
this.isOffscreenCanvasSupported =
|
||||
options.isOffscreenCanvasSupported ?? true;
|
||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||
this.l10n = options.l10n || NullL10n;
|
||||
this.#enablePermissions = options.enablePermissions || false;
|
||||
@ -775,6 +779,7 @@ class PDFViewer {
|
||||
? this.renderer
|
||||
: null,
|
||||
useOnlyCssZoom: this.useOnlyCssZoom,
|
||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
||||
maxCanvasPixels: this.maxCanvasPixels,
|
||||
pageColors: this.pageColors,
|
||||
l10n: this.l10n,
|
||||
@ -1635,12 +1640,9 @@ class PDFViewer {
|
||||
|
||||
/**
|
||||
* @typedef {Object} CreateTextLayerBuilderParameters
|
||||
* @property {HTMLDivElement} textLayerDiv
|
||||
* @property {number} pageIndex
|
||||
* @property {PageViewport} viewport
|
||||
* @property {EventBus} eventBus
|
||||
* @property {TextHighlighter} highlighter
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {boolean} [isOffscreenCanvasSupported]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -1648,20 +1650,14 @@ class PDFViewer {
|
||||
* @returns {TextLayerBuilder}
|
||||
*/
|
||||
createTextLayerBuilder({
|
||||
textLayerDiv,
|
||||
pageIndex,
|
||||
viewport,
|
||||
eventBus,
|
||||
highlighter,
|
||||
accessibilityManager = null,
|
||||
isOffscreenCanvasSupported = true,
|
||||
}) {
|
||||
return new TextLayerBuilder({
|
||||
textLayerDiv,
|
||||
eventBus,
|
||||
pageIndex,
|
||||
viewport,
|
||||
highlighter,
|
||||
accessibilityManager,
|
||||
isOffscreenCanvasSupported,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,7 @@ class TextHighlighter {
|
||||
);
|
||||
this._onUpdateTextLayerMatches = null;
|
||||
}
|
||||
this._updateMatches(/* reset = */ true);
|
||||
}
|
||||
|
||||
_convertMatches(matches, matchesLength) {
|
||||
@ -264,8 +265,8 @@ class TextHighlighter {
|
||||
}
|
||||
}
|
||||
|
||||
_updateMatches() {
|
||||
if (!this.enabled) {
|
||||
_updateMatches(reset = false) {
|
||||
if (!this.enabled && !reset) {
|
||||
return;
|
||||
}
|
||||
const { findController, matches, pageIdx } = this;
|
||||
@ -283,7 +284,7 @@ class TextHighlighter {
|
||||
clearedUntilDivIdx = match.end.divIdx + 1;
|
||||
}
|
||||
|
||||
if (!findController?.highlightMatches) {
|
||||
if (!findController?.highlightMatches || reset) {
|
||||
return;
|
||||
}
|
||||
// Convert the matches on the `findController` into the match format
|
||||
|
@ -25,6 +25,7 @@
|
||||
line-height: 1;
|
||||
text-size-adjust: none;
|
||||
forced-color-adjust: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.textLayer span,
|
||||
|
@ -15,22 +15,19 @@
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
|
||||
import { renderTextLayer } from "pdfjs-lib";
|
||||
import { renderTextLayer, updateTextLayer } from "pdfjs-lib";
|
||||
|
||||
/**
|
||||
* @typedef {Object} TextLayerBuilderOptions
|
||||
* @property {HTMLDivElement} textLayerDiv - The text layer container.
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {number} pageIndex - The page index.
|
||||
* @property {PageViewport} viewport - The viewport of the text layer.
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -39,28 +36,28 @@ import { renderTextLayer } from "pdfjs-lib";
|
||||
* contain text that matches the PDF text they are overlaying.
|
||||
*/
|
||||
class TextLayerBuilder {
|
||||
#scale = 0;
|
||||
|
||||
#rotation = 0;
|
||||
|
||||
constructor({
|
||||
textLayerDiv,
|
||||
eventBus,
|
||||
pageIndex,
|
||||
viewport,
|
||||
highlighter = null,
|
||||
accessibilityManager = null,
|
||||
isOffscreenCanvasSupported = true,
|
||||
}) {
|
||||
this.textLayerDiv = textLayerDiv;
|
||||
this.eventBus = eventBus;
|
||||
this.textContent = null;
|
||||
this.textContentItemsStr = [];
|
||||
this.textContentStream = null;
|
||||
this.renderingDone = false;
|
||||
this.pageNumber = pageIndex + 1;
|
||||
this.viewport = viewport;
|
||||
this.textDivs = [];
|
||||
this.textDivProperties = new WeakMap();
|
||||
this.textLayerRenderTask = null;
|
||||
this.highlighter = highlighter;
|
||||
this.accessibilityManager = accessibilityManager;
|
||||
this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
|
||||
|
||||
this.#bindMouse();
|
||||
this.div = document.createElement("div");
|
||||
this.div.className = "textLayer";
|
||||
}
|
||||
|
||||
#finishRendering() {
|
||||
@ -68,48 +65,80 @@ class TextLayerBuilder {
|
||||
|
||||
const endOfContent = document.createElement("div");
|
||||
endOfContent.className = "endOfContent";
|
||||
this.textLayerDiv.append(endOfContent);
|
||||
this.div.append(endOfContent);
|
||||
|
||||
this.eventBus.dispatch("textlayerrendered", {
|
||||
source: this,
|
||||
pageNumber: this.pageNumber,
|
||||
numTextDivs: this.textDivs.length,
|
||||
});
|
||||
this.#bindMouse();
|
||||
}
|
||||
|
||||
get numTextDivs() {
|
||||
return this.textDivs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the text layer.
|
||||
*/
|
||||
render() {
|
||||
if (!(this.textContent || this.textContentStream) || this.renderingDone) {
|
||||
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.textDivs.length = 0;
|
||||
this.cancel();
|
||||
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
|
||||
this.accessibilityManager?.setTextMapping(this.textDivs);
|
||||
|
||||
const textLayerFrag = document.createDocumentFragment();
|
||||
this.textLayerRenderTask = renderTextLayer({
|
||||
textContent: this.textContent,
|
||||
textContentStream: this.textContentStream,
|
||||
container: textLayerFrag,
|
||||
viewport: this.viewport,
|
||||
container: this.div,
|
||||
viewport,
|
||||
textDivs: this.textDivs,
|
||||
textDivProperties: this.textDivProperties,
|
||||
textContentItemsStr: this.textContentItemsStr,
|
||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
||||
});
|
||||
this.textLayerRenderTask.promise.then(
|
||||
() => {
|
||||
this.textLayerDiv.append(textLayerFrag);
|
||||
this.#finishRendering();
|
||||
this.highlighter?.enable();
|
||||
this.accessibilityManager?.enable();
|
||||
},
|
||||
function (reason) {
|
||||
// Cancelled or failed to render text layer; skipping errors.
|
||||
}
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,6 +151,9 @@ class TextLayerBuilder {
|
||||
}
|
||||
this.highlighter?.disable();
|
||||
this.accessibilityManager?.disable();
|
||||
this.textContentItemsStr.length = 0;
|
||||
this.textDivs.length = 0;
|
||||
this.textDivProperties = new WeakMap();
|
||||
}
|
||||
|
||||
setTextContentStream(readableStream) {
|
||||
@ -140,7 +172,7 @@ class TextLayerBuilder {
|
||||
* dragged up or down.
|
||||
*/
|
||||
#bindMouse() {
|
||||
const div = this.textLayerDiv;
|
||||
const { div } = this;
|
||||
|
||||
div.addEventListener("mousedown", evt => {
|
||||
const end = div.querySelector(".endOfContent");
|
||||
|
Loading…
Reference in New Issue
Block a user