mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
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
13 changed files with 362 additions and 240 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue