You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
12 KiB
394 lines
12 KiB
/* -*- 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, |
|
watchScroll, getVisibleElements */ |
|
|
|
'use strict'; |
|
|
|
var THUMBNAIL_SCROLL_MARGIN = -19; |
|
|
|
/** |
|
* @constructor |
|
* @param container |
|
* @param id |
|
* @param defaultViewport |
|
* @param linkService |
|
* @param renderingQueue |
|
* |
|
* @implements {IRenderableView} |
|
*/ |
|
var ThumbnailView = function thumbnailView(container, id, defaultViewport, |
|
linkService, renderingQueue) { |
|
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.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); |
|
|
|
return canvas.getContext('2d'); |
|
}; |
|
|
|
this.drawingRequired = function thumbnailViewDrawingRequired() { |
|
return !this.hasImage; |
|
}; |
|
|
|
this.draw = function thumbnailViewDraw() { |
|
if (this.renderingState !== RenderingStates.INITIAL) { |
|
console.error('Must be in new state before drawing'); |
|
} |
|
|
|
this.renderingState = RenderingStates.RUNNING; |
|
if (this.hasImage) { |
|
return Promise.resolve(undefined); |
|
} |
|
|
|
var resolveRenderPromise, rejectRenderPromise; |
|
var promise = new Promise(function (resolve, reject) { |
|
resolveRenderPromise = resolve; |
|
rejectRenderPromise = reject; |
|
}); |
|
|
|
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; |
|
resolveRenderPromise(undefined); |
|
}, |
|
function pdfPageRenderError(error) { |
|
self.renderingState = RenderingStates.FINISHED; |
|
rejectRenderPromise(error); |
|
} |
|
); |
|
this.hasImage = true; |
|
return promise; |
|
}; |
|
|
|
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(pageView) { |
|
var img = pageView.canvas; |
|
if (this.hasImage || !img) { |
|
return; |
|
} |
|
if (!this.pdfPage) { |
|
this.setPdfPage(pageView.pdfPage); |
|
} |
|
this.renderingState = RenderingStates.FINISHED; |
|
var ctx = this.getPageDrawContext(); |
|
|
|
var reducedImage = img; |
|
var reducedWidth = img.width; |
|
var reducedHeight = img.height; |
|
|
|
// drawImage does an awful job of rescaling the image, doing it gradually |
|
var MAX_SCALE_FACTOR = 2.0; |
|
if (Math.max(img.width / ctx.canvas.width, |
|
img.height / ctx.canvas.height) > MAX_SCALE_FACTOR) { |
|
reducedWidth >>= 1; |
|
reducedHeight >>= 1; |
|
reducedImage = getTempCanvas(reducedWidth, reducedHeight); |
|
var reducedImageCtx = reducedImage.getContext('2d'); |
|
reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, |
|
0, 0, reducedWidth, reducedHeight); |
|
while (Math.max(reducedWidth / ctx.canvas.width, |
|
reducedHeight / ctx.canvas.height) > MAX_SCALE_FACTOR) { |
|
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, ctx.canvas.width, ctx.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; |
|
this._pagesRequests = []; |
|
}, |
|
|
|
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 thumbnail = new ThumbnailView(this.container, pageNum, |
|
viewport.clone(), this.linkService, |
|
this.renderingQueue); |
|
this.thumbnails.push(thumbnail); |
|
} |
|
}.bind(this)); |
|
}, |
|
|
|
/** |
|
* @param {PDFPageView} pageView |
|
* @returns {PDFPage} |
|
* @private |
|
*/ |
|
_ensurePdfPageLoaded: function (thumbView) { |
|
if (thumbView.pdfPage) { |
|
return Promise.resolve(thumbView.pdfPage); |
|
} |
|
var pageNumber = thumbView.id; |
|
if (this._pagesRequests[pageNumber]) { |
|
return this._pagesRequests[pageNumber]; |
|
} |
|
var promise = this.pdfDocument.getPage(pageNumber).then( |
|
function (pdfPage) { |
|
thumbView.setPdfPage(pdfPage); |
|
this._pagesRequests[pageNumber] = null; |
|
return pdfPage; |
|
}.bind(this)); |
|
this._pagesRequests[pageNumber] = promise; |
|
return promise; |
|
}, |
|
|
|
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._ensurePdfPageLoaded(thumbView).then(function () { |
|
this.renderingQueue.renderView(thumbView); |
|
}.bind(this)); |
|
return true; |
|
} |
|
return false; |
|
} |
|
}; |
|
|
|
return PDFThumbnailViewer; |
|
})();
|
|
|