diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index cb4b4b63d..178fe755c 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -17,6 +17,7 @@ import { getOutputScale } from "./ui_utils.js"; import { RenderingCancelledException } from "pdfjs-lib"; import { RenderingStates } from "./pdf_rendering_queue.js"; +const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below. const MAX_NUM_SCALING_STEPS = 3; const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px const THUMBNAIL_WIDTH = 98; // px @@ -65,7 +66,7 @@ const TempImageFactory = (function TempImageFactoryClosure() { ctx.fillStyle = "rgb(255, 255, 255)"; ctx.fillRect(0, 0, width, height); ctx.restore(); - return tempCanvas; + return [tempCanvas, tempCanvas.getContext("2d")]; }, destroyCanvas() { @@ -226,11 +227,10 @@ class PDFThumbnailView { /** * @private */ - _getPageDrawContext() { - const canvas = document.createElement("canvas"); + _getPageDrawContext(upscaleFactor = 1) { // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`, // until rendering/image conversion is complete, to avoid display issues. - this.canvas = canvas; + const canvas = document.createElement("canvas"); if ( typeof PDFJSDev === "undefined" || @@ -241,50 +241,48 @@ class PDFThumbnailView { const ctx = canvas.getContext("2d", { alpha: false }); const outputScale = getOutputScale(ctx); - canvas.width = (this.canvasWidth * outputScale.sx) | 0; - canvas.height = (this.canvasHeight * outputScale.sy) | 0; - canvas.style.width = this.canvasWidth + "px"; - canvas.style.height = this.canvasHeight + "px"; + canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0; + canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0; const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null; - return [ctx, transform]; + return { ctx, canvas, transform }; } /** * @private */ - _convertCanvasToImage() { - if (!this.canvas) { - return; - } + _convertCanvasToImage(canvas) { if (this.renderingState !== RenderingStates.FINISHED) { - return; + throw new Error("_convertCanvasToImage: Rendering has not finished."); } - const className = "thumbnailImage"; + const reducedCanvas = this._reduceImage(canvas); if (this.disableCanvasToImageConversion) { - this.canvas.className = className; + reducedCanvas.className = "thumbnailImage"; this._thumbPageCanvas.then(msg => { - this.canvas.setAttribute("aria-label", msg); + reducedCanvas.setAttribute("aria-label", msg); }); + reducedCanvas.style.width = this.canvasWidth + "px"; + reducedCanvas.style.height = this.canvasHeight + "px"; + + this.canvas = reducedCanvas; this.div.setAttribute("data-loaded", true); - this.ring.appendChild(this.canvas); + this.ring.appendChild(reducedCanvas); return; } const image = document.createElement("img"); - image.className = className; + image.className = "thumbnailImage"; this._thumbPageCanvas.then(msg => { image.setAttribute("aria-label", msg); }); - image.style.width = this.canvasWidth + "px"; image.style.height = this.canvasHeight + "px"; - image.src = this.canvas.toDataURL(); + image.src = reducedCanvas.toDataURL(); this.image = image; this.div.setAttribute("data-loaded", true); @@ -292,9 +290,8 @@ class PDFThumbnailView { // 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; + reducedCanvas.width = 0; + reducedCanvas.height = 0; } draw() { @@ -322,17 +319,25 @@ class PDFThumbnailView { if (error instanceof RenderingCancelledException) { return; } - this.renderingState = RenderingStates.FINISHED; - this._convertCanvasToImage(); + this._convertCanvasToImage(canvas); if (error) { throw error; } }; - const [ctx, transform] = this._getPageDrawContext(); - const drawViewport = this.viewport.clone({ scale: this.scale }); + // Render the thumbnail at a larger size and downsize the canvas (similar + // to `setImage`), to improve consistency between thumbnails created by + // the `draw` and `setImage` methods (fixes issue 8233). + // NOTE: To primarily avoid increasing memory usage too much, but also to + // reduce downsizing overhead, we purposely limit the up-scaling factor. + const { ctx, canvas, transform } = this._getPageDrawContext( + DRAW_UPSCALE_FACTOR + ); + const drawViewport = this.viewport.clone({ + scale: DRAW_UPSCALE_FACTOR * this.scale, + }); const renderContinueCallback = cont => { if (!this.renderingQueue.isHighestPriority(this)) { this.renderingState = RenderingStates.PAUSED; @@ -356,20 +361,24 @@ class PDFThumbnailView { const resultPromise = renderTask.promise.then( function () { - finishRenderTask(null); + return finishRenderTask(null); }, function (error) { - finishRenderTask(error); + return finishRenderTask(error); } ); - // Only trigger cleanup, once rendering has finished, when the current - // pageView is *not* cached on the `BaseViewer`-instance. resultPromise.finally(() => { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + canvas.width = 0; + canvas.height = 0; + + // Only trigger cleanup, once rendering has finished, when the current + // pageView is *not* cached on the `BaseViewer`-instance. const pageCached = this.linkService.isPageCached(this.id); - if (pageCached) { - return; + if (!pageCached) { + this.pdfPage?.cleanup(); } - this.pdfPage?.cleanup(); }); return resultPromise; @@ -382,18 +391,23 @@ class PDFThumbnailView { if (this.renderingState !== RenderingStates.INITIAL) { return; } - const img = pageView.canvas; - if (!img) { + const { canvas, pdfPage } = pageView; + if (!canvas) { return; } if (!this.pdfPage) { - this.setPdfPage(pageView.pdfPage); + this.setPdfPage(pdfPage); } - this.renderingState = RenderingStates.FINISHED; + this._convertCanvasToImage(canvas); + } + + /** + * @private + */ + _reduceImage(img) { + const { ctx, canvas } = this._getPageDrawContext(); - const [ctx] = this._getPageDrawContext(); - const canvas = ctx.canvas; if (img.width <= 2 * canvas.width) { ctx.drawImage( img, @@ -406,18 +420,15 @@ class PDFThumbnailView { canvas.width, canvas.height ); - this._convertCanvasToImage(); - return; + return canvas; } - // drawImage does an awful job of rescaling the image, doing it gradually. let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; - const reducedImage = TempImageFactory.getCanvas( + const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas( reducedWidth, reducedHeight ); - const reducedImageCtx = reducedImage.getContext("2d"); while (reducedWidth > img.width || reducedHeight > img.height) { reducedWidth >>= 1; @@ -460,7 +471,7 @@ class PDFThumbnailView { canvas.width, canvas.height ); - this._convertCanvasToImage(); + return canvas; } get _thumbPageTitle() { @@ -492,7 +503,7 @@ class PDFThumbnailView { this._thumbPageCanvas.then(msg => { if (this.image) { this.image.setAttribute("aria-label", msg); - } else if (this.disableCanvasToImageConversion && this.canvas) { + } else if (this.canvas) { this.canvas.setAttribute("aria-label", msg); } });