/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* 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. */ /* globals mozL10n, RenderingStates, Promise, scrollIntoView, PDFPageSource, watchScroll, getVisibleElements */ 'use strict'; var THUMBNAIL_SCROLL_MARGIN = -19; /** * @constructor * @param container * @param id * @param defaultViewport * @param linkService * @param renderingQueue * @param pageSource * * @implements {IRenderableView} */ var ThumbnailView = function thumbnailView(container, id, defaultViewport, linkService, renderingQueue, pageSource) { 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.pdfPage = undefined; this.viewport = defaultViewport; this.pdfPageRotate = defaultViewport.rotation; this.rotation = 0; this.pageWidth = this.viewport.width; this.pageHeight = this.viewport.height; this.pageRatio = this.pageWidth / this.pageHeight; this.id = id; this.renderingId = 'thumbnail' + id; this.canvasWidth = 98; this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; this.scale = (this.canvasWidth / this.pageWidth); var div = this.el = document.createElement('div'); div.id = 'thumbnailContainer' + id; div.className = 'thumbnail'; 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'; ring.style.width = this.canvasWidth + 'px'; ring.style.height = this.canvasHeight + 'px'; div.appendChild(ring); anchor.appendChild(div); container.appendChild(anchor); this.hasImage = false; this.renderingState = RenderingStates.INITIAL; this.renderingQueue = renderingQueue; this.pageSource = pageSource; this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { this.pdfPage = pdfPage; this.pdfPageRotate = pdfPage.rotate; var totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = pdfPage.getViewport(1, totalRotation); this.update(); }; this.update = function thumbnailViewUpdate(rotation) { if (rotation !== undefined) { this.rotation = rotation; } var totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = this.viewport.clone({ scale: 1, rotation: totalRotation }); this.pageWidth = this.viewport.width; this.pageHeight = this.viewport.height; this.pageRatio = this.pageWidth / this.pageHeight; this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; this.scale = (this.canvasWidth / this.pageWidth); div.removeAttribute('data-loaded'); ring.textContent = ''; ring.style.width = this.canvasWidth + 'px'; ring.style.height = this.canvasHeight + 'px'; this.hasImage = false; this.renderingState = RenderingStates.INITIAL; this.resume = null; }; this.getPageDrawContext = function thumbnailViewGetPageDrawContext() { var canvas = document.createElement('canvas'); canvas.id = 'thumbnail' + id; canvas.width = this.canvasWidth; canvas.height = this.canvasHeight; canvas.className = 'thumbnailImage'; canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', {page: id}, 'Thumbnail of Page {{page}}')); div.setAttribute('data-loaded', true); ring.appendChild(canvas); var ctx = canvas.getContext('2d'); ctx.save(); ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); ctx.restore(); return ctx; }; this.drawingRequired = function thumbnailViewDrawingRequired() { return !this.hasImage; }; this.draw = function thumbnailViewDraw(callback) { if (!this.pdfPage) { var promise = this.pageSource.getPage(this.id); promise.then(function(pdfPage) { this.setPdfPage(pdfPage); this.draw(callback); }.bind(this)); return; } if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } this.renderingState = RenderingStates.RUNNING; if (this.hasImage) { callback(); return; } var self = this; var ctx = this.getPageDrawContext(); var drawViewport = this.viewport.clone({ scale: this.scale }); var renderContext = { canvasContext: ctx, viewport: drawViewport, continueCallback: function(cont) { if (!self.renderingQueue.isHighestPriority(self)) { self.renderingState = RenderingStates.PAUSED; self.resume = function() { self.renderingState = RenderingStates.RUNNING; cont(); }; return; } cont(); } }; this.pdfPage.render(renderContext).promise.then( function pdfPageRenderCallback() { self.renderingState = RenderingStates.FINISHED; callback(); }, function pdfPageRenderError(error) { self.renderingState = RenderingStates.FINISHED; callback(); } ); this.hasImage = true; }; function getTempCanvas(width, height) { var tempCanvas = ThumbnailView.tempImageCache; if (!tempCanvas) { tempCanvas = document.createElement('canvas'); ThumbnailView.tempImageCache = tempCanvas; } tempCanvas.width = width; tempCanvas.height = height; return tempCanvas; } this.setImage = function thumbnailViewSetImage(img) { if (!this.pdfPage) { var promise = this.pageSource.getPage(); promise.then(function(pdfPage) { this.setPdfPage(pdfPage); this.setImage(img); }.bind(this)); return; } if (this.hasImage || !img) { return; } this.renderingState = RenderingStates.FINISHED; var ctx = this.getPageDrawContext(); 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); } else { // 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.hasImage = true; }; }; ThumbnailView.tempImageCache = null; /** * @typedef {Object} PDFThumbnailViewerOptions * @property {HTMLDivElement} container - The container for the thumbs elements. * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. */ /** * Simple viewer control to display thumbs for pages. * @class */ var PDFThumbnailViewer = (function pdfThumbnailViewer() { /** * @constructs * @param {PDFThumbnailViewerOptions} options */ function PDFThumbnailViewer(options) { this.container = options.container; this.renderingQueue = options.renderingQueue; this.linkService = options.linkService; this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); this._resetView(); } PDFThumbnailViewer.prototype = { _scrollUpdated: function PDFThumbnailViewer_scrollUpdated() { this.renderingQueue.renderHighestPriority(); }, getThumbnail: function PDFThumbnailViewer_getThumbnail(index) { return this.thumbnails[index]; }, _getVisibleThumbs: function PDFThumbnailViewer_getVisibleThumbs() { return getVisibleElements(this.container, this.thumbnails); }, scrollThumbnailIntoView: function (page) { var selected = document.querySelector('.thumbnail.selected'); if (selected) { selected.classList.remove('selected'); } var thumbnail = document.getElementById('thumbnailContainer' + page); thumbnail.classList.add('selected'); var visibleThumbs = this._getVisibleThumbs(); var numVisibleThumbs = visibleThumbs.views.length; // If the thumbnail isn't currently visible, scroll it into view. if (numVisibleThumbs > 0) { var first = visibleThumbs.first.id; // Account for only one thumbnail being visible. var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); if (page <= first || page >= last) { scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); } } }, get pagesRotation() { return this._pagesRotation; }, set pagesRotation(rotation) { this._pagesRotation = rotation; for (var i = 0, l = this.thumbnails.length; i < l; i++) { var thumb = this.thumbnails[i]; thumb.update(rotation); } }, cleanup: function PDFThumbnailViewer_cleanup() { ThumbnailView.tempImageCache = null; }, _resetView: function () { this.thumbnails = []; this._pagesRotation = 0; }, setDocument: function (pdfDocument) { if (this.pdfDocument) { // cleanup of the elements and views var thumbsView = this.container; while (thumbsView.hasChildNodes()) { thumbsView.removeChild(thumbsView.lastChild); } this._resetView(); } this.pdfDocument = pdfDocument; if (!pdfDocument) { return Promise.resolve(); } return pdfDocument.getPage(1).then(function (firstPage) { var pagesCount = pdfDocument.numPages; var viewport = firstPage.getViewport(1.0); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { var pageSource = new PDFPageSource(pdfDocument, pageNum); var thumbnail = new ThumbnailView(this.container, pageNum, viewport.clone(), this.linkService, this.renderingQueue, pageSource); this.thumbnails.push(thumbnail); } }.bind(this)); }, ensureThumbnailVisible: function PDFThumbnailViewer_ensureThumbnailVisible(page) { // Ensure that the thumbnail of the current page is visible // when switching from another view. scrollIntoView(document.getElementById('thumbnailContainer' + page)); }, forceRendering: function () { var visibleThumbs = this._getVisibleThumbs(); var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs, this.thumbnails, this.scroll.down); if (thumbView) { this.renderingQueue.renderView(thumbView); return true; } return false; } }; return PDFThumbnailViewer; })();