/* Copyright 2012 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. */ 'use strict'; (function (root, factory) { if (typeof define === 'function' && define.amd) { define('pdfjs-web/pdf_thumbnail_view', ['exports', 'pdfjs-web/ui_utils', 'pdfjs-web/pdf_rendering_queue'], factory); } else if (typeof exports !== 'undefined') { factory(exports, require('./ui_utils.js'), require('./pdf_rendering_queue.js')); } else { factory((root.pdfjsWebPDFThumbnailView = {}), root.pdfjsWebUIUtils, root.pdfjsWebPDFRenderingQueue); } }(this, function (exports, uiUtils, pdfRenderingQueue) { var mozL10n = uiUtils.mozL10n; var getOutputScale = uiUtils.getOutputScale; var RenderingStates = pdfRenderingQueue.RenderingStates; var THUMBNAIL_WIDTH = 98; // px var THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px /** * @typedef {Object} PDFThumbnailViewOptions * @property {HTMLDivElement} container - The viewer element. * @property {number} id - The thumbnail's unique ID (normally its number). * @property {PageViewport} defaultViewport - The page viewport. * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {boolean} disableCanvasToImageConversion - (optional) Don't convert * the canvas thumbnails to images. This prevents `toDataURL` calls, * but increases the overall memory usage. The default value is false. */ /** * @class * @implements {IRenderableView} */ var PDFThumbnailView = (function PDFThumbnailViewClosure() { function getTempCanvas(width, height) { var tempCanvas = PDFThumbnailView.tempImageCache; if (!tempCanvas) { tempCanvas = document.createElement('canvas'); PDFThumbnailView.tempImageCache = tempCanvas; } tempCanvas.width = width; tempCanvas.height = height; // Since this is a temporary canvas, we need to fill the canvas with a white // background ourselves. `_getPageDrawContext` uses CSS rules for this. if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('MOZCENTRAL || FIREFOX || GENERIC')) { tempCanvas.mozOpaque = true; } var ctx = tempCanvas.getContext('2d', {alpha: false}); ctx.save(); ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.fillRect(0, 0, width, height); ctx.restore(); return tempCanvas; } /** * @constructs PDFThumbnailView * @param {PDFThumbnailViewOptions} options */ function PDFThumbnailView(options) { var container = options.container; var id = options.id; var defaultViewport = options.defaultViewport; var linkService = options.linkService; var renderingQueue = options.renderingQueue; var disableCanvasToImageConversion = options.disableCanvasToImageConversion || false; this.id = id; this.renderingId = 'thumbnail' + id; this.pageLabel = null; this.pdfPage = null; this.rotation = 0; this.viewport = defaultViewport; this.pdfPageRotate = defaultViewport.rotation; this.linkService = linkService; this.renderingQueue = renderingQueue; this.renderTask = null; this.renderingState = RenderingStates.INITIAL; this.resume = null; this.disableCanvasToImageConversion = disableCanvasToImageConversion; this.pageWidth = this.viewport.width; this.pageHeight = this.viewport.height; this.pageRatio = this.pageWidth / this.pageHeight; this.canvasWidth = THUMBNAIL_WIDTH; this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; this.scale = this.canvasWidth / this.pageWidth; var anchor = document.createElement('a'); anchor.href = linkService.getAnchorUrl('#page=' + id); anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); anchor.onclick = function stopNavigation() { linkService.page = id; return false; }; this.anchor = anchor; var div = document.createElement('div'); div.className = 'thumbnail'; div.setAttribute('data-page-number', this.id); this.div = div; if (id === 1) { // Highlight the thumbnail of the first page when no page number is // specified (or exists in cache) when the document is loaded. div.classList.add('selected'); } var ring = document.createElement('div'); ring.className = 'thumbnailSelectionRing'; var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; ring.style.width = this.canvasWidth + borderAdjustment + 'px'; ring.style.height = this.canvasHeight + borderAdjustment + 'px'; this.ring = ring; div.appendChild(ring); anchor.appendChild(div); container.appendChild(anchor); } PDFThumbnailView.prototype = { setPdfPage: function PDFThumbnailView_setPdfPage(pdfPage) { this.pdfPage = pdfPage; this.pdfPageRotate = pdfPage.rotate; var totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = pdfPage.getViewport(1, totalRotation); this.reset(); }, reset: function PDFThumbnailView_reset() { this.cancelRendering(); this.pageWidth = this.viewport.width; this.pageHeight = this.viewport.height; this.pageRatio = this.pageWidth / this.pageHeight; this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; this.scale = (this.canvasWidth / this.pageWidth); this.div.removeAttribute('data-loaded'); var ring = this.ring; var childNodes = ring.childNodes; for (var i = childNodes.length - 1; i >= 0; i--) { ring.removeChild(childNodes[i]); } var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; ring.style.width = this.canvasWidth + borderAdjustment + 'px'; ring.style.height = this.canvasHeight + borderAdjustment + 'px'; if (this.canvas) { // Zeroing the width and height causes Firefox to release graphics // resources immediately, which can greatly reduce memory consumption. this.canvas.width = 0; this.canvas.height = 0; delete this.canvas; } if (this.image) { this.image.removeAttribute('src'); delete this.image; } }, update: function PDFThumbnailView_update(rotation) { if (typeof rotation !== 'undefined') { this.rotation = rotation; } var totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = this.viewport.clone({ scale: 1, rotation: totalRotation }); this.reset(); }, cancelRendering: function PDFThumbnailView_cancelRendering() { if (this.renderTask) { this.renderTask.cancel(); this.renderTask = null; } this.renderingState = RenderingStates.INITIAL; this.resume = null; }, /** * @private */ _getPageDrawContext: function PDFThumbnailView_getPageDrawContext(noCtxScale) { var canvas = document.createElement('canvas'); // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`, // until rendering/image conversion is complete, to avoid display issues. this.canvas = canvas; if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('MOZCENTRAL || FIREFOX || GENERIC')) { canvas.mozOpaque = true; } var ctx = canvas.getContext('2d', {alpha: false}); var outputScale = getOutputScale(ctx); canvas.width = (this.canvasWidth * outputScale.sx) | 0; canvas.height = (this.canvasHeight * outputScale.sy) | 0; canvas.style.width = this.canvasWidth + 'px'; canvas.style.height = this.canvasHeight + 'px'; if (!noCtxScale && outputScale.scaled) { ctx.scale(outputScale.sx, outputScale.sy); } return ctx; }, /** * @private */ _convertCanvasToImage: function PDFThumbnailView_convertCanvasToImage() { if (!this.canvas) { return; } if (this.renderingState !== RenderingStates.FINISHED) { return; } var id = this.renderingId; var className = 'thumbnailImage'; var ariaLabel = mozL10n.get('thumb_page_canvas', { page: this.pageId }, 'Thumbnail of Page {{page}}'); if (this.disableCanvasToImageConversion) { this.canvas.id = id; this.canvas.className = className; this.canvas.setAttribute('aria-label', ariaLabel); this.div.setAttribute('data-loaded', true); this.ring.appendChild(this.canvas); return; } var image = document.createElement('img'); image.id = id; image.className = className; image.setAttribute('aria-label', ariaLabel); image.style.width = this.canvasWidth + 'px'; image.style.height = this.canvasHeight + 'px'; image.src = this.canvas.toDataURL(); this.image = image; this.div.setAttribute('data-loaded', true); this.ring.appendChild(image); // Zeroing the width and height causes Firefox to release graphics // resources immediately, which can greatly reduce memory consumption. this.canvas.width = 0; this.canvas.height = 0; delete this.canvas; }, draw: function PDFThumbnailView_draw() { if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); return Promise.resolve(undefined); } this.renderingState = RenderingStates.RUNNING; var resolveRenderPromise, rejectRenderPromise; var promise = new Promise(function (resolve, reject) { resolveRenderPromise = resolve; rejectRenderPromise = reject; }); var self = this; function thumbnailDrawCallback(error) { // The renderTask may have been replaced by a new one, so only remove // the reference to the renderTask if it matches the one that is // triggering this callback. if (renderTask === self.renderTask) { self.renderTask = null; } if (error === 'cancelled') { rejectRenderPromise(error); return; } self.renderingState = RenderingStates.FINISHED; self._convertCanvasToImage(); if (!error) { resolveRenderPromise(undefined); } else { rejectRenderPromise(error); } } var ctx = this._getPageDrawContext(); var drawViewport = this.viewport.clone({ scale: this.scale }); var renderContinueCallback = function renderContinueCallback(cont) { if (!self.renderingQueue.isHighestPriority(self)) { self.renderingState = RenderingStates.PAUSED; self.resume = function resumeCallback() { self.renderingState = RenderingStates.RUNNING; cont(); }; return; } cont(); }; var renderContext = { canvasContext: ctx, viewport: drawViewport }; var renderTask = this.renderTask = this.pdfPage.render(renderContext); renderTask.onContinue = renderContinueCallback; renderTask.promise.then( function pdfPageRenderCallback() { thumbnailDrawCallback(null); }, function pdfPageRenderError(error) { thumbnailDrawCallback(error); } ); return promise; }, setImage: function PDFThumbnailView_setImage(pageView) { if (this.renderingState !== RenderingStates.INITIAL) { return; } var img = pageView.canvas; if (!img) { return; } if (!this.pdfPage) { this.setPdfPage(pageView.pdfPage); } this.renderingState = RenderingStates.FINISHED; var ctx = this._getPageDrawContext(true); var canvas = ctx.canvas; if (img.width <= 2 * canvas.width) { ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); this._convertCanvasToImage(); return; } // drawImage does an awful job of rescaling the image, doing it gradually. var MAX_NUM_SCALING_STEPS = 3; var reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; var reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; var reducedImage = getTempCanvas(reducedWidth, reducedHeight); var reducedImageCtx = reducedImage.getContext('2d'); 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); this._convertCanvasToImage(); }, get pageId() { return (this.pageLabel !== null ? this.pageLabel : this.id); }, /** * @param {string|null} label */ setPageLabel: function PDFThumbnailView_setPageLabel(label) { this.pageLabel = (typeof label === 'string' ? label : null); this.anchor.title = mozL10n.get('thumb_page_title', { page: this.pageId }, 'Page {{page}}'); if (this.renderingState !== RenderingStates.FINISHED) { return; } var ariaLabel = mozL10n.get('thumb_page_canvas', { page: this.pageId }, 'Thumbnail of Page {{page}}'); if (this.image) { this.image.setAttribute('aria-label', ariaLabel); } else if (this.disableCanvasToImageConversion && this.canvas) { this.canvas.setAttribute('aria-label', ariaLabel); } }, }; return PDFThumbnailView; })(); PDFThumbnailView.tempImageCache = null; exports.PDFThumbnailView = PDFThumbnailView; }));