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:
Tim van der Meij 2021-04-09 21:51:42 +02:00 committed by GitHub
commit f5e973d555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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);
} }
); );
// Only trigger cleanup, once rendering has finished, when the current
// pageView is *not* cached on the `BaseViewer`-instance.
resultPromise.finally(() => { 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); 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.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) { 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);
} }
}); });