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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user