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:
Calixte Denizet 2022-11-21 17:15:39 +01:00
parent fa54a58790
commit eed9bf71c5
13 changed files with 362 additions and 240 deletions

View File

@ -16,6 +16,7 @@
import { import {
AbortException, AbortException,
createPromiseCapability, createPromiseCapability,
FeatureTest,
Util, Util,
} from "../shared/util.js"; } from "../shared/util.js";
@ -27,16 +28,40 @@ import {
* render (the object is returned by the page's `getTextContent` method). * render (the object is returned by the page's `getTextContent` method).
* @property {ReadableStream} [textContentStream] - Text content stream to * @property {ReadableStream} [textContentStream] - Text content stream to
* render (the stream is returned by the page's `streamTextContent` method). * render (the stream is returned by the page's `streamTextContent` method).
* @property {DocumentFragment | HTMLElement} container - The DOM node that * @property {HTMLElement} container - The DOM node that will contain the text
* will contain the text runs. * runs.
* @property {import("./display_utils").PageViewport} viewport - The target * @property {import("./display_utils").PageViewport} viewport - The target
* viewport to properly layout the text runs. * viewport to properly layout the text runs.
* @property {Array<HTMLElement>} [textDivs] - HTML elements that correspond to * @property {Array<HTMLElement>} [textDivs] - HTML elements that correspond to
* the text items of the textContent input. * the text items of the textContent input.
* This is output and shall initially be set to an empty array. * 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 * @property {Array<string>} [textContentItemsStr] - Strings that correspond to
* the `str` property of the text items of the textContent input. * the `str` property of the text items of the textContent input.
* This is output and shall initially be set to an empty array. * 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; const MAX_TEXT_DIVS_TO_RENDER = 100000;
@ -44,13 +69,27 @@ const DEFAULT_FONT_SIZE = 30;
const DEFAULT_FONT_ASCENT = 0.8; const DEFAULT_FONT_ASCENT = 0.8;
const ascentCache = new Map(); 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); const cachedAscent = ascentCache.get(fontFamily);
if (cachedAscent) { if (cachedAscent) {
return cachedAscent; return cachedAscent;
} }
ctx.save(); const ctx = getCtx(DEFAULT_FONT_SIZE, isOffscreenCanvasSupported);
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`; ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
const metrics = ctx.measureText(""); const metrics = ctx.measureText("");
@ -58,9 +97,10 @@ function getAscent(fontFamily, ctx) {
let ascent = metrics.fontBoundingBoxAscent; let ascent = metrics.fontBoundingBoxAscent;
let descent = Math.abs(metrics.fontBoundingBoxDescent); let descent = Math.abs(metrics.fontBoundingBoxDescent);
if (ascent) { if (ascent) {
ctx.restore();
const ratio = ascent / (ascent + descent); const ratio = ascent / (ascent + descent);
ascentCache.set(fontFamily, ratio); ascentCache.set(fontFamily, ratio);
ctx.canvas.width = ctx.canvas.height = 0;
return ratio; return ratio;
} }
@ -99,7 +139,7 @@ function getAscent(fontFamily, ctx) {
} }
} }
ctx.restore(); ctx.canvas.width = ctx.canvas.height = 0;
if (ascent) { if (ascent) {
const ratio = ascent / (ascent + descent); const ratio = ascent / (ascent + descent);
@ -111,7 +151,7 @@ function getAscent(fontFamily, ctx) {
return DEFAULT_FONT_ASCENT; return DEFAULT_FONT_ASCENT;
} }
function appendText(task, geom, styles, ctx) { function appendText(task, geom, styles) {
// Initialize all used properties to keep the caches monomorphic. // Initialize all used properties to keep the caches monomorphic.
const textDiv = document.createElement("span"); const textDiv = document.createElement("span");
const textDivProperties = { const textDivProperties = {
@ -123,14 +163,15 @@ function appendText(task, geom, styles, ctx) {
}; };
task._textDivs.push(textDiv); 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]); let angle = Math.atan2(tx[1], tx[0]);
const style = styles[geom.fontName]; const style = styles[geom.fontName];
if (style.vertical) { if (style.vertical) {
angle += Math.PI / 2; angle += Math.PI / 2;
} }
const fontHeight = Math.hypot(tx[2], tx[3]); 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; let left, top;
if (angle === 0) { if (angle === 0) {
@ -140,12 +181,21 @@ function appendText(task, geom, styles, ctx) {
left = tx[4] + fontAscent * Math.sin(angle); left = tx[4] + fontAscent * Math.sin(angle);
top = tx[5] - fontAscent * Math.cos(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, // Setting the style properties individually, rather than all at once,
// should be OK since the `textDiv` isn't appended to the document yet. // should be OK since the `textDiv` isn't appended to the document yet.
textDiv.style.left = `${left}px`; if (task._container === task._rootContainer) {
textDiv.style.top = `${top}px`; divStyle.left = `${((100 * left) / task._pageWidth).toFixed(2)}%`;
textDiv.style.fontSize = `${fontHeight}px`; divStyle.top = `${((100 * top) / task._pageHeight).toFixed(2)}%`;
textDiv.style.fontFamily = style.fontFamily; } 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; textDivProperties.fontSize = fontHeight;
@ -183,11 +233,7 @@ function appendText(task, geom, styles, ctx) {
} }
} }
if (shouldScaleText) { if (shouldScaleText) {
if (style.vertical) { textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width;
textDivProperties.canvasWidth = geom.height * task._viewport.scale;
} else {
textDivProperties.canvasWidth = geom.width * task._viewport.scale;
}
} }
task._textDivProperties.set(textDiv, textDivProperties); task._textDivProperties.set(textDiv, textDivProperties);
if (task._textContentStream) { 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) { function render(task) {
if (task._canceled) { if (task._canceled) {
return; return;
@ -228,40 +303,41 @@ class TextLayerRenderTask {
container, container,
viewport, viewport,
textDivs, textDivs,
textDivProperties,
textContentItemsStr, textContentItemsStr,
isOffscreenCanvasSupported,
}) { }) {
this._textContent = textContent; this._textContent = textContent;
this._textContentStream = textContentStream; this._textContentStream = textContentStream;
this._container = container; this._container = this._rootContainer = container;
this._document = container.ownerDocument;
this._viewport = viewport;
this._textDivs = textDivs || []; this._textDivs = textDivs || [];
this._textContentItemsStr = textContentItemsStr || []; this._textContentItemsStr = textContentItemsStr || [];
this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled; this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
this._reader = null; this._reader = null;
this._layoutTextLastFontSize = null; this._textDivProperties = textDivProperties || new WeakMap();
this._layoutTextLastFontFamily = null;
this._layoutTextCtx = null;
this._textDivProperties = new WeakMap();
this._renderingDone = false; this._renderingDone = false;
this._canceled = false; this._canceled = false;
this._capability = createPromiseCapability(); 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. // Always clean-up the temporary canvas once rendering is no longer pending.
this._capability.promise this._capability.promise
.finally(() => { .finally(() => {
// The `textDiv` properties are no longer needed. this._layoutTextParams = null;
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;
}
}) })
.catch(() => { .catch(() => {
// Avoid "Uncaught promise" messages in the console. // Avoid "Uncaught promise" messages in the console.
@ -289,7 +365,7 @@ class TextLayerRenderTask {
}); });
this._reader = null; 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; continue;
} }
this._textContentItemsStr.push(item.str); this._textContentItemsStr.push(item.str);
appendText(this, item, styleCache, this._layoutTextCtx); appendText(this, item, styleCache);
} }
} }
@ -323,39 +399,10 @@ class TextLayerRenderTask {
* @private * @private
*/ */
_layoutText(textDiv) { _layoutText(textDiv) {
const textDivProperties = this._textDivProperties.get(textDiv); const textDivProperties = (this._layoutTextParams.properties =
this._textDivProperties.get(textDiv));
let transform = ""; this._layoutTextParams.div = textDiv;
if (textDivProperties.canvasWidth !== 0 && textDivProperties.hasText) { layout(this._layoutTextParams);
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;
}
if (textDivProperties.hasText) { if (textDivProperties.hasText) {
this._container.append(textDiv); this._container.append(textDiv);
@ -375,10 +422,6 @@ class TextLayerRenderTask {
let styleCache = Object.create(null); let styleCache = Object.create(null);
// The temporary canvas is used to measure text length in the DOM. // 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) { if (this._textContent) {
const textItems = this._textContent.items; const textItems = this._textContent.items;
@ -426,9 +469,67 @@ function renderTextLayer(renderParameters) {
viewport: renderParameters.viewport, viewport: renderParameters.viewport,
textDivs: renderParameters.textDivs, textDivs: renderParameters.textDivs,
textContentItemsStr: renderParameters.textContentItemsStr, textContentItemsStr: renderParameters.textContentItemsStr,
textDivProperties: renderParameters.textDivProperties,
isOffscreenCanvasSupported: renderParameters.isOffscreenCanvasSupported,
}); });
task._render(); task._render();
return task; 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 };

View File

@ -23,6 +23,7 @@
/** @typedef {import("./display/text_layer").TextLayerRenderTask} TextLayerRenderTask */ /** @typedef {import("./display/text_layer").TextLayerRenderTask} TextLayerRenderTask */
import { import {
AbortException,
AnnotationEditorParamsType, AnnotationEditorParamsType,
AnnotationEditorType, AnnotationEditorType,
AnnotationMode, AnnotationMode,
@ -60,12 +61,12 @@ import {
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
} from "./display/display_utils.js"; } from "./display/display_utils.js";
import { renderTextLayer, updateTextLayer } from "./display/text_layer.js";
import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js";
import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationEditorUIManager } from "./display/editor/tools.js";
import { AnnotationLayer } from "./display/annotation_layer.js"; import { AnnotationLayer } from "./display/annotation_layer.js";
import { GlobalWorkerOptions } from "./display/worker_options.js"; import { GlobalWorkerOptions } from "./display/worker_options.js";
import { isNodeJS } from "./shared/is_node.js"; import { isNodeJS } from "./shared/is_node.js";
import { renderTextLayer } from "./display/text_layer.js";
import { SVGGraphics } from "./display/svg.js"; import { SVGGraphics } from "./display/svg.js";
import { XfaLayer } from "./display/xfa_layer.js"; import { XfaLayer } from "./display/xfa_layer.js";
@ -110,6 +111,7 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
} }
export { export {
AbortException,
AnnotationEditorLayer, AnnotationEditorLayer,
AnnotationEditorParamsType, AnnotationEditorParamsType,
AnnotationEditorType, AnnotationEditorType,
@ -143,6 +145,7 @@ export {
SVGGraphics, SVGGraphics,
UnexpectedResponseException, UnexpectedResponseException,
UNSUPPORTED_FEATURES, UNSUPPORTED_FEATURES,
updateTextLayer,
Util, Util,
VerbosityLevel, VerbosityLevel,
version, version,

View File

@ -168,7 +168,7 @@ class Rasterize {
} }
static get textStylePromise() { static get textStylePromise() {
const styles = ["./text_layer_test.css"]; const styles = [VIEWER_CSS, "./text_layer_test.css"];
return shadow(this, "textStylePromise", loadStyles(styles)); return shadow(this, "textStylePromise", loadStyles(styles));
} }
@ -256,8 +256,10 @@ class Rasterize {
// Items are transformed to have 1px font size. // Items are transformed to have 1px font size.
svg.setAttribute("font-size", 1); svg.setAttribute("font-size", 1);
const [overrides] = await this.textStylePromise; const [common, overrides] = await this.textStylePromise;
style.textContent = overrides; style.textContent =
`${common}\n${overrides}\n` +
`:root { --scale-factor: ${viewport.scale} }`;
// Rendering text layer as HTML. // Rendering text layer as HTML.
const task = renderTextLayer({ const task = renderTextLayer({
@ -265,6 +267,7 @@ class Rasterize {
container: div, container: div,
viewport, viewport,
}); });
await task.promise; await task.promise;
svg.append(foreignObject); svg.append(foreignObject);

View File

@ -22,9 +22,11 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
line-height: 1; line-height: 1;
opacity: 1;
} }
.textLayer span, .textLayer span,
.textLayer br { .textLayer br {
color: black;
position: absolute; position: absolute;
white-space: pre; white-space: pre;
transform-origin: 0% 0%; transform-origin: 0% 0%;

View File

@ -24,7 +24,7 @@ import { isNodeJS } from "../../src/shared/is_node.js";
describe("textLayer", function () { describe("textLayer", function () {
it("creates textLayer from ReadableStream", async function () { it("creates textLayer from ReadableStream", async function () {
if (isNodeJS) { 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 loadingTask = getDocument(buildGetDocumentParams("basicapi.pdf"));
const pdfDocument = await loadingTask.promise; const pdfDocument = await loadingTask.promise;
@ -34,7 +34,7 @@ describe("textLayer", function () {
const textLayerRenderTask = renderTextLayer({ const textLayerRenderTask = renderTextLayer({
textContentStream: page.streamTextContent(), textContentStream: page.streamTextContent(),
container: document.createDocumentFragment(), container: document.createElement("div"),
viewport: page.getViewport(), viewport: page.getViewport(),
textContentItemsStr, textContentItemsStr,
}); });

View File

@ -501,6 +501,7 @@ const PDFViewerApplication = {
imageResourcesPath: AppOptions.get("imageResourcesPath"), imageResourcesPath: AppOptions.get("imageResourcesPath"),
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
isOffscreenCanvasSupported: AppOptions.get("isOffscreenCanvasSupported"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
enablePermissions: AppOptions.get("enablePermissions"), enablePermissions: AppOptions.get("enablePermissions"),
pageColors, pageColors,

View File

@ -165,12 +165,9 @@ class DefaultStructTreeLayerFactory {
class DefaultTextLayerFactory { class DefaultTextLayerFactory {
/** /**
* @typedef {Object} CreateTextLayerBuilderParameters * @typedef {Object} CreateTextLayerBuilderParameters
* @property {HTMLDivElement} textLayerDiv
* @property {number} pageIndex
* @property {PageViewport} viewport
* @property {EventBus} eventBus
* @property {TextHighlighter} highlighter * @property {TextHighlighter} highlighter
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {boolean} [isOffscreenCanvasSupported]
*/ */
/** /**
@ -178,20 +175,14 @@ class DefaultTextLayerFactory {
* @returns {TextLayerBuilder} * @returns {TextLayerBuilder}
*/ */
createTextLayerBuilder({ createTextLayerBuilder({
textLayerDiv,
pageIndex,
viewport,
eventBus,
highlighter, highlighter,
accessibilityManager = null, accessibilityManager = null,
isOffscreenCanvasSupported = true,
}) { }) {
return new TextLayerBuilder({ return new TextLayerBuilder({
textLayerDiv,
pageIndex,
viewport,
eventBus,
highlighter, highlighter,
accessibilityManager, accessibilityManager,
isOffscreenCanvasSupported,
}); });
} }
} }

View File

@ -21,7 +21,6 @@
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */ /** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */ /** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */
/** @typedef {import("./event_utils").EventBus} EventBus */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */ /** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */ /** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
@ -168,12 +167,9 @@ class IRenderableView {
class IPDFTextLayerFactory { class IPDFTextLayerFactory {
/** /**
* @typedef {Object} CreateTextLayerBuilderParameters * @typedef {Object} CreateTextLayerBuilderParameters
* @property {HTMLDivElement} textLayerDiv
* @property {number} pageIndex
* @property {PageViewport} viewport
* @property {EventBus} eventBus
* @property {TextHighlighter} highlighter * @property {TextHighlighter} highlighter
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {boolean} [isOffscreenCanvasSupported]
*/ */
/** /**
@ -181,12 +177,9 @@ class IPDFTextLayerFactory {
* @returns {TextLayerBuilder} * @returns {TextLayerBuilder}
*/ */
createTextLayerBuilder({ createTextLayerBuilder({
textLayerDiv,
pageIndex,
viewport,
eventBus,
highlighter, highlighter,
accessibilityManager, accessibilityManager,
isOffscreenCanvasSupported,
}) {} }) {}
} }

View File

@ -33,6 +33,7 @@
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
import { import {
AbortException,
AnnotationMode, AnnotationMode,
createPromiseCapability, createPromiseCapability,
PixelsPerInch, PixelsPerInch,
@ -82,6 +83,8 @@ import { TextAccessibilityManager } from "./text_accessibility.js";
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
* @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
* value is `false`. * value is `false`.
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
* OffscreenCanvas if needed.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use -1 for no limit. The default value * total pixels, i.e. width * height. Use -1 for no limit. The default value
* is 4096 * 4096 (16 mega-pixels). * is 4096 * 4096 (16 mega-pixels).
@ -128,6 +131,8 @@ class PDFPageView {
options.annotationMode ?? AnnotationMode.ENABLE_FORMS; options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
this.imageResourcesPath = options.imageResourcesPath || ""; this.imageResourcesPath = options.imageResourcesPath || "";
this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.useOnlyCssZoom = options.useOnlyCssZoom || false;
this.isOffscreenCanvasSupported =
options.isOffscreenCanvasSupported ?? true;
this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS; this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
this.pageColors = options.pageColors || null; this.pageColors = options.pageColors || null;
@ -174,8 +179,8 @@ class PDFPageView {
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "page"; div.className = "page";
div.style.width = Math.floor(this.viewport.width) + "px"; div.style.width = Math.round(this.viewport.width) + "px";
div.style.height = Math.floor(this.viewport.height) + "px"; div.style.height = Math.round(this.viewport.height) + "px";
div.setAttribute("data-page-number", this.id); div.setAttribute("data-page-number", this.id);
div.setAttribute("role", "region"); div.setAttribute("role", "region");
this.l10n.get("page_landmark", { page: this.id }).then(msg => { 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) { async _buildXfaTextContentItems(textDivs) {
const text = await this.pdfPage.getTextContent(); const text = await this.pdfPage.getTextContent();
const items = []; const items = [];
@ -320,17 +356,19 @@ class PDFPageView {
keepAnnotationLayer = false, keepAnnotationLayer = false,
keepAnnotationEditorLayer = false, keepAnnotationEditorLayer = false,
keepXfaLayer = false, keepXfaLayer = false,
keepTextLayer = false,
} = {}) { } = {}) {
this.cancelRendering({ this.cancelRendering({
keepAnnotationLayer, keepAnnotationLayer,
keepAnnotationEditorLayer, keepAnnotationEditorLayer,
keepXfaLayer, keepXfaLayer,
keepTextLayer,
}); });
this.renderingState = RenderingStates.INITIAL; this.renderingState = RenderingStates.INITIAL;
const div = this.div; const div = this.div;
div.style.width = Math.floor(this.viewport.width) + "px"; div.style.width = Math.round(this.viewport.width) + "px";
div.style.height = Math.floor(this.viewport.height) + "px"; div.style.height = Math.round(this.viewport.height) + "px";
const childNodes = div.childNodes, const childNodes = div.childNodes,
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null, zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
@ -338,7 +376,8 @@ class PDFPageView {
(keepAnnotationLayer && this.annotationLayer?.div) || null, (keepAnnotationLayer && this.annotationLayer?.div) || null,
annotationEditorLayerNode = annotationEditorLayerNode =
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null, (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--) { for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i]; const node = childNodes[i];
switch (node) { switch (node) {
@ -346,6 +385,7 @@ class PDFPageView {
case annotationLayerNode: case annotationLayerNode:
case annotationEditorLayerNode: case annotationEditorLayerNode:
case xfaLayerNode: case xfaLayerNode:
case textLayerNode:
continue; continue;
} }
node.remove(); node.remove();
@ -369,6 +409,10 @@ class PDFPageView {
this.xfaLayer.hide(); this.xfaLayer.hide();
} }
if (textLayerNode) {
this.textLayer.hide();
}
if (!zoomLayerNode) { if (!zoomLayerNode) {
if (this.canvas) { if (this.canvas) {
this.paintedViewportMap.delete(this.canvas); this.paintedViewportMap.delete(this.canvas);
@ -450,6 +494,7 @@ class PDFPageView {
redrawAnnotationLayer: true, redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true, redrawAnnotationEditorLayer: true,
redrawXfaLayer: true, redrawXfaLayer: true,
redrawTextLayer: true,
}); });
this.eventBus.dispatch("pagerendered", { this.eventBus.dispatch("pagerendered", {
@ -484,6 +529,7 @@ class PDFPageView {
redrawAnnotationLayer: true, redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true, redrawAnnotationEditorLayer: true,
redrawXfaLayer: true, redrawXfaLayer: true,
redrawTextLayer: true,
}); });
this.eventBus.dispatch("pagerendered", { this.eventBus.dispatch("pagerendered", {
@ -508,6 +554,7 @@ class PDFPageView {
keepAnnotationLayer: true, keepAnnotationLayer: true,
keepAnnotationEditorLayer: true, keepAnnotationEditorLayer: true,
keepXfaLayer: true, keepXfaLayer: true,
keepTextLayer: true,
}); });
} }
@ -519,6 +566,7 @@ class PDFPageView {
keepAnnotationLayer = false, keepAnnotationLayer = false,
keepAnnotationEditorLayer = false, keepAnnotationEditorLayer = false,
keepXfaLayer = false, keepXfaLayer = false,
keepTextLayer = false,
} = {}) { } = {}) {
if (this.paintTask) { if (this.paintTask) {
this.paintTask.cancel(); this.paintTask.cancel();
@ -526,7 +574,7 @@ class PDFPageView {
} }
this.resume = null; this.resume = null;
if (this.textLayer) { if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
this.textLayer.cancel(); this.textLayer.cancel();
this.textLayer = null; this.textLayer = null;
} }
@ -561,6 +609,7 @@ class PDFPageView {
redrawAnnotationLayer = false, redrawAnnotationLayer = false,
redrawAnnotationEditorLayer = false, redrawAnnotationEditorLayer = false,
redrawXfaLayer = false, redrawXfaLayer = false,
redrawTextLayer = false,
}) { }) {
// Scale target (canvas or svg), its wrapper and page container. // Scale target (canvas or svg), its wrapper and page container.
const width = this.viewport.width; const width = this.viewport.width;
@ -587,49 +636,6 @@ class PDFPageView {
} }
target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`; 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) { if (redrawAnnotationLayer && this.annotationLayer) {
this._renderAnnotationLayer(); this._renderAnnotationLayer();
} }
@ -639,6 +645,9 @@ class PDFPageView {
if (redrawXfaLayer && this.xfaLayer) { if (redrawXfaLayer && this.xfaLayer) {
this._renderXfaLayer(); this._renderXfaLayer();
} }
if (redrawTextLayer && this.textLayer) {
this.#renderTextLayer();
}
} }
get width() { get width() {
@ -686,40 +695,33 @@ class PDFPageView {
canvasWrapper.style.height = div.style.height; canvasWrapper.style.height = div.style.height;
canvasWrapper.classList.add("canvasWrapper"); canvasWrapper.classList.add("canvasWrapper");
const lastDivBeforeTextDiv = if (this.textLayer) {
this.annotationLayer?.div || this.annotationEditorLayer?.div; this.textLayer.div.before(canvasWrapper);
if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top.
lastDivBeforeTextDiv.before(canvasWrapper);
} else { } else {
div.append(canvasWrapper); const lastDivBeforeTextDiv =
} this.annotationLayer?.div || this.annotationEditorLayer?.div;
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;
if (lastDivBeforeTextDiv) { if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top. // The annotation layer needs to stay on top.
lastDivBeforeTextDiv.before(textLayerDiv); lastDivBeforeTextDiv.before(canvasWrapper);
} else { } else {
div.append(textLayerDiv); div.append(canvasWrapper);
} }
}
textLayer = this.textLayerFactory.createTextLayerBuilder({ if (
textLayerDiv, !this.textLayer &&
pageIndex: this.id - 1, this.textLayerMode !== TextLayerMode.DISABLE &&
viewport: this.viewport, this.textLayerFactory
eventBus: this.eventBus, ) {
this._accessibilityManager ||= new TextAccessibilityManager();
this.textLayer = this.textLayerFactory.createTextLayerBuilder({
highlighter: this.textHighlighter, highlighter: this.textHighlighter,
accessibilityManager: this._accessibilityManager, accessibilityManager: this._accessibilityManager,
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
}); });
canvasWrapper.after(this.textLayer.div);
} }
this.textLayer = textLayer;
if ( if (
this.#annotationMode !== AnnotationMode.DISABLE && this.#annotationMode !== AnnotationMode.DISABLE &&
@ -809,13 +811,7 @@ class PDFPageView {
const resultPromise = paintTask.promise.then( const resultPromise = paintTask.promise.then(
() => { () => {
return finishPaintTask(null).then(() => { return finishPaintTask(null).then(() => {
if (textLayer) { this.#renderTextLayer();
const readableStream = pdfPage.streamTextContent({
includeMarkedContent: true,
});
textLayer.setTextContentStream(readableStream);
textLayer.render();
}
if (this.annotationLayer) { if (this.annotationLayer) {
this._renderAnnotationLayer().then(() => { this._renderAnnotationLayer().then(() => {
@ -949,10 +945,12 @@ class PDFPageView {
const sfx = approximateFraction(outputScale.sx); const sfx = approximateFraction(outputScale.sx);
const sfy = approximateFraction(outputScale.sy); const sfy = approximateFraction(outputScale.sy);
canvas.width = roundToDivide(viewport.width * outputScale.sx, sfx[0]); canvas.width = roundToDivide(viewport.width * outputScale.sx, sfx[0]);
canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]); canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]);
canvas.style.width = roundToDivide(viewport.width, sfx[1]) + "px"; const { style } = canvas;
canvas.style.height = roundToDivide(viewport.height, sfy[1]) + "px"; 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. // Add the viewport so it's known what it was originally drawn with.
this.paintedViewportMap.set(canvas, viewport); this.paintedViewportMap.set(canvas, viewport);

View File

@ -128,6 +128,8 @@ function isValidAnnotationEditorMode(mode) {
* landscape pages upon printing. The default is `false`. * landscape pages upon printing. The default is `false`.
* @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
* value is `false`. * value is `false`.
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
* OffscreenCanvas if needed.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use -1 for no limit. The default value * total pixels, i.e. width * height. Use -1 for no limit. The default value
* is 4096 * 4096 (16 mega-pixels). * is 4096 * 4096 (16 mega-pixels).
@ -287,6 +289,8 @@ class PDFViewer {
this.renderer = options.renderer || RendererType.CANVAS; this.renderer = options.renderer || RendererType.CANVAS;
} }
this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.useOnlyCssZoom = options.useOnlyCssZoom || false;
this.isOffscreenCanvasSupported =
options.isOffscreenCanvasSupported ?? true;
this.maxCanvasPixels = options.maxCanvasPixels; this.maxCanvasPixels = options.maxCanvasPixels;
this.l10n = options.l10n || NullL10n; this.l10n = options.l10n || NullL10n;
this.#enablePermissions = options.enablePermissions || false; this.#enablePermissions = options.enablePermissions || false;
@ -775,6 +779,7 @@ class PDFViewer {
? this.renderer ? this.renderer
: null, : null,
useOnlyCssZoom: this.useOnlyCssZoom, useOnlyCssZoom: this.useOnlyCssZoom,
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
maxCanvasPixels: this.maxCanvasPixels, maxCanvasPixels: this.maxCanvasPixels,
pageColors: this.pageColors, pageColors: this.pageColors,
l10n: this.l10n, l10n: this.l10n,
@ -1635,12 +1640,9 @@ class PDFViewer {
/** /**
* @typedef {Object} CreateTextLayerBuilderParameters * @typedef {Object} CreateTextLayerBuilderParameters
* @property {HTMLDivElement} textLayerDiv
* @property {number} pageIndex
* @property {PageViewport} viewport
* @property {EventBus} eventBus
* @property {TextHighlighter} highlighter * @property {TextHighlighter} highlighter
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {boolean} [isOffscreenCanvasSupported]
*/ */
/** /**
@ -1648,20 +1650,14 @@ class PDFViewer {
* @returns {TextLayerBuilder} * @returns {TextLayerBuilder}
*/ */
createTextLayerBuilder({ createTextLayerBuilder({
textLayerDiv,
pageIndex,
viewport,
eventBus,
highlighter, highlighter,
accessibilityManager = null, accessibilityManager = null,
isOffscreenCanvasSupported = true,
}) { }) {
return new TextLayerBuilder({ return new TextLayerBuilder({
textLayerDiv,
eventBus,
pageIndex,
viewport,
highlighter, highlighter,
accessibilityManager, accessibilityManager,
isOffscreenCanvasSupported,
}); });
} }

View File

@ -95,6 +95,7 @@ class TextHighlighter {
); );
this._onUpdateTextLayerMatches = null; this._onUpdateTextLayerMatches = null;
} }
this._updateMatches(/* reset = */ true);
} }
_convertMatches(matches, matchesLength) { _convertMatches(matches, matchesLength) {
@ -264,8 +265,8 @@ class TextHighlighter {
} }
} }
_updateMatches() { _updateMatches(reset = false) {
if (!this.enabled) { if (!this.enabled && !reset) {
return; return;
} }
const { findController, matches, pageIdx } = this; const { findController, matches, pageIdx } = this;
@ -283,7 +284,7 @@ class TextHighlighter {
clearedUntilDivIdx = match.end.divIdx + 1; clearedUntilDivIdx = match.end.divIdx + 1;
} }
if (!findController?.highlightMatches) { if (!findController?.highlightMatches || reset) {
return; return;
} }
// Convert the matches on the `findController` into the match format // Convert the matches on the `findController` into the match format

View File

@ -25,6 +25,7 @@
line-height: 1; line-height: 1;
text-size-adjust: none; text-size-adjust: none;
forced-color-adjust: none; forced-color-adjust: none;
transform-origin: 0 0;
} }
.textLayer span, .textLayer span,

View File

@ -15,22 +15,19 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */ /** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
import { renderTextLayer } from "pdfjs-lib"; import { renderTextLayer, updateTextLayer } from "pdfjs-lib";
/** /**
* @typedef {Object} TextLayerBuilderOptions * @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 * @property {TextHighlighter} highlighter - Optional object that will handle
* highlighting text from the find controller. * highlighting text from the find controller.
* @property {TextAccessibilityManager} [accessibilityManager] * @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. * contain text that matches the PDF text they are overlaying.
*/ */
class TextLayerBuilder { class TextLayerBuilder {
#scale = 0;
#rotation = 0;
constructor({ constructor({
textLayerDiv,
eventBus,
pageIndex,
viewport,
highlighter = null, highlighter = null,
accessibilityManager = null, accessibilityManager = null,
isOffscreenCanvasSupported = true,
}) { }) {
this.textLayerDiv = textLayerDiv;
this.eventBus = eventBus;
this.textContent = null; this.textContent = null;
this.textContentItemsStr = []; this.textContentItemsStr = [];
this.textContentStream = null; this.textContentStream = null;
this.renderingDone = false; this.renderingDone = false;
this.pageNumber = pageIndex + 1;
this.viewport = viewport;
this.textDivs = []; this.textDivs = [];
this.textDivProperties = new WeakMap();
this.textLayerRenderTask = null; this.textLayerRenderTask = null;
this.highlighter = highlighter; this.highlighter = highlighter;
this.accessibilityManager = accessibilityManager; this.accessibilityManager = accessibilityManager;
this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
this.#bindMouse(); this.div = document.createElement("div");
this.div.className = "textLayer";
} }
#finishRendering() { #finishRendering() {
@ -68,48 +65,80 @@ class TextLayerBuilder {
const endOfContent = document.createElement("div"); const endOfContent = document.createElement("div");
endOfContent.className = "endOfContent"; endOfContent.className = "endOfContent";
this.textLayerDiv.append(endOfContent); this.div.append(endOfContent);
this.eventBus.dispatch("textlayerrendered", { this.#bindMouse();
source: this, }
pageNumber: this.pageNumber,
numTextDivs: this.textDivs.length, get numTextDivs() {
}); return this.textDivs.length;
} }
/** /**
* Renders the text layer. * Renders the text layer.
*/ */
render() { async render(viewport) {
if (!(this.textContent || this.textContentStream) || this.renderingDone) { 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; return;
} }
this.cancel();
this.textDivs.length = 0; this.cancel();
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr); this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
this.accessibilityManager?.setTextMapping(this.textDivs); this.accessibilityManager?.setTextMapping(this.textDivs);
const textLayerFrag = document.createDocumentFragment();
this.textLayerRenderTask = renderTextLayer({ this.textLayerRenderTask = renderTextLayer({
textContent: this.textContent, textContent: this.textContent,
textContentStream: this.textContentStream, textContentStream: this.textContentStream,
container: textLayerFrag, container: this.div,
viewport: this.viewport, viewport,
textDivs: this.textDivs, textDivs: this.textDivs,
textDivProperties: this.textDivProperties,
textContentItemsStr: this.textContentItemsStr, textContentItemsStr: this.textContentItemsStr,
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
}); });
this.textLayerRenderTask.promise.then(
() => { await this.textLayerRenderTask.promise;
this.textLayerDiv.append(textLayerFrag); this.#finishRendering();
this.#finishRendering(); this.#scale = scale;
this.highlighter?.enable(); this.accessibilityManager?.enable();
this.accessibilityManager?.enable(); this.show();
}, }
function (reason) {
// Cancelled or failed to render text layer; skipping errors. 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.highlighter?.disable();
this.accessibilityManager?.disable(); this.accessibilityManager?.disable();
this.textContentItemsStr.length = 0;
this.textDivs.length = 0;
this.textDivProperties = new WeakMap();
} }
setTextContentStream(readableStream) { setTextContentStream(readableStream) {
@ -140,7 +172,7 @@ class TextLayerBuilder {
* dragged up or down. * dragged up or down.
*/ */
#bindMouse() { #bindMouse() {
const div = this.textLayerDiv; const { div } = this;
div.addEventListener("mousedown", evt => { div.addEventListener("mousedown", evt => {
const end = div.querySelector(".endOfContent"); const end = div.querySelector(".endOfContent");