Skip to content

Commit

Permalink
Improve the image quality of thumbnails rendered by `PDFThumbnailView…
Browse files Browse the repository at this point in the history
….draw` (issue 8233)

The reason for the fairly large discrepancy, in the thumbnail quality, between the `draw`/`setImage`-methods is that in the former case we *directly* render the thumbnails at the final size that they'll appear at in the sidebar. In the latter case, we instead downsize the (generally) much larger "regular" pages.

To address this, I'm thus proposing that we let `PDFThumbnailView.draw` render thumbnails at *twice* their intended size and then downsize them to the final size.
Obviously this will increase *peak* memory usage during thumbnail rendering in `PDFThumbnailView.draw`, since doubling the width/height of a `canvas` will lead to its pixel-count increasing by a factor of `4`. Furthermore, since you need four components per pixel (given that it's RGBA-data), this will thus lead to the *temporary* thumbnail `canvas`-sizes increasing by a factor of `16` during rendering. Hence why rendering thumbnails at their "original" scale, i.e. using something like `PDFPageProxy.getViewport({ scale: 1 });`, would be an absolutely terrible idea!

To reduce the size and scope of these changes, I've tried to re-factor and re-use as much of the existing downsizing-implementation already present in `PDFThumbnailView` as possible.

While this will generally *not* make make thumbnails rendered by `PDFThumbnailView.draw` look *identical* to those based on the rendered pages (via `PDFThumbnailView.setImage`), it's a considerable improvement as far as I'm concerned and enough to call the issue fixed.

*Please note:* This patch will not lead to *any* additional overhead, in either memory usage or parsing, for thumbnails which are based on the rendered pages.
  • Loading branch information
Snuffleupagus committed Apr 7, 2021
1 parent 26552fb commit 3c1024b
Showing 1 changed file with 56 additions and 38 deletions.
94 changes: 56 additions & 38 deletions web/pdf_thumbnail_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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() {
Expand Down Expand Up @@ -229,11 +229,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" ||
Expand All @@ -244,60 +243,67 @@ 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.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 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
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
: null;

return [ctx, transform];
return { ctx, canvas, transform };
}

/**
* @private
*/
_convertCanvasToImage() {
if (!this.canvas) {
return;
}
_convertCanvasToImage(canvas, resetCanvas = false) {
if (this.renderingState !== RenderingStates.FINISHED) {
return;
throw new Error("_convertCanvasToImage: Rendering has not finished.");
}
let reducedCanvas = this._reduceImage(canvas);

if (resetCanvas) {
// Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
canvas.width = 0;
canvas.height = 0;
canvas = null;
}
const className = "thumbnailImage";

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);
});

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);
this.ring.appendChild(image);

// 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;
reducedCanvas = null;
}

draw() {
Expand Down Expand Up @@ -325,17 +331,27 @@ class PDFThumbnailView {
if (error instanceof RenderingCancelledException) {
return;
}

this.renderingState = RenderingStates.FINISHED;
this._convertCanvasToImage();
this._convertCanvasToImage(canvas, /* resetCanvas = */ true);

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 DRAW_UPSCALE_FACTOR = 2;

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;
Expand Down Expand Up @@ -385,18 +401,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,
Expand All @@ -409,18 +430,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;
Expand Down Expand Up @@ -463,7 +481,7 @@ class PDFThumbnailView {
canvas.width,
canvas.height
);
this._convertCanvasToImage();
return canvas;
}

get _thumbPageTitle() {
Expand Down Expand Up @@ -495,7 +513,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);
}
});
Expand Down

0 comments on commit 3c1024b

Please sign in to comment.