1
0
Fork 0
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:
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

@ -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,

View file

@ -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,
});
}
}

View file

@ -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,
}) {}
}

View file

@ -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);

View file

@ -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,
});
}

View file

@ -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

View file

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

View file

@ -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");