Skip to content

Commit fb4980a

Browse files
committed
Slightly reduce the memory used by thumbnails
In using a blob instead of a base64 string it's possible to reduce the memory. And simplify a bit the thumbnails themselves.
1 parent ec71e4e commit fb4980a

File tree

3 files changed

+84
-113
lines changed

3 files changed

+84
-113
lines changed

web/pdf_thumbnail_view.js

Lines changed: 35 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
3131
const MAX_NUM_SCALING_STEPS = 3;
3232
const THUMBNAIL_WIDTH = 98; // px
3333

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-
4134
/**
4235
* @typedef {Object} PDFThumbnailViewOptions
4336
* @property {HTMLDivElement} container - The viewer element.
@@ -61,12 +54,8 @@ function zeroCanvas(c) {
6154
*/
6255

6356
class TempImageFactory {
64-
static #tempCanvas = null;
65-
6657
static getCanvas(width, height) {
67-
const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas"));
68-
tempCanvas.width = width;
69-
tempCanvas.height = height;
58+
const tempCanvas = new OffscreenCanvas(width, height);
7059

7160
// Since this is a temporary canvas, we need to fill it with a white
7261
// background ourselves. `#getPageDrawContext` uses CSS rules for this.
@@ -75,14 +64,7 @@ class TempImageFactory {
7564
ctx.fillStyle = "rgb(255, 255, 255)";
7665
ctx.fillRect(0, 0, width, height);
7766
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;
67+
return [tempCanvas, ctx];
8668
}
8769
}
8870

@@ -126,27 +108,24 @@ class PDFThumbnailView {
126108
this.renderingState = RenderingStates.INITIAL;
127109
this.resume = null;
128110

129-
const anchor = document.createElement("a");
130-
anchor.href = linkService.getAnchorUrl("#page=" + id);
111+
const anchor = (this.anchor = document.createElement("a"));
112+
anchor.href = linkService.getAnchorUrl(`#page=${id}`);
131113
anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
132114
anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
133-
anchor.onclick = function () {
115+
anchor.onclick = () => {
134116
linkService.goToPage(id);
135117
return false;
136118
};
137-
this.anchor = anchor;
138119

139-
const div = document.createElement("div");
140-
div.className = "thumbnail";
120+
const div = (this.div = document.createElement("div"));
121+
div.classList.add("thumbnail", "missingThumbnailImage");
141122
div.setAttribute("data-page-number", this.id);
142-
this.div = div;
143123
this.#updateDims();
144124

145-
const img = document.createElement("div");
146-
img.className = "thumbnailImage";
147-
this._placeholderImg = img;
125+
const image = (this.image = document.createElement("img"));
126+
image.className = "thumbnailImage";
148127

149-
div.append(img);
128+
div.append(image);
150129
anchor.append(div);
151130
container.append(anchor);
152131
}
@@ -155,13 +134,11 @@ class PDFThumbnailView {
155134
const { width, height } = this.viewport;
156135
const ratio = width / height;
157136

158-
this.canvasWidth = THUMBNAIL_WIDTH;
159-
this.canvasHeight = (this.canvasWidth / ratio) | 0;
160-
this.scale = this.canvasWidth / width;
137+
const canvasWidth = (this.canvasWidth = THUMBNAIL_WIDTH);
138+
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
139+
this.scale = canvasWidth / width;
161140

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

167144
setPdfPage(pdfPage) {
@@ -175,14 +152,16 @@ class PDFThumbnailView {
175152
reset() {
176153
this.cancelRendering();
177154
this.renderingState = RenderingStates.INITIAL;
178-
179-
this.div.removeAttribute("data-loaded");
180-
this.image?.replaceWith(this._placeholderImg);
181155
this.#updateDims();
182156

183-
if (this.image) {
184-
this.image.removeAttribute("src");
185-
delete this.image;
157+
const { image } = this;
158+
const url = image.src;
159+
if (url) {
160+
URL.revokeObjectURL(url);
161+
image.removeAttribute("data-l10n-id");
162+
image.removeAttribute("data-l10n-args");
163+
image.src = "";
164+
this.div.classList.add("missingThumbnailImage");
186165
}
187166
}
188167

@@ -213,7 +192,6 @@ class PDFThumbnailView {
213192
#getPageDrawContext(upscaleFactor = 1) {
214193
// Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
215194
// until rendering/image conversion is complete, to avoid display issues.
216-
const canvas = document.createElement("canvas");
217195
const outputScale = new OutputScale();
218196
const width = upscaleFactor * this.canvasWidth,
219197
height = upscaleFactor * this.canvasHeight;
@@ -224,8 +202,10 @@ class PDFThumbnailView {
224202
this.maxCanvasPixels,
225203
this.maxCanvasDim
226204
);
227-
canvas.width = (width * outputScale.sx) | 0;
228-
canvas.height = (height * outputScale.sy) | 0;
205+
const canvas = new OffscreenCanvas(
206+
(width * outputScale.sx) | 0,
207+
(height * outputScale.sy) | 0
208+
);
229209

230210
const transform = outputScale.scaled
231211
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
@@ -239,18 +219,13 @@ class PDFThumbnailView {
239219
throw new Error("#convertCanvasToImage: Rendering has not finished.");
240220
}
241221
const reducedCanvas = this.#reduceImage(canvas);
242-
243-
const image = document.createElement("img");
244-
image.className = "thumbnailImage";
245-
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
246-
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);
222+
reducedCanvas.convertToBlob().then(blob => {
223+
const { image } = this;
224+
image.src = URL.createObjectURL(blob);
225+
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
226+
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
227+
this.div.classList.remove("missingThumbnailImage");
228+
});
254229
}
255230

256231
async draw() {
@@ -303,7 +278,6 @@ class PDFThumbnailView {
303278
await renderTask.promise;
304279
} catch (e) {
305280
if (e instanceof RenderingCancelledException) {
306-
zeroCanvas(canvas);
307281
return;
308282
}
309283
error = e;
@@ -318,7 +292,6 @@ class PDFThumbnailView {
318292
this.renderingState = RenderingStates.FINISHED;
319293

320294
this.#convertCanvasToImage(canvas);
321-
zeroCanvas(canvas);
322295

323296
this.eventBus.dispatch("thumbnailrendered", {
324297
source: this,
@@ -449,14 +422,9 @@ class PDFThumbnailView {
449422
*/
450423
setPageLabel(label) {
451424
this.pageLabel = typeof label === "string" ? label : null;
452-
453425
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);
426+
this.image.setAttribute("data-l10n-args", this.#pageL10nArgs);
459427
}
460428
}
461429

462-
export { PDFThumbnailView, TempImageFactory };
430+
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)