From f3038406b1c2685a09d300ec3f18a2b89c642b22 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 4 Dec 2024 19:34:57 +0100 Subject: [PATCH] When zooming replace the css-zoomed canvas by the new one only when rendering is finished It fixes #18622. It avoids to recreate a canvasWrapper element in order minimize the DOM operations. --- test/integration/viewer_spec.mjs | 21 ++++- web/pdf_page_view.js | 152 ++++++++++++++----------------- web/pdf_viewer.css | 13 +-- 3 files changed, 90 insertions(+), 96 deletions(-) diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 4d14649d2..ac9af9569 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -276,9 +276,20 @@ describe("PDF viewer", () => { beforeEach(async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.evaluate(() => { - window.PDFViewerApplication.pdfViewer.currentScale = 0.5; - }); + const handle = await waitForPageRendered(page); + if ( + await page.evaluate(() => { + if ( + window.PDFViewerApplication.pdfViewer.currentScale !== 0.5 + ) { + window.PDFViewerApplication.pdfViewer.currentScale = 0.5; + return true; + } + return false; + }) + ) { + await awaitPromise(handle); + } }) ); }); @@ -317,12 +328,14 @@ describe("PDF viewer", () => { const originalCanvasSize = await getCanvasSize(page); const factor = 2; + const handle = await waitForPageRendered(page); await page.evaluate(scaleFactor => { window.PDFViewerApplication.pdfViewer.increaseScale({ drawingDelay: 0, scaleFactor, }); }, factor); + await awaitPromise(handle); const canvasSize = await getCanvasSize(page); @@ -343,12 +356,14 @@ describe("PDF viewer", () => { const originalCanvasSize = await getCanvasSize(page); const factor = 4; + const handle = await waitForPageRendered(page); await page.evaluate(scaleFactor => { window.PDFViewerApplication.pdfViewer.increaseScale({ drawingDelay: 0, scaleFactor, }); }, factor); + await awaitPromise(handle); const canvasSize = await getCanvasSize(page); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 010aa18ae..46ab525a0 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -116,6 +116,8 @@ const LAYERS_ORDER = new Map([ class PDFPageView { #annotationMode = AnnotationMode.ENABLE_FORMS; + #canvasWrapper = null; + #enableHWA = false; #hasRestrictedScaling = false; @@ -126,6 +128,8 @@ class PDFPageView { #loadingId = null; + #originalViewport = null; + #previousRotation = null; #scaleRoundX = 1; @@ -144,8 +148,6 @@ class PDFPageView { regularAnnotations: true, }; - #viewportMap = new WeakMap(); - #layers = [null, null, null, null]; /** @@ -195,7 +197,6 @@ class PDFPageView { this.annotationLayer = null; this.annotationEditorLayer = null; this.textLayer = null; - this.zoomLayer = null; this.xfaLayer = null; this.structTreeLayer = null; this.drawLayer = null; @@ -508,33 +509,23 @@ class PDFPageView { this._textHighlighter.enable(); } - /** - * @private - */ - _resetZoomLayer(removeFromDOM = false) { - if (!this.zoomLayer) { + #resetCanvas() { + const { canvas } = this; + if (!canvas) { return; } - const zoomLayerCanvas = this.zoomLayer.firstChild; - this.#viewportMap.delete(zoomLayerCanvas); - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - zoomLayerCanvas.width = 0; - zoomLayerCanvas.height = 0; - - if (removeFromDOM) { - // Note: `ChildNode.remove` doesn't throw if the parent node is undefined. - this.zoomLayer.remove(); - } - this.zoomLayer = null; + canvas.remove(); + canvas.width = canvas.height = 0; + this.canvas = null; + this.#originalViewport = null; } reset({ - keepZoomLayer = false, keepAnnotationLayer = false, keepAnnotationEditorLayer = false, keepXfaLayer = false, keepTextLayer = false, + keepCanvasWrapper = false, } = {}) { this.cancelRendering({ keepAnnotationLayer, @@ -547,21 +538,21 @@ class PDFPageView { const div = this.div; const childNodes = div.childNodes, - zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null, annotationLayerNode = (keepAnnotationLayer && this.annotationLayer?.div) || null, annotationEditorLayerNode = (keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null, xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null, - textLayerNode = (keepTextLayer && this.textLayer?.div) || null; + textLayerNode = (keepTextLayer && this.textLayer?.div) || null, + canvasWrapperNode = (keepCanvasWrapper && this.#canvasWrapper) || null; for (let i = childNodes.length - 1; i >= 0; i--) { const node = childNodes[i]; switch (node) { - case zoomLayerNode: case annotationLayerNode: case annotationEditorLayerNode: case xfaLayerNode: case textLayerNode: + case canvasWrapperNode: continue; } node.remove(); @@ -590,16 +581,9 @@ class PDFPageView { } this.structTreeLayer?.hide(); - if (!zoomLayerNode) { - if (this.canvas) { - this.#viewportMap.delete(this.canvas); - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - this.canvas.width = 0; - this.canvas.height = 0; - delete this.canvas; - } - this._resetZoomLayer(); + if (!keepCanvasWrapper && this.#canvasWrapper) { + this.#canvasWrapper = null; + this.#resetCanvas(); } } @@ -609,11 +593,11 @@ class PDFPageView { } this.#isEditing = isEditing; this.reset({ - keepZoomLayer: true, keepAnnotationLayer: true, keepAnnotationEditorLayer: true, keepXfaLayer: true, keepTextLayer: true, + keepCanvasWrapper: true, }); } @@ -697,7 +681,6 @@ class PDFPageView { this.renderingState !== RenderingStates.FINISHED ) { this.cancelRendering({ - keepZoomLayer: true, keepAnnotationLayer: true, keepAnnotationEditorLayer: true, keepXfaLayer: true, @@ -715,7 +698,6 @@ class PDFPageView { } this.cssTransform({ - target: this.canvas, redrawAnnotationLayer: true, redrawAnnotationEditorLayer: true, redrawXfaLayer: true, @@ -737,20 +719,14 @@ class PDFPageView { }); return; } - if (!this.zoomLayer && !this.canvas.hidden) { - this.zoomLayer = this.canvas.parentNode; - this.zoomLayer.style.position = "absolute"; - } - } - if (this.zoomLayer) { - this.cssTransform({ target: this.zoomLayer.firstChild }); } + this.cssTransform({}); this.reset({ - keepZoomLayer: true, keepAnnotationLayer: true, keepAnnotationEditorLayer: true, keepXfaLayer: true, keepTextLayer: true, + keepCanvasWrapper: true, }); } @@ -805,41 +781,32 @@ class PDFPageView { } cssTransform({ - target, redrawAnnotationLayer = false, redrawAnnotationEditorLayer = false, redrawXfaLayer = false, redrawTextLayer = false, hideTextLayer = false, }) { - // Scale target (canvas), its wrapper and page container. - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - !(target instanceof HTMLCanvasElement) - ) { - throw new Error("Expected `target` to be a canvas."); - } - if (!target.hasAttribute("zooming")) { - target.setAttribute("zooming", true); - const { style } = target; - style.width = style.height = ""; + const { canvas } = this; + if (!canvas) { + return; } - const originalViewport = this.#viewportMap.get(target); + const originalViewport = this.#originalViewport; if (this.viewport !== originalViewport) { // The canvas may have been originally rotated; rotate relative to that. const relativeRotation = - this.viewport.rotation - originalViewport.rotation; - const absRotation = Math.abs(relativeRotation); - let scaleX = 1, - scaleY = 1; - if (absRotation === 90 || absRotation === 270) { + (360 + this.viewport.rotation - originalViewport.rotation) % 360; + if (relativeRotation === 90 || relativeRotation === 270) { const { width, height } = this.viewport; // Scale x and y because of the rotation. - scaleX = height / width; - scaleY = width / height; + const scaleX = height / width; + const scaleY = width / height; + canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`; + } else { + canvas.style.transform = + relativeRotation === 0 ? "" : `rotate(${relativeRotation}deg)`; } - target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`; } if (redrawAnnotationLayer && this.annotationLayer) { @@ -892,7 +859,6 @@ class PDFPageView { this.#renderError = error; this.renderingState = RenderingStates.FINISHED; - this._resetZoomLayer(/* removeFromDOM = */ true); // Ensure that the thumbnails won't become partially (or fully) blank, // for documents that contain interactive form elements. @@ -927,9 +893,12 @@ class PDFPageView { // Wrap the canvas so that if it has a CSS transform for high DPI the // overflow will be hidden in Firefox. - const canvasWrapper = document.createElement("div"); - canvasWrapper.classList.add("canvasWrapper"); - this.#addLayer(canvasWrapper, "canvasWrapper"); + let canvasWrapper = this.#canvasWrapper; + if (!canvasWrapper) { + canvasWrapper = this.#canvasWrapper = document.createElement("div"); + canvasWrapper.classList.add("canvasWrapper"); + this.#addLayer(canvasWrapper, "canvasWrapper"); + } if ( !this.textLayer && @@ -1004,22 +973,37 @@ class PDFPageView { const canvas = document.createElement("canvas"); canvas.setAttribute("role", "presentation"); - // Keep the canvas hidden until the first draw callback, or until drawing - // is complete when `!this.renderingQueue`, to prevent black flickering. - canvas.hidden = true; const hasHCM = !!(pageColors?.background && pageColors?.foreground); + const prevCanvas = this.canvas; + const updateOnFirstShow = !prevCanvas && !hasHCM; + this.canvas = canvas; + this.#originalViewport = viewport; let showCanvas = isLastShow => { - // In HCM, a final filter is applied on the canvas which means that - // before it's applied we've normal colors. Consequently, to avoid to have - // a final flash we just display it once all the drawing is done. - if (!hasHCM || isLastShow) { - canvas.hidden = false; - showCanvas = null; // Only invoke the function once. + if (updateOnFirstShow) { + // Don't add the canvas until the first draw callback, or until + // drawing is complete when `!this.renderingQueue`, to prevent black + // flickering. + canvasWrapper.append(canvas); + showCanvas = null; + return; } + if (!isLastShow) { + return; + } + + if (prevCanvas) { + prevCanvas.replaceWith(canvas); + prevCanvas.width = prevCanvas.height = 0; + } else { + // In HCM, a final filter is applied on the canvas which means that + // before it's applied we've normal colors. Consequently, to avoid to + // have a final flash we just display it once all the drawing is done. + canvasWrapper.append(canvas); + } + + showCanvas = null; }; - canvasWrapper.append(canvas); - this.canvas = canvas; const ctx = canvas.getContext("2d", { alpha: false, @@ -1073,9 +1057,6 @@ class PDFPageView { this.#scaleRoundY = sfy[1]; } - // Add the viewport so it's known what it was originally drawn with. - this.#viewportMap.set(canvas, viewport); - // Rendering area const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] @@ -1141,6 +1122,9 @@ class PDFPageView { // callback had been invoked at least once. if (!(error instanceof RenderingCancelledException)) { showCanvas?.(true); + } else { + prevCanvas?.remove(); + this.#resetCanvas(); } return this.#finishRenderTask(renderTask, error); } diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index 115a8f853..97dcb6cd7 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -83,19 +83,14 @@ height: 100%; canvas { + position: absolute; + top: 0; + left: 0; margin: 0; display: block; width: 100%; height: 100%; - - &[hidden] { - display: none; - } - - &[zooming] { - width: 100%; - height: 100%; - } + contain: content; .structTree { contain: strict;