Merge pull request #13197 from Snuffleupagus/issue-8233
Improve the image quality of thumbnails rendered by `PDFThumbnailView.draw` (issue 8233)
This commit is contained in:
		
						commit
						f5e973d555
					
				| @ -17,6 +17,7 @@ import { getOutputScale } from "./ui_utils.js"; | |||||||
| import { RenderingCancelledException } from "pdfjs-lib"; | import { RenderingCancelledException } from "pdfjs-lib"; | ||||||
| import { RenderingStates } from "./pdf_rendering_queue.js"; | 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 MAX_NUM_SCALING_STEPS = 3; | ||||||
| const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px
 | const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px
 | ||||||
| const THUMBNAIL_WIDTH = 98; // px
 | const THUMBNAIL_WIDTH = 98; // px
 | ||||||
| @ -65,7 +66,7 @@ const TempImageFactory = (function TempImageFactoryClosure() { | |||||||
|       ctx.fillStyle = "rgb(255, 255, 255)"; |       ctx.fillStyle = "rgb(255, 255, 255)"; | ||||||
|       ctx.fillRect(0, 0, width, height); |       ctx.fillRect(0, 0, width, height); | ||||||
|       ctx.restore(); |       ctx.restore(); | ||||||
|       return tempCanvas; |       return [tempCanvas, tempCanvas.getContext("2d")]; | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     destroyCanvas() { |     destroyCanvas() { | ||||||
| @ -122,13 +123,13 @@ class PDFThumbnailView { | |||||||
|       }; |       }; | ||||||
|     this.disableCanvasToImageConversion = disableCanvasToImageConversion; |     this.disableCanvasToImageConversion = disableCanvasToImageConversion; | ||||||
| 
 | 
 | ||||||
|     this.pageWidth = this.viewport.width; |     const pageWidth = this.viewport.width, | ||||||
|     this.pageHeight = this.viewport.height; |       pageHeight = this.viewport.height, | ||||||
|     this.pageRatio = this.pageWidth / this.pageHeight; |       pageRatio = pageWidth / pageHeight; | ||||||
| 
 | 
 | ||||||
|     this.canvasWidth = THUMBNAIL_WIDTH; |     this.canvasWidth = THUMBNAIL_WIDTH; | ||||||
|     this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; |     this.canvasHeight = (this.canvasWidth / pageRatio) | 0; | ||||||
|     this.scale = this.canvasWidth / this.pageWidth; |     this.scale = this.canvasWidth / pageWidth; | ||||||
| 
 | 
 | ||||||
|     this.l10n = l10n; |     this.l10n = l10n; | ||||||
| 
 | 
 | ||||||
| @ -172,19 +173,16 @@ class PDFThumbnailView { | |||||||
|     this.cancelRendering(); |     this.cancelRendering(); | ||||||
|     this.renderingState = RenderingStates.INITIAL; |     this.renderingState = RenderingStates.INITIAL; | ||||||
| 
 | 
 | ||||||
|     this.pageWidth = this.viewport.width; |     const pageWidth = this.viewport.width, | ||||||
|     this.pageHeight = this.viewport.height; |       pageHeight = this.viewport.height, | ||||||
|     this.pageRatio = this.pageWidth / this.pageHeight; |       pageRatio = pageWidth / pageHeight; | ||||||
| 
 | 
 | ||||||
|     this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; |     this.canvasHeight = (this.canvasWidth / pageRatio) | 0; | ||||||
|     this.scale = this.canvasWidth / this.pageWidth; |     this.scale = this.canvasWidth / pageWidth; | ||||||
| 
 | 
 | ||||||
|     this.div.removeAttribute("data-loaded"); |     this.div.removeAttribute("data-loaded"); | ||||||
|     const ring = this.ring; |     const ring = this.ring; | ||||||
|     const childNodes = ring.childNodes; |     ring.textContent = ""; // Remove the thumbnail from the DOM.
 | ||||||
|     for (let i = childNodes.length - 1; i >= 0; i--) { |  | ||||||
|       ring.removeChild(childNodes[i]); |  | ||||||
|     } |  | ||||||
|     const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; |     const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; | ||||||
|     ring.style.width = this.canvasWidth + borderAdjustment + "px"; |     ring.style.width = this.canvasWidth + borderAdjustment + "px"; | ||||||
|     ring.style.height = this.canvasHeight + borderAdjustment + "px"; |     ring.style.height = this.canvasHeight + borderAdjustment + "px"; | ||||||
| @ -229,11 +227,10 @@ class PDFThumbnailView { | |||||||
|   /** |   /** | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   _getPageDrawContext() { |   _getPageDrawContext(upscaleFactor = 1) { | ||||||
|     const canvas = document.createElement("canvas"); |  | ||||||
|     // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
 |     // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
 | ||||||
|     // until rendering/image conversion is complete, to avoid display issues.
 |     // until rendering/image conversion is complete, to avoid display issues.
 | ||||||
|     this.canvas = canvas; |     const canvas = document.createElement("canvas"); | ||||||
| 
 | 
 | ||||||
|     if ( |     if ( | ||||||
|       typeof PDFJSDev === "undefined" || |       typeof PDFJSDev === "undefined" || | ||||||
| @ -244,50 +241,48 @@ class PDFThumbnailView { | |||||||
|     const ctx = canvas.getContext("2d", { alpha: false }); |     const ctx = canvas.getContext("2d", { alpha: false }); | ||||||
|     const outputScale = getOutputScale(ctx); |     const outputScale = getOutputScale(ctx); | ||||||
| 
 | 
 | ||||||
|     canvas.width = (this.canvasWidth * outputScale.sx) | 0; |     canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0; | ||||||
|     canvas.height = (this.canvasHeight * outputScale.sy) | 0; |     canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0; | ||||||
|     canvas.style.width = this.canvasWidth + "px"; |  | ||||||
|     canvas.style.height = this.canvasHeight + "px"; |  | ||||||
| 
 | 
 | ||||||
|     const transform = outputScale.scaled |     const transform = outputScale.scaled | ||||||
|       ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] |       ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] | ||||||
|       : null; |       : null; | ||||||
| 
 | 
 | ||||||
|     return [ctx, transform]; |     return { ctx, canvas, transform }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   _convertCanvasToImage() { |   _convertCanvasToImage(canvas) { | ||||||
|     if (!this.canvas) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if (this.renderingState !== RenderingStates.FINISHED) { |     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) { |     if (this.disableCanvasToImageConversion) { | ||||||
|       this.canvas.className = className; |       reducedCanvas.className = "thumbnailImage"; | ||||||
|       this._thumbPageCanvas.then(msg => { |       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.div.setAttribute("data-loaded", true); | ||||||
|       this.ring.appendChild(this.canvas); |       this.ring.appendChild(reducedCanvas); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const image = document.createElement("img"); |     const image = document.createElement("img"); | ||||||
|     image.className = className; |     image.className = "thumbnailImage"; | ||||||
|     this._thumbPageCanvas.then(msg => { |     this._thumbPageCanvas.then(msg => { | ||||||
|       image.setAttribute("aria-label", msg); |       image.setAttribute("aria-label", msg); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     image.style.width = this.canvasWidth + "px"; |     image.style.width = this.canvasWidth + "px"; | ||||||
|     image.style.height = this.canvasHeight + "px"; |     image.style.height = this.canvasHeight + "px"; | ||||||
| 
 | 
 | ||||||
|     image.src = this.canvas.toDataURL(); |     image.src = reducedCanvas.toDataURL(); | ||||||
|     this.image = image; |     this.image = image; | ||||||
| 
 | 
 | ||||||
|     this.div.setAttribute("data-loaded", true); |     this.div.setAttribute("data-loaded", true); | ||||||
| @ -295,9 +290,8 @@ class PDFThumbnailView { | |||||||
| 
 | 
 | ||||||
|     // Zeroing the width and height causes Firefox to release graphics
 |     // Zeroing the width and height causes Firefox to release graphics
 | ||||||
|     // resources immediately, which can greatly reduce memory consumption.
 |     // resources immediately, which can greatly reduce memory consumption.
 | ||||||
|     this.canvas.width = 0; |     reducedCanvas.width = 0; | ||||||
|     this.canvas.height = 0; |     reducedCanvas.height = 0; | ||||||
|     delete this.canvas; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   draw() { |   draw() { | ||||||
| @ -325,17 +319,25 @@ class PDFThumbnailView { | |||||||
|       if (error instanceof RenderingCancelledException) { |       if (error instanceof RenderingCancelledException) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 |  | ||||||
|       this.renderingState = RenderingStates.FINISHED; |       this.renderingState = RenderingStates.FINISHED; | ||||||
|       this._convertCanvasToImage(); |       this._convertCanvasToImage(canvas); | ||||||
| 
 | 
 | ||||||
|       if (error) { |       if (error) { | ||||||
|         throw error; |         throw error; | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const [ctx, transform] = this._getPageDrawContext(); |     // Render the thumbnail at a larger size and downsize the canvas (similar
 | ||||||
|     const drawViewport = this.viewport.clone({ scale: this.scale }); |     // 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 => { |     const renderContinueCallback = cont => { | ||||||
|       if (!this.renderingQueue.isHighestPriority(this)) { |       if (!this.renderingQueue.isHighestPriority(this)) { | ||||||
|         this.renderingState = RenderingStates.PAUSED; |         this.renderingState = RenderingStates.PAUSED; | ||||||
| @ -359,20 +361,24 @@ class PDFThumbnailView { | |||||||
| 
 | 
 | ||||||
|     const resultPromise = renderTask.promise.then( |     const resultPromise = renderTask.promise.then( | ||||||
|       function () { |       function () { | ||||||
|         finishRenderTask(null); |         return finishRenderTask(null); | ||||||
|       }, |       }, | ||||||
|       function (error) { |       function (error) { | ||||||
|         finishRenderTask(error); |         return finishRenderTask(error); | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|  |     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
 |       // Only trigger cleanup, once rendering has finished, when the current
 | ||||||
|       // pageView is *not* cached on the `BaseViewer`-instance.
 |       // pageView is *not* cached on the `BaseViewer`-instance.
 | ||||||
|     resultPromise.finally(() => { |  | ||||||
|       const pageCached = this.linkService.isPageCached(this.id); |       const pageCached = this.linkService.isPageCached(this.id); | ||||||
|       if (pageCached) { |       if (!pageCached) { | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|         this.pdfPage?.cleanup(); |         this.pdfPage?.cleanup(); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return resultPromise; |     return resultPromise; | ||||||
| @ -385,18 +391,23 @@ class PDFThumbnailView { | |||||||
|     if (this.renderingState !== RenderingStates.INITIAL) { |     if (this.renderingState !== RenderingStates.INITIAL) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const img = pageView.canvas; |     const { canvas, pdfPage } = pageView; | ||||||
|     if (!img) { |     if (!canvas) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (!this.pdfPage) { |     if (!this.pdfPage) { | ||||||
|       this.setPdfPage(pageView.pdfPage); |       this.setPdfPage(pdfPage); | ||||||
|  |     } | ||||||
|  |     this.renderingState = RenderingStates.FINISHED; | ||||||
|  |     this._convertCanvasToImage(canvas); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     this.renderingState = RenderingStates.FINISHED; |   /** | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _reduceImage(img) { | ||||||
|  |     const { ctx, canvas } = this._getPageDrawContext(); | ||||||
| 
 | 
 | ||||||
|     const [ctx] = this._getPageDrawContext(); |  | ||||||
|     const canvas = ctx.canvas; |  | ||||||
|     if (img.width <= 2 * canvas.width) { |     if (img.width <= 2 * canvas.width) { | ||||||
|       ctx.drawImage( |       ctx.drawImage( | ||||||
|         img, |         img, | ||||||
| @ -409,18 +420,15 @@ class PDFThumbnailView { | |||||||
|         canvas.width, |         canvas.width, | ||||||
|         canvas.height |         canvas.height | ||||||
|       ); |       ); | ||||||
|       this._convertCanvasToImage(); |       return canvas; | ||||||
|       return; |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     // drawImage does an awful job of rescaling the image, doing it gradually.
 |     // drawImage does an awful job of rescaling the image, doing it gradually.
 | ||||||
|     let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; |     let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; | ||||||
|     let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; |     let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; | ||||||
|     const reducedImage = TempImageFactory.getCanvas( |     const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas( | ||||||
|       reducedWidth, |       reducedWidth, | ||||||
|       reducedHeight |       reducedHeight | ||||||
|     ); |     ); | ||||||
|     const reducedImageCtx = reducedImage.getContext("2d"); |  | ||||||
| 
 | 
 | ||||||
|     while (reducedWidth > img.width || reducedHeight > img.height) { |     while (reducedWidth > img.width || reducedHeight > img.height) { | ||||||
|       reducedWidth >>= 1; |       reducedWidth >>= 1; | ||||||
| @ -463,7 +471,7 @@ class PDFThumbnailView { | |||||||
|       canvas.width, |       canvas.width, | ||||||
|       canvas.height |       canvas.height | ||||||
|     ); |     ); | ||||||
|     this._convertCanvasToImage(); |     return canvas; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get _thumbPageTitle() { |   get _thumbPageTitle() { | ||||||
| @ -495,7 +503,7 @@ class PDFThumbnailView { | |||||||
|     this._thumbPageCanvas.then(msg => { |     this._thumbPageCanvas.then(msg => { | ||||||
|       if (this.image) { |       if (this.image) { | ||||||
|         this.image.setAttribute("aria-label", msg); |         this.image.setAttribute("aria-label", msg); | ||||||
|       } else if (this.disableCanvasToImageConversion && this.canvas) { |       } else if (this.canvas) { | ||||||
|         this.canvas.setAttribute("aria-label", msg); |         this.canvas.setAttribute("aria-label", msg); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user