/** * @licstart The following is the entire license notice for the * JavaScript code in this page * * Copyright 2022 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @licend The above is the entire license notice for the * JavaScript code in this page */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TempImageFactory = exports.PDFThumbnailView = void 0; var _ui_utils = require("./ui_utils.js"); var _pdf = require("../pdf"); const DRAW_UPSCALE_FACTOR = 2; const MAX_NUM_SCALING_STEPS = 3; const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; const THUMBNAIL_WIDTH = 98; class TempImageFactory { static #tempCanvas = null; static getCanvas(width, height) { const tempCanvas = this.#tempCanvas ||= document.createElement("canvas"); tempCanvas.width = width; tempCanvas.height = height; const ctx = tempCanvas.getContext("2d", { alpha: false }); ctx.save(); ctx.fillStyle = "rgb(255, 255, 255)"; ctx.fillRect(0, 0, width, height); ctx.restore(); return [tempCanvas, tempCanvas.getContext("2d")]; } static destroyCanvas() { const tempCanvas = this.#tempCanvas; if (tempCanvas) { tempCanvas.width = 0; tempCanvas.height = 0; } this.#tempCanvas = null; } } exports.TempImageFactory = TempImageFactory; class PDFThumbnailView { constructor({ container, id, defaultViewport, optionalContentConfigPromise, linkService, renderingQueue, l10n, pageColors }) { this.id = id; this.renderingId = "thumbnail" + id; this.pageLabel = null; this.pdfPage = null; this.rotation = 0; this.viewport = defaultViewport; this.pdfPageRotate = defaultViewport.rotation; this._optionalContentConfigPromise = optionalContentConfigPromise || null; this.pageColors = pageColors || null; this.linkService = linkService; this.renderingQueue = renderingQueue; this.renderTask = null; this.renderingState = _ui_utils.RenderingStates.INITIAL; this.resume = null; const pageWidth = this.viewport.width, pageHeight = this.viewport.height, pageRatio = pageWidth / pageHeight; this.canvasWidth = THUMBNAIL_WIDTH; this.canvasHeight = this.canvasWidth / pageRatio | 0; this.scale = this.canvasWidth / pageWidth; this.l10n = l10n; const anchor = document.createElement("a"); anchor.href = linkService.getAnchorUrl("#page=" + id); this._thumbPageTitle.then(msg => { anchor.title = msg; }); anchor.onclick = function () { linkService.goToPage(id); return false; }; this.anchor = anchor; const div = document.createElement("div"); div.className = "thumbnail"; div.setAttribute("data-page-number", this.id); this.div = div; const ring = document.createElement("div"); ring.className = "thumbnailSelectionRing"; const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; ring.style.width = this.canvasWidth + borderAdjustment + "px"; ring.style.height = this.canvasHeight + borderAdjustment + "px"; this.ring = ring; div.append(ring); anchor.append(div); container.append(anchor); } setPdfPage(pdfPage) { this.pdfPage = pdfPage; this.pdfPageRotate = pdfPage.rotate; const totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = pdfPage.getViewport({ scale: 1, rotation: totalRotation }); this.reset(); } reset() { this.cancelRendering(); this.renderingState = _ui_utils.RenderingStates.INITIAL; const pageWidth = this.viewport.width, pageHeight = this.viewport.height, pageRatio = pageWidth / pageHeight; this.canvasHeight = this.canvasWidth / pageRatio | 0; this.scale = this.canvasWidth / pageWidth; this.div.removeAttribute("data-loaded"); const ring = this.ring; ring.textContent = ""; const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; ring.style.width = this.canvasWidth + borderAdjustment + "px"; ring.style.height = this.canvasHeight + borderAdjustment + "px"; if (this.canvas) { this.canvas.width = 0; this.canvas.height = 0; delete this.canvas; } if (this.image) { this.image.removeAttribute("src"); delete this.image; } } update({ rotation = null }) { if (typeof rotation === "number") { this.rotation = rotation; } const totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = this.viewport.clone({ scale: 1, rotation: totalRotation }); this.reset(); } cancelRendering() { if (this.renderTask) { this.renderTask.cancel(); this.renderTask = null; } this.resume = null; } _getPageDrawContext(upscaleFactor = 1) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d", { alpha: false }); const outputScale = new _ui_utils.OutputScale(); canvas.width = upscaleFactor * this.canvasWidth * outputScale.sx | 0; canvas.height = upscaleFactor * this.canvasHeight * outputScale.sy | 0; const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null; return { ctx, canvas, transform }; } _convertCanvasToImage(canvas) { if (this.renderingState !== _ui_utils.RenderingStates.FINISHED) { throw new Error("_convertCanvasToImage: Rendering has not finished."); } const reducedCanvas = this._reduceImage(canvas); const image = document.createElement("img"); image.className = "thumbnailImage"; this._thumbPageCanvas.then(msg => { image.setAttribute("aria-label", msg); }); image.style.width = this.canvasWidth + "px"; image.style.height = this.canvasHeight + "px"; image.src = reducedCanvas.toDataURL(); this.image = image; this.div.setAttribute("data-loaded", true); this.ring.append(image); reducedCanvas.width = 0; reducedCanvas.height = 0; } draw() { if (this.renderingState !== _ui_utils.RenderingStates.INITIAL) { console.error("Must be in new state before drawing"); return Promise.resolve(); } const { pdfPage } = this; if (!pdfPage) { this.renderingState = _ui_utils.RenderingStates.FINISHED; return Promise.reject(new Error("pdfPage is not loaded")); } this.renderingState = _ui_utils.RenderingStates.RUNNING; const finishRenderTask = async (error = null) => { if (renderTask === this.renderTask) { this.renderTask = null; } if (error instanceof _pdf.RenderingCancelledException) { return; } this.renderingState = _ui_utils.RenderingStates.FINISHED; this._convertCanvasToImage(canvas); if (error) { throw error; } }; const { ctx, canvas, transform } = this._getPageDrawContext(DRAW_UPSCALE_FACTOR); const drawViewport = this.viewport.clone({ scale: DRAW_UPSCALE_FACTOR * this.scale }); const renderContinueCallback = cont => { if (!this.renderingQueue.isHighestPriority(this)) { this.renderingState = _ui_utils.RenderingStates.PAUSED; this.resume = () => { this.renderingState = _ui_utils.RenderingStates.RUNNING; cont(); }; return; } cont(); }; const renderContext = { canvasContext: ctx, transform, viewport: drawViewport, optionalContentConfigPromise: this._optionalContentConfigPromise, pageColors: this.pageColors }; const renderTask = this.renderTask = pdfPage.render(renderContext); renderTask.onContinue = renderContinueCallback; const resultPromise = renderTask.promise.then(function () { return finishRenderTask(null); }, function (error) { return finishRenderTask(error); }); resultPromise.finally(() => { canvas.width = 0; canvas.height = 0; const pageCached = this.linkService.isPageCached(this.id); if (!pageCached) { this.pdfPage?.cleanup(); } }); return resultPromise; } setImage(pageView) { if (this.renderingState !== _ui_utils.RenderingStates.INITIAL) { return; } const { thumbnailCanvas: canvas, pdfPage, scale } = pageView; if (!canvas) { return; } if (!this.pdfPage) { this.setPdfPage(pdfPage); } if (scale < this.scale) { return; } this.renderingState = _ui_utils.RenderingStates.FINISHED; this._convertCanvasToImage(canvas); } _reduceImage(img) { const { ctx, canvas } = this._getPageDrawContext(); if (img.width <= 2 * canvas.width) { ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); return canvas; } let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas(reducedWidth, reducedHeight); while (reducedWidth > img.width || reducedHeight > img.height) { reducedWidth >>= 1; reducedHeight >>= 1; } reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, 0, 0, reducedWidth, reducedHeight); while (reducedWidth > 2 * canvas.width) { reducedImageCtx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, 0, 0, reducedWidth >> 1, reducedHeight >> 1); reducedWidth >>= 1; reducedHeight >>= 1; } ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, 0, 0, canvas.width, canvas.height); return canvas; } get _thumbPageTitle() { return this.l10n.get("thumb_page_title", { page: this.pageLabel ?? this.id }); } get _thumbPageCanvas() { return this.l10n.get("thumb_page_canvas", { page: this.pageLabel ?? this.id }); } setPageLabel(label) { this.pageLabel = typeof label === "string" ? label : null; this._thumbPageTitle.then(msg => { this.anchor.title = msg; }); if (this.renderingState !== _ui_utils.RenderingStates.FINISHED) { return; } this._thumbPageCanvas.then(msg => { this.image?.setAttribute("aria-label", msg); }); } } exports.PDFThumbnailView = PDFThumbnailView;