1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

Merge pull request #15722 from calixteman/refactor_textlayer

[api-minor] Refactor the text layer code in order to avoid to recompute it on each draw
This commit is contained in:
calixteman 2022-12-01 18:56:02 +01:00 committed by GitHub
commit 6e4968225e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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");