Skip to content

Commit f44e5f0

Browse files
Merge pull request #20462 from calixteman/improve_thumbnails
Slightly reduce the memory used by thumbnails
2 parents a965536 + 423379e commit f44e5f0

File tree

4 files changed

+102
-114
lines changed

4 files changed

+102
-114
lines changed

test/integration/thumbnail_view_spec.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ describe("PDF Thumbnail View", () => {
2121
await page.waitForSelector(thumbSelector, { visible: true });
2222

2323
await page.waitForSelector(
24-
'#thumbnailView .thumbnail[data-loaded="true"]'
24+
"#thumbnailView .thumbnail:not(.missingThumbnailImage)"
2525
);
2626

2727
const src = await page.$eval(thumbSelector, el => el.src);
2828
expect(src)
2929
.withContext(`In ${browserName}`)
30-
.toMatch(/^data:image\//);
30+
.toMatch(/^blob:http:/);
3131
})
3232
);
3333
});

web/pdf_thumbnail_view.js

Lines changed: 51 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,18 @@
2323
// eslint-disable-next-line max-len
2424
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
2525

26-
import { OutputScale, RenderingCancelledException } from "pdfjs-lib";
26+
import {
27+
FeatureTest,
28+
OutputScale,
29+
RenderingCancelledException,
30+
} from "pdfjs-lib";
2731
import { AppOptions } from "./app_options.js";
2832
import { RenderingStates } from "./ui_utils.js";
2933

3034
const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
3135
const MAX_NUM_SCALING_STEPS = 3;
3236
const THUMBNAIL_WIDTH = 98; // px
3337

34-
function zeroCanvas(c) {
35-
// Zeroing the width and height causes Firefox to release graphics
36-
// resources immediately, which can greatly reduce memory consumption.
37-
c.width = 0;
38-
c.height = 0;
39-
}
40-
4138
/**
4239
* @typedef {Object} PDFThumbnailViewOptions
4340
* @property {HTMLDivElement} container - The viewer element.
@@ -61,12 +58,15 @@ function zeroCanvas(c) {
6158
*/
6259

6360
class TempImageFactory {
64-
static #tempCanvas = null;
65-
6661
static getCanvas(width, height) {
67-
const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas"));
68-
tempCanvas.width = width;
69-
tempCanvas.height = height;
62+
let tempCanvas;
63+
if (FeatureTest.isOffscreenCanvasSupported) {
64+
tempCanvas = new OffscreenCanvas(width, height);
65+
} else {
66+
tempCanvas = document.createElement("canvas");
67+
tempCanvas.width = width;
68+
tempCanvas.height = height;
69+
}
7070

7171
// Since this is a temporary canvas, we need to fill it with a white
7272
// background ourselves. `#getPageDrawContext` uses CSS rules for this.
@@ -75,14 +75,7 @@ class TempImageFactory {
7575
ctx.fillStyle = "rgb(255, 255, 255)";
7676
ctx.fillRect(0, 0, width, height);
7777
ctx.restore();
78-
return [tempCanvas, tempCanvas.getContext("2d")];
79-
}
80-
81-
static destroyCanvas() {
82-
if (this.#tempCanvas) {
83-
zeroCanvas(this.#tempCanvas);
84-
}
85-
this.#tempCanvas = null;
78+
return [tempCanvas, ctx];
8679
}
8780
}
8881

@@ -126,27 +119,24 @@ class PDFThumbnailView {
126119
this.renderingState = RenderingStates.INITIAL;
127120
this.resume = null;
128121

129-
const anchor = document.createElement("a");
130-
anchor.href = linkService.getAnchorUrl("#page=" + id);
122+
const anchor = (this.anchor = document.createElement("a"));
123+
anchor.href = linkService.getAnchorUrl(`#page=${id}`);
131124
anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
132125
anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
133-
anchor.onclick = function () {
126+
anchor.onclick = () => {
134127
linkService.goToPage(id);
135128
return false;
136129
};
137-
this.anchor = anchor;
138130

139-
const div = document.createElement("div");
140-
div.className = "thumbnail";
131+
const div = (this.div = document.createElement("div"));
132+
div.classList.add("thumbnail", "missingThumbnailImage");
141133
div.setAttribute("data-page-number", this.id);
142-
this.div = div;
143134
this.#updateDims();
144135

145-
const img = document.createElement("div");
146-
img.className = "thumbnailImage";
147-
this._placeholderImg = img;
136+
const image = (this.image = document.createElement("img"));
137+
image.className = "thumbnailImage";
148138

149-
div.append(img);
139+
div.append(image);
150140
anchor.append(div);
151141
container.append(anchor);
152142
}
@@ -155,13 +145,11 @@ class PDFThumbnailView {
155145
const { width, height } = this.viewport;
156146
const ratio = width / height;
157147

158-
this.canvasWidth = THUMBNAIL_WIDTH;
159-
this.canvasHeight = (this.canvasWidth / ratio) | 0;
160-
this.scale = this.canvasWidth / width;
148+
const canvasWidth = (this.canvasWidth = THUMBNAIL_WIDTH);
149+
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
150+
this.scale = canvasWidth / width;
161151

162-
const { style } = this.div;
163-
style.setProperty("--thumbnail-width", `${this.canvasWidth}px`);
164-
style.setProperty("--thumbnail-height", `${this.canvasHeight}px`);
152+
this.div.style.height = `${canvasHeight}px`;
165153
}
166154

167155
setPdfPage(pdfPage) {
@@ -175,14 +163,16 @@ class PDFThumbnailView {
175163
reset() {
176164
this.cancelRendering();
177165
this.renderingState = RenderingStates.INITIAL;
178-
179-
this.div.removeAttribute("data-loaded");
180-
this.image?.replaceWith(this._placeholderImg);
181166
this.#updateDims();
182167

183-
if (this.image) {
184-
this.image.removeAttribute("src");
185-
delete this.image;
168+
const { image } = this;
169+
const url = image.src;
170+
if (url) {
171+
URL.revokeObjectURL(url);
172+
image.removeAttribute("data-l10n-id");
173+
image.removeAttribute("data-l10n-args");
174+
image.src = "";
175+
this.div.classList.add("missingThumbnailImage");
186176
}
187177
}
188178

@@ -213,7 +203,6 @@ class PDFThumbnailView {
213203
#getPageDrawContext(upscaleFactor = 1) {
214204
// Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
215205
// until rendering/image conversion is complete, to avoid display issues.
216-
const canvas = document.createElement("canvas");
217206
const outputScale = new OutputScale();
218207
const width = upscaleFactor * this.canvasWidth,
219208
height = upscaleFactor * this.canvasHeight;
@@ -224,6 +213,9 @@ class PDFThumbnailView {
224213
this.maxCanvasPixels,
225214
this.maxCanvasDim
226215
);
216+
// Because of: https://bugzilla.mozilla.org/show_bug.cgi?id=2003060
217+
// we need use a HTMLCanvasElement here.
218+
const canvas = document.createElement("canvas");
227219
canvas.width = (width * outputScale.sx) | 0;
228220
canvas.height = (height * outputScale.sy) | 0;
229221

@@ -234,23 +226,23 @@ class PDFThumbnailView {
234226
return { canvas, transform };
235227
}
236228

237-
#convertCanvasToImage(canvas) {
229+
async #convertCanvasToImage(canvas) {
238230
if (this.renderingState !== RenderingStates.FINISHED) {
239231
throw new Error("#convertCanvasToImage: Rendering has not finished.");
240232
}
241233
const reducedCanvas = this.#reduceImage(canvas);
242-
243-
const image = document.createElement("img");
244-
image.className = "thumbnailImage";
234+
const { image } = this;
235+
const { promise, resolve } = Promise.withResolvers();
236+
reducedCanvas.toBlob(resolve);
237+
const blob = await promise;
238+
image.src = URL.createObjectURL(blob);
245239
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
246240
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
247-
image.src = reducedCanvas.toDataURL();
248-
this.image = image;
249-
250-
this.div.setAttribute("data-loaded", true);
251-
this._placeholderImg.replaceWith(image);
252-
253-
zeroCanvas(reducedCanvas);
241+
this.div.classList.remove("missingThumbnailImage");
242+
if (!FeatureTest.isOffscreenCanvasSupported) {
243+
// Clean up the canvas element since it is no longer needed.
244+
reducedCanvas.width = reducedCanvas.height = 0;
245+
}
254246
}
255247

256248
async draw() {
@@ -303,7 +295,6 @@ class PDFThumbnailView {
303295
await renderTask.promise;
304296
} catch (e) {
305297
if (e instanceof RenderingCancelledException) {
306-
zeroCanvas(canvas);
307298
return;
308299
}
309300
error = e;
@@ -317,8 +308,7 @@ class PDFThumbnailView {
317308
}
318309
this.renderingState = RenderingStates.FINISHED;
319310

320-
this.#convertCanvasToImage(canvas);
321-
zeroCanvas(canvas);
311+
await this.#convertCanvasToImage(canvas);
322312

323313
this.eventBus.dispatch("thumbnailrendered", {
324314
source: this,
@@ -449,14 +439,9 @@ class PDFThumbnailView {
449439
*/
450440
setPageLabel(label) {
451441
this.pageLabel = typeof label === "string" ? label : null;
452-
453442
this.anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
454-
455-
if (this.renderingState !== RenderingStates.FINISHED) {
456-
return;
457-
}
458-
this.image?.setAttribute("data-l10n-args", this.#pageL10nArgs);
443+
this.image.setAttribute("data-l10n-args", this.#pageL10nArgs);
459444
}
460445
}
461446

462-
export { PDFThumbnailView, TempImageFactory };
447+
export { PDFThumbnailView };

web/pdf_thumbnail_viewer.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
scrollIntoView,
2828
watchScroll,
2929
} from "./ui_utils.js";
30-
import { PDFThumbnailView, TempImageFactory } from "./pdf_thumbnail_view.js";
30+
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
3131

3232
const THUMBNAIL_SCROLL_MARGIN = -19;
3333
const THUMBNAIL_SELECTED_CLASS = "selected";
@@ -174,7 +174,6 @@ class PDFThumbnailViewer {
174174
thumbnail.reset();
175175
}
176176
}
177-
TempImageFactory.destroyCanvas();
178177
}
179178

180179
#resetView() {
@@ -209,10 +208,11 @@ class PDFThumbnailViewer {
209208
.then(firstPdfPage => {
210209
const pagesCount = pdfDocument.numPages;
211210
const viewport = firstPdfPage.getViewport({ scale: 1 });
211+
const fragment = document.createDocumentFragment();
212212

213213
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
214214
const thumbnail = new PDFThumbnailView({
215-
container: this.container,
215+
container: fragment,
216216
eventBus: this.eventBus,
217217
id: pageNum,
218218
defaultViewport: viewport.clone(),
@@ -234,6 +234,7 @@ class PDFThumbnailViewer {
234234
// Ensure that the current thumbnail is always highlighted on load.
235235
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
236236
thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
237+
this.container.append(fragment);
237238
})
238239
.catch(reason => {
239240
console.error("Unable to initialize thumbnail viewer", reason);

web/viewer.css

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -717,61 +717,63 @@ body {
717717
}
718718

719719
#thumbnailView {
720+
--thumbnail-width: 98px;
721+
722+
display: flex;
723+
flex-wrap: wrap;
720724
width: calc(100% - 60px);
721725
padding: 10px 30px 0;
722-
}
723726

724-
#thumbnailView > a:is(:active, :focus) {
725-
outline: 0;
726-
}
727+
> a {
728+
width: auto;
729+
height: auto;
727730

728-
.thumbnail {
729-
/* Define these variables here, and not in :root, since the individual
730-
thumbnails may have different sizes. */
731-
--thumbnail-width: 0;
732-
--thumbnail-height: 0;
731+
> .thumbnail {
732+
width: var(--thumbnail-width);
733+
margin: 0 10px 5px;
734+
padding: 1px;
735+
border: 7px solid transparent;
736+
border-radius: 2px;
733737

734-
float: var(--inline-start);
735-
width: var(--thumbnail-width);
736-
height: var(--thumbnail-height);
737-
margin: 0 10px 5px;
738-
padding: 1px;
739-
border: 7px solid transparent;
740-
border-radius: 2px;
741-
}
738+
&.selected {
739+
border-color: var(--thumbnail-selected-color) !important;
742740

743-
#thumbnailView > a:last-of-type > .thumbnail {
744-
margin-bottom: 10px;
745-
}
741+
> .thumbnailImage {
742+
opacity: 1 !important;
743+
}
744+
}
746745

747-
a:focus > .thumbnail,
748-
.thumbnail:hover {
749-
border-color: var(--thumbnail-hover-color);
750-
}
746+
&.missingThumbnailImage {
747+
border: 1px dashed rgb(132 132 132);
748+
padding: 7px;
749+
> .thumbnailImage {
750+
display: none;
751+
}
752+
}
751753

752-
.thumbnail.selected {
753-
border-color: var(--thumbnail-selected-color) !important;
754-
}
754+
> .thumbnailImage {
755+
width: 100%;
756+
opacity: 0.9;
757+
}
758+
}
755759

756-
.thumbnailImage {
757-
width: var(--thumbnail-width);
758-
height: var(--thumbnail-height);
759-
opacity: 0.9;
760-
}
760+
&:is(:active, :focus) {
761+
outline: 0;
762+
}
761763

762-
a:focus > .thumbnail > .thumbnailImage,
763-
.thumbnail:hover > .thumbnailImage {
764-
opacity: 0.95;
765-
}
764+
&:last-of-type > .thumbnail {
765+
margin-bottom: 10px;
766+
}
766767

767-
.thumbnail.selected > .thumbnailImage {
768-
opacity: 1 !important;
769-
}
768+
&:focus > .thumbnail,
769+
.thumbnail:hover {
770+
border-color: var(--thumbnail-hover-color);
770771

771-
.thumbnail:not([data-loaded]) > .thumbnailImage {
772-
width: calc(var(--thumbnail-width) - 2px);
773-
height: calc(var(--thumbnail-height) - 2px);
774-
border: 1px dashed rgb(132 132 132);
772+
> .thumbnailImage {
773+
opacity: 0.95;
774+
}
775+
}
776+
}
775777
}
776778

777779
.treeWithDeepNesting > .treeItem,

0 commit comments

Comments
 (0)