UNPKG

10.9 kBJavaScriptView Raw
1/**
2 * @licstart The following is the entire license notice for the
3 * JavaScript code in this page
4 *
5 * Copyright 2022 Mozilla Foundation
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * @licend The above is the entire license notice for the
20 * JavaScript code in this page
21 */
22"use strict";
23
24Object.defineProperty(exports, "__esModule", {
25 value: true
26});
27exports.TempImageFactory = exports.PDFThumbnailView = void 0;
28
29var _ui_utils = require("./ui_utils.js");
30
31var _pdf = require("../pdf");
32
33const DRAW_UPSCALE_FACTOR = 2;
34const MAX_NUM_SCALING_STEPS = 3;
35const THUMBNAIL_CANVAS_BORDER_WIDTH = 1;
36const THUMBNAIL_WIDTH = 98;
37
38class TempImageFactory {
39 static #tempCanvas = null;
40
41 static getCanvas(width, height) {
42 const tempCanvas = this.#tempCanvas ||= document.createElement("canvas");
43 tempCanvas.width = width;
44 tempCanvas.height = height;
45 const ctx = tempCanvas.getContext("2d", {
46 alpha: false
47 });
48 ctx.save();
49 ctx.fillStyle = "rgb(255, 255, 255)";
50 ctx.fillRect(0, 0, width, height);
51 ctx.restore();
52 return [tempCanvas, tempCanvas.getContext("2d")];
53 }
54
55 static destroyCanvas() {
56 const tempCanvas = this.#tempCanvas;
57
58 if (tempCanvas) {
59 tempCanvas.width = 0;
60 tempCanvas.height = 0;
61 }
62
63 this.#tempCanvas = null;
64 }
65
66}
67
68exports.TempImageFactory = TempImageFactory;
69
70class PDFThumbnailView {
71 constructor({
72 container,
73 id,
74 defaultViewport,
75 optionalContentConfigPromise,
76 linkService,
77 renderingQueue,
78 l10n,
79 pageColors
80 }) {
81 this.id = id;
82 this.renderingId = "thumbnail" + id;
83 this.pageLabel = null;
84 this.pdfPage = null;
85 this.rotation = 0;
86 this.viewport = defaultViewport;
87 this.pdfPageRotate = defaultViewport.rotation;
88 this._optionalContentConfigPromise = optionalContentConfigPromise || null;
89 this.pageColors = pageColors || null;
90 this.linkService = linkService;
91 this.renderingQueue = renderingQueue;
92 this.renderTask = null;
93 this.renderingState = _ui_utils.RenderingStates.INITIAL;
94 this.resume = null;
95 const pageWidth = this.viewport.width,
96 pageHeight = this.viewport.height,
97 pageRatio = pageWidth / pageHeight;
98 this.canvasWidth = THUMBNAIL_WIDTH;
99 this.canvasHeight = this.canvasWidth / pageRatio | 0;
100 this.scale = this.canvasWidth / pageWidth;
101 this.l10n = l10n;
102 const anchor = document.createElement("a");
103 anchor.href = linkService.getAnchorUrl("#page=" + id);
104
105 this._thumbPageTitle.then(msg => {
106 anchor.title = msg;
107 });
108
109 anchor.onclick = function () {
110 linkService.goToPage(id);
111 return false;
112 };
113
114 this.anchor = anchor;
115 const div = document.createElement("div");
116 div.className = "thumbnail";
117 div.setAttribute("data-page-number", this.id);
118 this.div = div;
119 const ring = document.createElement("div");
120 ring.className = "thumbnailSelectionRing";
121 const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
122 ring.style.width = this.canvasWidth + borderAdjustment + "px";
123 ring.style.height = this.canvasHeight + borderAdjustment + "px";
124 this.ring = ring;
125 div.append(ring);
126 anchor.append(div);
127 container.append(anchor);
128 }
129
130 setPdfPage(pdfPage) {
131 this.pdfPage = pdfPage;
132 this.pdfPageRotate = pdfPage.rotate;
133 const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
134 this.viewport = pdfPage.getViewport({
135 scale: 1,
136 rotation: totalRotation
137 });
138 this.reset();
139 }
140
141 reset() {
142 this.cancelRendering();
143 this.renderingState = _ui_utils.RenderingStates.INITIAL;
144 const pageWidth = this.viewport.width,
145 pageHeight = this.viewport.height,
146 pageRatio = pageWidth / pageHeight;
147 this.canvasHeight = this.canvasWidth / pageRatio | 0;
148 this.scale = this.canvasWidth / pageWidth;
149 this.div.removeAttribute("data-loaded");
150 const ring = this.ring;
151 ring.textContent = "";
152 const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
153 ring.style.width = this.canvasWidth + borderAdjustment + "px";
154 ring.style.height = this.canvasHeight + borderAdjustment + "px";
155
156 if (this.canvas) {
157 this.canvas.width = 0;
158 this.canvas.height = 0;
159 delete this.canvas;
160 }
161
162 if (this.image) {
163 this.image.removeAttribute("src");
164 delete this.image;
165 }
166 }
167
168 update({
169 rotation = null
170 }) {
171 if (typeof rotation === "number") {
172 this.rotation = rotation;
173 }
174
175 const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
176 this.viewport = this.viewport.clone({
177 scale: 1,
178 rotation: totalRotation
179 });
180 this.reset();
181 }
182
183 cancelRendering() {
184 if (this.renderTask) {
185 this.renderTask.cancel();
186 this.renderTask = null;
187 }
188
189 this.resume = null;
190 }
191
192 _getPageDrawContext(upscaleFactor = 1) {
193 const canvas = document.createElement("canvas");
194 const ctx = canvas.getContext("2d", {
195 alpha: false
196 });
197 const outputScale = new _ui_utils.OutputScale();
198 canvas.width = upscaleFactor * this.canvasWidth * outputScale.sx | 0;
199 canvas.height = upscaleFactor * this.canvasHeight * outputScale.sy | 0;
200 const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null;
201 return {
202 ctx,
203 canvas,
204 transform
205 };
206 }
207
208 _convertCanvasToImage(canvas) {
209 if (this.renderingState !== _ui_utils.RenderingStates.FINISHED) {
210 throw new Error("_convertCanvasToImage: Rendering has not finished.");
211 }
212
213 const reducedCanvas = this._reduceImage(canvas);
214
215 const image = document.createElement("img");
216 image.className = "thumbnailImage";
217
218 this._thumbPageCanvas.then(msg => {
219 image.setAttribute("aria-label", msg);
220 });
221
222 image.style.width = this.canvasWidth + "px";
223 image.style.height = this.canvasHeight + "px";
224 image.src = reducedCanvas.toDataURL();
225 this.image = image;
226 this.div.setAttribute("data-loaded", true);
227 this.ring.append(image);
228 reducedCanvas.width = 0;
229 reducedCanvas.height = 0;
230 }
231
232 draw() {
233 if (this.renderingState !== _ui_utils.RenderingStates.INITIAL) {
234 console.error("Must be in new state before drawing");
235 return Promise.resolve();
236 }
237
238 const {
239 pdfPage
240 } = this;
241
242 if (!pdfPage) {
243 this.renderingState = _ui_utils.RenderingStates.FINISHED;
244 return Promise.reject(new Error("pdfPage is not loaded"));
245 }
246
247 this.renderingState = _ui_utils.RenderingStates.RUNNING;
248
249 const finishRenderTask = async (error = null) => {
250 if (renderTask === this.renderTask) {
251 this.renderTask = null;
252 }
253
254 if (error instanceof _pdf.RenderingCancelledException) {
255 return;
256 }
257
258 this.renderingState = _ui_utils.RenderingStates.FINISHED;
259
260 this._convertCanvasToImage(canvas);
261
262 if (error) {
263 throw error;
264 }
265 };
266
267 const {
268 ctx,
269 canvas,
270 transform
271 } = this._getPageDrawContext(DRAW_UPSCALE_FACTOR);
272
273 const drawViewport = this.viewport.clone({
274 scale: DRAW_UPSCALE_FACTOR * this.scale
275 });
276
277 const renderContinueCallback = cont => {
278 if (!this.renderingQueue.isHighestPriority(this)) {
279 this.renderingState = _ui_utils.RenderingStates.PAUSED;
280
281 this.resume = () => {
282 this.renderingState = _ui_utils.RenderingStates.RUNNING;
283 cont();
284 };
285
286 return;
287 }
288
289 cont();
290 };
291
292 const renderContext = {
293 canvasContext: ctx,
294 transform,
295 viewport: drawViewport,
296 optionalContentConfigPromise: this._optionalContentConfigPromise,
297 pageColors: this.pageColors
298 };
299 const renderTask = this.renderTask = pdfPage.render(renderContext);
300 renderTask.onContinue = renderContinueCallback;
301 const resultPromise = renderTask.promise.then(function () {
302 return finishRenderTask(null);
303 }, function (error) {
304 return finishRenderTask(error);
305 });
306 resultPromise.finally(() => {
307 canvas.width = 0;
308 canvas.height = 0;
309 const pageCached = this.linkService.isPageCached(this.id);
310
311 if (!pageCached) {
312 this.pdfPage?.cleanup();
313 }
314 });
315 return resultPromise;
316 }
317
318 setImage(pageView) {
319 if (this.renderingState !== _ui_utils.RenderingStates.INITIAL) {
320 return;
321 }
322
323 const {
324 thumbnailCanvas: canvas,
325 pdfPage
326 } = pageView;
327
328 if (!canvas) {
329 return;
330 }
331
332 if (!this.pdfPage) {
333 this.setPdfPage(pdfPage);
334 }
335
336 this.renderingState = _ui_utils.RenderingStates.FINISHED;
337
338 this._convertCanvasToImage(canvas);
339 }
340
341 _reduceImage(img) {
342 const {
343 ctx,
344 canvas
345 } = this._getPageDrawContext();
346
347 if (img.width <= 2 * canvas.width) {
348 ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
349 return canvas;
350 }
351
352 let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
353 let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
354 const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas(reducedWidth, reducedHeight);
355
356 while (reducedWidth > img.width || reducedHeight > img.height) {
357 reducedWidth >>= 1;
358 reducedHeight >>= 1;
359 }
360
361 reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, 0, 0, reducedWidth, reducedHeight);
362
363 while (reducedWidth > 2 * canvas.width) {
364 reducedImageCtx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, 0, 0, reducedWidth >> 1, reducedHeight >> 1);
365 reducedWidth >>= 1;
366 reducedHeight >>= 1;
367 }
368
369 ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, 0, 0, canvas.width, canvas.height);
370 return canvas;
371 }
372
373 get _thumbPageTitle() {
374 return this.l10n.get("thumb_page_title", {
375 page: this.pageLabel ?? this.id
376 });
377 }
378
379 get _thumbPageCanvas() {
380 return this.l10n.get("thumb_page_canvas", {
381 page: this.pageLabel ?? this.id
382 });
383 }
384
385 setPageLabel(label) {
386 this.pageLabel = typeof label === "string" ? label : null;
387
388 this._thumbPageTitle.then(msg => {
389 this.anchor.title = msg;
390 });
391
392 if (this.renderingState !== _ui_utils.RenderingStates.FINISHED) {
393 return;
394 }
395
396 this._thumbPageCanvas.then(msg => {
397 this.image?.setAttribute("aria-label", msg);
398 });
399 }
400
401}
402
403exports.PDFThumbnailView = PDFThumbnailView;
\No newline at end of file