Merge pull request #20462 from calixteman/improve_thumbnails
Slightly reduce the memory used by thumbnails
This commit is contained in:
commit
f44e5f0e63
@ -21,13 +21,13 @@ describe("PDF Thumbnail View", () => {
|
|||||||
await page.waitForSelector(thumbSelector, { visible: true });
|
await page.waitForSelector(thumbSelector, { visible: true });
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
'#thumbnailView .thumbnail[data-loaded="true"]'
|
"#thumbnailView .thumbnail:not(.missingThumbnailImage)"
|
||||||
);
|
);
|
||||||
|
|
||||||
const src = await page.$eval(thumbSelector, el => el.src);
|
const src = await page.$eval(thumbSelector, el => el.src);
|
||||||
expect(src)
|
expect(src)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toMatch(/^data:image\//);
|
.toMatch(/^blob:http:/);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,7 +23,11 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
||||||
|
|
||||||
import { OutputScale, RenderingCancelledException } from "pdfjs-lib";
|
import {
|
||||||
|
FeatureTest,
|
||||||
|
OutputScale,
|
||||||
|
RenderingCancelledException,
|
||||||
|
} from "pdfjs-lib";
|
||||||
import { AppOptions } from "./app_options.js";
|
import { AppOptions } from "./app_options.js";
|
||||||
import { RenderingStates } from "./ui_utils.js";
|
import { RenderingStates } from "./ui_utils.js";
|
||||||
|
|
||||||
@ -31,13 +35,6 @@ 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_WIDTH = 98; // px
|
const THUMBNAIL_WIDTH = 98; // px
|
||||||
|
|
||||||
function zeroCanvas(c) {
|
|
||||||
// Zeroing the width and height causes Firefox to release graphics
|
|
||||||
// resources immediately, which can greatly reduce memory consumption.
|
|
||||||
c.width = 0;
|
|
||||||
c.height = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PDFThumbnailViewOptions
|
* @typedef {Object} PDFThumbnailViewOptions
|
||||||
* @property {HTMLDivElement} container - The viewer element.
|
* @property {HTMLDivElement} container - The viewer element.
|
||||||
@ -61,12 +58,15 @@ function zeroCanvas(c) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class TempImageFactory {
|
class TempImageFactory {
|
||||||
static #tempCanvas = null;
|
|
||||||
|
|
||||||
static getCanvas(width, height) {
|
static getCanvas(width, height) {
|
||||||
const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas"));
|
let tempCanvas;
|
||||||
|
if (FeatureTest.isOffscreenCanvasSupported) {
|
||||||
|
tempCanvas = new OffscreenCanvas(width, height);
|
||||||
|
} else {
|
||||||
|
tempCanvas = document.createElement("canvas");
|
||||||
tempCanvas.width = width;
|
tempCanvas.width = width;
|
||||||
tempCanvas.height = height;
|
tempCanvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
// Since this is a temporary canvas, we need to fill it with a white
|
// Since this is a temporary canvas, we need to fill it with a white
|
||||||
// background ourselves. `#getPageDrawContext` uses CSS rules for this.
|
// background ourselves. `#getPageDrawContext` uses CSS rules for this.
|
||||||
@ -75,14 +75,7 @@ class TempImageFactory {
|
|||||||
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, tempCanvas.getContext("2d")];
|
return [tempCanvas, ctx];
|
||||||
}
|
|
||||||
|
|
||||||
static destroyCanvas() {
|
|
||||||
if (this.#tempCanvas) {
|
|
||||||
zeroCanvas(this.#tempCanvas);
|
|
||||||
}
|
|
||||||
this.#tempCanvas = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,27 +119,24 @@ class PDFThumbnailView {
|
|||||||
this.renderingState = RenderingStates.INITIAL;
|
this.renderingState = RenderingStates.INITIAL;
|
||||||
this.resume = null;
|
this.resume = null;
|
||||||
|
|
||||||
const anchor = document.createElement("a");
|
const anchor = (this.anchor = document.createElement("a"));
|
||||||
anchor.href = linkService.getAnchorUrl("#page=" + id);
|
anchor.href = linkService.getAnchorUrl(`#page=${id}`);
|
||||||
anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
|
anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
|
||||||
anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
anchor.onclick = function () {
|
anchor.onclick = () => {
|
||||||
linkService.goToPage(id);
|
linkService.goToPage(id);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
this.anchor = anchor;
|
|
||||||
|
|
||||||
const div = document.createElement("div");
|
const div = (this.div = document.createElement("div"));
|
||||||
div.className = "thumbnail";
|
div.classList.add("thumbnail", "missingThumbnailImage");
|
||||||
div.setAttribute("data-page-number", this.id);
|
div.setAttribute("data-page-number", this.id);
|
||||||
this.div = div;
|
|
||||||
this.#updateDims();
|
this.#updateDims();
|
||||||
|
|
||||||
const img = document.createElement("div");
|
const image = (this.image = document.createElement("img"));
|
||||||
img.className = "thumbnailImage";
|
image.className = "thumbnailImage";
|
||||||
this._placeholderImg = img;
|
|
||||||
|
|
||||||
div.append(img);
|
div.append(image);
|
||||||
anchor.append(div);
|
anchor.append(div);
|
||||||
container.append(anchor);
|
container.append(anchor);
|
||||||
}
|
}
|
||||||
@ -155,13 +145,11 @@ class PDFThumbnailView {
|
|||||||
const { width, height } = this.viewport;
|
const { width, height } = this.viewport;
|
||||||
const ratio = width / height;
|
const ratio = width / height;
|
||||||
|
|
||||||
this.canvasWidth = THUMBNAIL_WIDTH;
|
const canvasWidth = (this.canvasWidth = THUMBNAIL_WIDTH);
|
||||||
this.canvasHeight = (this.canvasWidth / ratio) | 0;
|
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
|
||||||
this.scale = this.canvasWidth / width;
|
this.scale = canvasWidth / width;
|
||||||
|
|
||||||
const { style } = this.div;
|
this.div.style.height = `${canvasHeight}px`;
|
||||||
style.setProperty("--thumbnail-width", `${this.canvasWidth}px`);
|
|
||||||
style.setProperty("--thumbnail-height", `${this.canvasHeight}px`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPdfPage(pdfPage) {
|
setPdfPage(pdfPage) {
|
||||||
@ -175,14 +163,16 @@ class PDFThumbnailView {
|
|||||||
reset() {
|
reset() {
|
||||||
this.cancelRendering();
|
this.cancelRendering();
|
||||||
this.renderingState = RenderingStates.INITIAL;
|
this.renderingState = RenderingStates.INITIAL;
|
||||||
|
|
||||||
this.div.removeAttribute("data-loaded");
|
|
||||||
this.image?.replaceWith(this._placeholderImg);
|
|
||||||
this.#updateDims();
|
this.#updateDims();
|
||||||
|
|
||||||
if (this.image) {
|
const { image } = this;
|
||||||
this.image.removeAttribute("src");
|
const url = image.src;
|
||||||
delete this.image;
|
if (url) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
image.removeAttribute("data-l10n-id");
|
||||||
|
image.removeAttribute("data-l10n-args");
|
||||||
|
image.src = "";
|
||||||
|
this.div.classList.add("missingThumbnailImage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +203,6 @@ class PDFThumbnailView {
|
|||||||
#getPageDrawContext(upscaleFactor = 1) {
|
#getPageDrawContext(upscaleFactor = 1) {
|
||||||
// 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.
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const outputScale = new OutputScale();
|
const outputScale = new OutputScale();
|
||||||
const width = upscaleFactor * this.canvasWidth,
|
const width = upscaleFactor * this.canvasWidth,
|
||||||
height = upscaleFactor * this.canvasHeight;
|
height = upscaleFactor * this.canvasHeight;
|
||||||
@ -224,6 +213,9 @@ class PDFThumbnailView {
|
|||||||
this.maxCanvasPixels,
|
this.maxCanvasPixels,
|
||||||
this.maxCanvasDim
|
this.maxCanvasDim
|
||||||
);
|
);
|
||||||
|
// Because of: https://bugzilla.mozilla.org/show_bug.cgi?id=2003060
|
||||||
|
// we need use a HTMLCanvasElement here.
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = (width * outputScale.sx) | 0;
|
canvas.width = (width * outputScale.sx) | 0;
|
||||||
canvas.height = (height * outputScale.sy) | 0;
|
canvas.height = (height * outputScale.sy) | 0;
|
||||||
|
|
||||||
@ -234,23 +226,23 @@ class PDFThumbnailView {
|
|||||||
return { canvas, transform };
|
return { canvas, transform };
|
||||||
}
|
}
|
||||||
|
|
||||||
#convertCanvasToImage(canvas) {
|
async #convertCanvasToImage(canvas) {
|
||||||
if (this.renderingState !== RenderingStates.FINISHED) {
|
if (this.renderingState !== RenderingStates.FINISHED) {
|
||||||
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
||||||
}
|
}
|
||||||
const reducedCanvas = this.#reduceImage(canvas);
|
const reducedCanvas = this.#reduceImage(canvas);
|
||||||
|
const { image } = this;
|
||||||
const image = document.createElement("img");
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
image.className = "thumbnailImage";
|
reducedCanvas.toBlob(resolve);
|
||||||
|
const blob = await promise;
|
||||||
|
image.src = URL.createObjectURL(blob);
|
||||||
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
||||||
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
image.src = reducedCanvas.toDataURL();
|
this.div.classList.remove("missingThumbnailImage");
|
||||||
this.image = image;
|
if (!FeatureTest.isOffscreenCanvasSupported) {
|
||||||
|
// Clean up the canvas element since it is no longer needed.
|
||||||
this.div.setAttribute("data-loaded", true);
|
reducedCanvas.width = reducedCanvas.height = 0;
|
||||||
this._placeholderImg.replaceWith(image);
|
}
|
||||||
|
|
||||||
zeroCanvas(reducedCanvas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async draw() {
|
async draw() {
|
||||||
@ -303,7 +295,6 @@ class PDFThumbnailView {
|
|||||||
await renderTask.promise;
|
await renderTask.promise;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RenderingCancelledException) {
|
if (e instanceof RenderingCancelledException) {
|
||||||
zeroCanvas(canvas);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
error = e;
|
error = e;
|
||||||
@ -317,8 +308,7 @@ class PDFThumbnailView {
|
|||||||
}
|
}
|
||||||
this.renderingState = RenderingStates.FINISHED;
|
this.renderingState = RenderingStates.FINISHED;
|
||||||
|
|
||||||
this.#convertCanvasToImage(canvas);
|
await this.#convertCanvasToImage(canvas);
|
||||||
zeroCanvas(canvas);
|
|
||||||
|
|
||||||
this.eventBus.dispatch("thumbnailrendered", {
|
this.eventBus.dispatch("thumbnailrendered", {
|
||||||
source: this,
|
source: this,
|
||||||
@ -449,14 +439,9 @@ class PDFThumbnailView {
|
|||||||
*/
|
*/
|
||||||
setPageLabel(label) {
|
setPageLabel(label) {
|
||||||
this.pageLabel = typeof label === "string" ? label : null;
|
this.pageLabel = typeof label === "string" ? label : null;
|
||||||
|
|
||||||
this.anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
this.anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
|
this.image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
if (this.renderingState !== RenderingStates.FINISHED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.image?.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PDFThumbnailView, TempImageFactory };
|
export { PDFThumbnailView };
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {
|
|||||||
scrollIntoView,
|
scrollIntoView,
|
||||||
watchScroll,
|
watchScroll,
|
||||||
} from "./ui_utils.js";
|
} from "./ui_utils.js";
|
||||||
import { PDFThumbnailView, TempImageFactory } from "./pdf_thumbnail_view.js";
|
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
|
||||||
|
|
||||||
const THUMBNAIL_SCROLL_MARGIN = -19;
|
const THUMBNAIL_SCROLL_MARGIN = -19;
|
||||||
const THUMBNAIL_SELECTED_CLASS = "selected";
|
const THUMBNAIL_SELECTED_CLASS = "selected";
|
||||||
@ -174,7 +174,6 @@ class PDFThumbnailViewer {
|
|||||||
thumbnail.reset();
|
thumbnail.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TempImageFactory.destroyCanvas();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#resetView() {
|
#resetView() {
|
||||||
@ -209,10 +208,11 @@ class PDFThumbnailViewer {
|
|||||||
.then(firstPdfPage => {
|
.then(firstPdfPage => {
|
||||||
const pagesCount = pdfDocument.numPages;
|
const pagesCount = pdfDocument.numPages;
|
||||||
const viewport = firstPdfPage.getViewport({ scale: 1 });
|
const viewport = firstPdfPage.getViewport({ scale: 1 });
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||||
const thumbnail = new PDFThumbnailView({
|
const thumbnail = new PDFThumbnailView({
|
||||||
container: this.container,
|
container: fragment,
|
||||||
eventBus: this.eventBus,
|
eventBus: this.eventBus,
|
||||||
id: pageNum,
|
id: pageNum,
|
||||||
defaultViewport: viewport.clone(),
|
defaultViewport: viewport.clone(),
|
||||||
@ -234,6 +234,7 @@ class PDFThumbnailViewer {
|
|||||||
// Ensure that the current thumbnail is always highlighted on load.
|
// Ensure that the current thumbnail is always highlighted on load.
|
||||||
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
||||||
thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
|
thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
|
||||||
|
this.container.append(fragment);
|
||||||
})
|
})
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
console.error("Unable to initialize thumbnail viewer", reason);
|
console.error("Unable to initialize thumbnail viewer", reason);
|
||||||
|
|||||||
@ -717,61 +717,63 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#thumbnailView {
|
#thumbnailView {
|
||||||
|
--thumbnail-width: 98px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: calc(100% - 60px);
|
width: calc(100% - 60px);
|
||||||
padding: 10px 30px 0;
|
padding: 10px 30px 0;
|
||||||
}
|
|
||||||
|
|
||||||
#thumbnailView > a:is(:active, :focus) {
|
> a {
|
||||||
outline: 0;
|
width: auto;
|
||||||
}
|
height: auto;
|
||||||
|
|
||||||
.thumbnail {
|
> .thumbnail {
|
||||||
/* Define these variables here, and not in :root, since the individual
|
|
||||||
thumbnails may have different sizes. */
|
|
||||||
--thumbnail-width: 0;
|
|
||||||
--thumbnail-height: 0;
|
|
||||||
|
|
||||||
float: var(--inline-start);
|
|
||||||
width: var(--thumbnail-width);
|
width: var(--thumbnail-width);
|
||||||
height: var(--thumbnail-height);
|
|
||||||
margin: 0 10px 5px;
|
margin: 0 10px 5px;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
border: 7px solid transparent;
|
border: 7px solid transparent;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--thumbnail-selected-color) !important;
|
||||||
|
|
||||||
|
> .thumbnailImage {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#thumbnailView > a:last-of-type > .thumbnail {
|
&.missingThumbnailImage {
|
||||||
|
border: 1px dashed rgb(132 132 132);
|
||||||
|
padding: 7px;
|
||||||
|
> .thumbnailImage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .thumbnailImage {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:active, :focus) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type > .thumbnail {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus > .thumbnail,
|
&:focus > .thumbnail,
|
||||||
.thumbnail:hover {
|
.thumbnail:hover {
|
||||||
border-color: var(--thumbnail-hover-color);
|
border-color: var(--thumbnail-hover-color);
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail.selected {
|
> .thumbnailImage {
|
||||||
border-color: var(--thumbnail-selected-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnailImage {
|
|
||||||
width: var(--thumbnail-width);
|
|
||||||
height: var(--thumbnail-height);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus > .thumbnail > .thumbnailImage,
|
|
||||||
.thumbnail:hover > .thumbnailImage {
|
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail.selected > .thumbnailImage {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.thumbnail:not([data-loaded]) > .thumbnailImage {
|
|
||||||
width: calc(var(--thumbnail-width) - 2px);
|
|
||||||
height: calc(var(--thumbnail-height) - 2px);
|
|
||||||
border: 1px dashed rgb(132 132 132);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeWithDeepNesting > .treeItem,
|
.treeWithDeepNesting > .treeItem,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user