From 484da62f50e6516c0ed7fab9ab201d435d0a7637 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 28 Mar 2023 21:52:07 +0200 Subject: [PATCH] Inline `PDFPageView.paintOnCanvas` in the `draw` method, now that SVG-rendering is removed --- web/pdf_page_view.js | 322 +++++++++++++++++++------------------------ 1 file changed, 140 insertions(+), 182 deletions(-) diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 67afad367..e0b0d5632 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -26,7 +26,6 @@ import { AbortException, AnnotationMode, - createPromiseCapability, PixelsPerInch, RenderingCancelledException, setLayerDimensions, @@ -119,6 +118,8 @@ class PDFPageView { #previousRotation = null; + #renderError = null; + #renderingState = RenderingStates.INITIAL; #useThumbnailCanvas = { @@ -126,6 +127,8 @@ class PDFPageView { regularAnnotations: true, }; + #viewportMap = new WeakMap(); + /** * @param {PDFPageViewOptions} options */ @@ -160,10 +163,8 @@ class PDFPageView { this.renderingQueue = options.renderingQueue; this.l10n = options.l10n || NullL10n; - this.paintTask = null; - this.paintedViewportMap = new WeakMap(); + this.renderTask = null; this.resume = null; - this._renderError = null; if ( typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || GENERIC") @@ -430,7 +431,7 @@ class PDFPageView { return; } const zoomLayerCanvas = this.zoomLayer.firstChild; - this.paintedViewportMap.delete(zoomLayerCanvas); + 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; @@ -502,7 +503,7 @@ class PDFPageView { if (!zoomLayerNode) { if (this.canvas) { - this.paintedViewportMap.delete(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; @@ -559,10 +560,10 @@ class PDFPageView { let isScalingRestricted = false; if (this.canvas && this.maxCanvasPixels > 0) { - const outputScale = this.outputScale; + const { width, height } = this.viewport; + const { sx, sy } = this.outputScale; if ( - ((Math.floor(this.viewport.width) * outputScale.sx) | 0) * - ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > + ((Math.floor(width) * sx) | 0) * ((Math.floor(height) * sy) | 0) > this.maxCanvasPixels ) { isScalingRestricted = true; @@ -608,7 +609,7 @@ class PDFPageView { pageNumber: this.id, cssTransform: true, timestamp: performance.now(), - error: this._renderError, + error: this.#renderError, }); return; } @@ -640,9 +641,9 @@ class PDFPageView { keepTextLayer = false, cancelExtraDelay = 0, } = {}) { - if (this.paintTask) { - this.paintTask.cancel(cancelExtraDelay); - this.paintTask = null; + if (this.renderTask) { + this.renderTask.cancel(cancelExtraDelay); + this.renderTask = null; } this.resume = null; @@ -697,7 +698,7 @@ class PDFPageView { style.width = style.height = ""; } - const originalViewport = this.paintedViewportMap.get(target); + const originalViewport = this.#viewportMap.get(target); if (this.viewport !== originalViewport) { // The canvas may have been originally rotated; rotate relative to that. const relativeRotation = @@ -746,33 +747,33 @@ class PDFPageView { return this.viewport.convertToPdfPoint(x, y); } - async #finishPaintTask(paintTask, error = null) { - // The paintTask may have been replaced by a new one, so only remove - // the reference to the paintTask if it matches the one that is + async #finishRenderTask(renderTask, error = null) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is // triggering this callback. - if (paintTask === this.paintTask) { - this.paintTask = null; + if (renderTask === this.renderTask) { + this.renderTask = null; } if (error instanceof RenderingCancelledException) { - this._renderError = null; + this.#renderError = null; return; } - this._renderError = error; + 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. - this.#useThumbnailCanvas.regularAnnotations = !paintTask.separateAnnots; + this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots; this.eventBus.dispatch("pagerendered", { source: this, pageNumber: this.id, cssTransform: false, timestamp: performance.now(), - error: this._renderError, + error: this.#renderError, }); if (error) { @@ -785,7 +786,7 @@ class PDFPageView { console.error("Must be in new state before drawing"); this.reset(); // Ensure that we reset all state to prevent issues. } - const { div, pdfPage } = this; + const { div, l10n, pageColors, pdfPage, viewport } = this; if (!pdfPage) { this.renderingState = RenderingStates.FINISHED; @@ -837,7 +838,7 @@ class PDFPageView { renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS, linkService, downloadManager, - l10n: this.l10n, + l10n, enableScripting, hasJSActionsPromise, fieldObjectsPromise, @@ -846,53 +847,127 @@ class PDFPageView { }); } - let renderContinueCallback = null; - if (this.renderingQueue) { - renderContinueCallback = cont => { - if (!this.renderingQueue.isHighestPriority(this)) { - this.renderingState = RenderingStates.PAUSED; - this.resume = () => { - this.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - cont(); - }; + const renderContinueCallback = cont => { + showCanvas?.(false); + if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) { + this.renderingState = RenderingStates.PAUSED; + this.resume = () => { + this.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + + const { width, height } = viewport; + 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); + + 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. + } + }; + canvasWrapper.append(canvas); + this.canvas = canvas; + + const ctx = canvas.getContext("2d", { alpha: false }); + const outputScale = (this.outputScale = new OutputScale()); + + if (this.useOnlyCssZoom) { + const actualSizeViewport = viewport.clone({ + scale: PixelsPerInch.PDF_TO_CSS_UNITS, + }); + // Use a scale that makes the canvas have the originally intended size + // of the page. + outputScale.sx *= actualSizeViewport.width / width; + outputScale.sy *= actualSizeViewport.height / height; } - const paintTask = this.paintOnCanvas(canvasWrapper); - paintTask.onRenderContinue = renderContinueCallback; - this.paintTask = paintTask; + if (this.maxCanvasPixels > 0) { + const pixelsInViewport = width * height; + const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + this.hasRestrictedScaling = true; + } else { + this.hasRestrictedScaling = false; + } + } + const sfx = approximateFraction(outputScale.sx); + const sfy = approximateFraction(outputScale.sy); - const resultPromise = paintTask.promise.then( - () => { - return this.#finishPaintTask(paintTask).then(async () => { - this.#renderTextLayer(); + canvas.width = roundToDivide(width * outputScale.sx, sfx[0]); + canvas.height = roundToDivide(height * outputScale.sy, sfy[0]); + const { style } = canvas; + style.width = roundToDivide(width, sfx[1]) + "px"; + style.height = roundToDivide(height, sfy[1]) + "px"; - if (this.annotationLayer) { - await this.#renderAnnotationLayer(); + // 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] + : null; + const renderContext = { + canvasContext: ctx, + transform, + viewport, + annotationMode: this.#annotationMode, + optionalContentConfigPromise: this._optionalContentConfigPromise, + annotationCanvasMap: this._annotationCanvasMap, + pageColors, + }; + const renderTask = (this.renderTask = this.pdfPage.render(renderContext)); + renderTask.onContinue = renderContinueCallback; + + const resultPromise = renderTask.promise.then( + async () => { + showCanvas?.(true); + await this.#finishRenderTask(renderTask); + + this.#renderTextLayer(); + + if (this.annotationLayer) { + await this.#renderAnnotationLayer(); + } + + if (!this.annotationEditorLayer) { + const { annotationEditorUIManager } = this.#layerProperties(); + + if (!annotationEditorUIManager) { + return; } - - if (!this.annotationEditorLayer) { - const { annotationEditorUIManager } = this.#layerProperties(); - - if (!annotationEditorUIManager) { - return; - } - this.annotationEditorLayer = new AnnotationEditorLayerBuilder({ - uiManager: annotationEditorUIManager, - pageDiv: div, - pdfPage, - l10n: this.l10n, - accessibilityManager: this._accessibilityManager, - }); - } - this.#renderAnnotationEditorLayer(); - }); + this.annotationEditorLayer = new AnnotationEditorLayerBuilder({ + uiManager: annotationEditorUIManager, + pageDiv: div, + pdfPage, + l10n, + accessibilityManager: this._accessibilityManager, + }); + } + this.#renderAnnotationEditorLayer(); }, error => { - return this.#finishPaintTask(paintTask, error); + // When zooming with a `drawingDelay` set, avoid temporarily showing + // a black canvas if rendering was cancelled before the `onContinue`- + // callback had been invoked at least once. + if (!(error instanceof RenderingCancelledException)) { + showCanvas?.(true); + } + return this.#finishRenderTask(renderTask, error); } ); @@ -922,123 +997,6 @@ class PDFPageView { return resultPromise; } - paintOnCanvas(canvasWrapper) { - const renderCapability = createPromiseCapability(); - const result = { - promise: renderCapability.promise, - onRenderContinue(cont) { - cont(); - }, - cancel(extraDelay = 0) { - renderTask.cancel(extraDelay); - }, - get separateAnnots() { - return renderTask.separateAnnots; - }, - }; - - const viewport = this.viewport; - const { width, height } = viewport; - 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 = !!( - this.pageColors?.background && this.pageColors?.foreground - ); - 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. - } - }; - - canvasWrapper.append(canvas); - this.canvas = canvas; - - const ctx = canvas.getContext("2d", { alpha: false }); - const outputScale = (this.outputScale = new OutputScale()); - - if (this.useOnlyCssZoom) { - const actualSizeViewport = viewport.clone({ - scale: PixelsPerInch.PDF_TO_CSS_UNITS, - }); - // Use a scale that makes the canvas have the originally intended size - // of the page. - outputScale.sx *= actualSizeViewport.width / width; - outputScale.sy *= actualSizeViewport.height / height; - } - - if (this.maxCanvasPixels > 0) { - const pixelsInViewport = width * height; - const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); - if (outputScale.sx > maxScale || outputScale.sy > maxScale) { - outputScale.sx = maxScale; - outputScale.sy = maxScale; - this.hasRestrictedScaling = true; - } else { - this.hasRestrictedScaling = false; - } - } - - 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]); - 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); - - // Rendering area - const transform = outputScale.scaled - ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] - : null; - const renderContext = { - canvasContext: ctx, - transform, - viewport, - annotationMode: this.#annotationMode, - optionalContentConfigPromise: this._optionalContentConfigPromise, - annotationCanvasMap: this._annotationCanvasMap, - pageColors: this.pageColors, - }; - const renderTask = this.pdfPage.render(renderContext); - renderTask.onContinue = function (cont) { - showCanvas?.(false); - if (result.onRenderContinue) { - result.onRenderContinue(cont); - } else { - cont(); - } - }; - - renderTask.promise.then( - function () { - showCanvas?.(true); - renderCapability.resolve(); - }, - function (error) { - // When zooming with a `drawingDelay` set, avoid temporarily showing - // a black canvas if rendering was cancelled before the `onContinue`- - // callback had been invoked at least once. - if (!(error instanceof RenderingCancelledException)) { - showCanvas?.(true); - } - renderCapability.reject(error); - } - ); - return result; - } - /** * @param {string|null} label */