/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2014 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 watchScroll, PDFPageView, UNKNOWN_SCALE, SCROLLBAR_PADDING, VERTICAL_PADDING, MAX_AUTO_SCALE, CSS_UNITS, DEFAULT_SCALE, scrollIntoView, getVisibleElements, RenderingStates, PDFJS, Promise, TextLayerBuilder, PDFRenderingQueue, AnnotationsLayerBuilder, DEFAULT_SCALE_VALUE */ 'use strict'; var PresentationModeState = { UNKNOWN: 0, NORMAL: 1, CHANGING: 2, FULLSCREEN: 3, }; var IGNORE_CURRENT_POSITION_ON_ZOOM = false; var DEFAULT_CACHE_SIZE = 10; //#include pdf_rendering_queue.js //#include pdf_page_view.js //#include text_layer_builder.js //#include annotations_layer_builder.js /** * @typedef {Object} PDFViewerOptions * @property {HTMLDivElement} container - The container for the viewer element. * @property {HTMLDivElement} viewer - (optional) The viewer element. * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering * queue object. * @property {boolean} removePageBorders - (optional) Removes the border shadow * around the pages. The default is false. */ /** * Simple viewer control to display PDF content/pages. * @class * @implements {IRenderableView} */ var PDFViewer = (function pdfViewer() { function PDFPageViewBuffer(size) { var data = []; this.push = function cachePush(view) { var i = data.indexOf(view); if (i >= 0) { data.splice(i, 1); } data.push(view); if (data.length > size) { data.shift().destroy(); } }; this.resize = function (newSize) { size = newSize; while (data.length > size) { data.shift().destroy(); } }; } function isSameScale(oldScale, newScale) { if (newScale === oldScale) { return true; } if (Math.abs(newScale - oldScale) < 1e-15) { // Prevent unnecessary re-rendering of all pages when the scale // changes only because of limited numerical precision. return true; } return false; } /** * @constructs PDFViewer * @param {PDFViewerOptions} options */ function PDFViewer(options) { this.container = options.container; this.viewer = options.viewer || options.container.firstElementChild; this.linkService = options.linkService || new SimpleLinkService(); this.removePageBorders = options.removePageBorders || false; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { // Custom rendering queue is not specified, using default one this.renderingQueue = new PDFRenderingQueue(); this.renderingQueue.setViewer(this); } else { this.renderingQueue = options.renderingQueue; } this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); this.updateInProgress = false; this.presentationModeState = PresentationModeState.UNKNOWN; this._resetView(); if (this.removePageBorders) { this.viewer.classList.add('removePageBorders'); } } PDFViewer.prototype = /** @lends PDFViewer.prototype */{ get pagesCount() { return this._pages.length; }, getPageView: function (index) { return this._pages[index]; }, get currentPageNumber() { return this._currentPageNumber; }, set currentPageNumber(val) { if (!this.pdfDocument) { this._currentPageNumber = val; return; } var event = document.createEvent('UIEvents'); event.initUIEvent('pagechange', true, true, window, 0); event.updateInProgress = this.updateInProgress; if (!(0 < val && val <= this.pagesCount)) { event.pageNumber = this._currentPageNumber; event.previousPageNumber = val; this.container.dispatchEvent(event); return; } event.previousPageNumber = this._currentPageNumber; this._currentPageNumber = val; event.pageNumber = val; this.container.dispatchEvent(event); // Check if the caller is `PDFViewer_update`, to avoid breaking scrolling. if (this.updateInProgress) { return; } this.scrollPageIntoView(val); }, /** * @returns {number} */ get currentScale() { return this._currentScale !== UNKNOWN_SCALE ? this._currentScale : DEFAULT_SCALE; }, /** * @param {number} val - Scale of the pages in percents. */ set currentScale(val) { if (isNaN(val)) { throw new Error('Invalid numeric scale'); } if (!this.pdfDocument) { this._currentScale = val; this._currentScaleValue = val !== UNKNOWN_SCALE ? val.toString() : null; return; } this._setScale(val, false); }, /** * @returns {string} */ get currentScaleValue() { return this._currentScaleValue; }, /** * @param val - The scale of the pages (in percent or predefined value). */ set currentScaleValue(val) { if (!this.pdfDocument) { this._currentScale = isNaN(val) ? UNKNOWN_SCALE : val; this._currentScaleValue = val; return; } this._setScale(val, false); }, /** * @returns {number} */ get pagesRotation() { return this._pagesRotation; }, /** * @param {number} rotation - The rotation of the pages (0, 90, 180, 270). */ set pagesRotation(rotation) { this._pagesRotation = rotation; for (var i = 0, l = this._pages.length; i < l; i++) { var pageView = this._pages[i]; pageView.update(pageView.scale, rotation); } this._setScale(this._currentScaleValue, true); if (this.defaultRenderingQueue) { this.update(); } }, /** * @param pdfDocument {PDFDocument} */ setDocument: function (pdfDocument) { if (this.pdfDocument) { this._resetView(); } this.pdfDocument = pdfDocument; if (!pdfDocument) { return; } var pagesCount = pdfDocument.numPages; var self = this; var resolvePagesPromise; var pagesPromise = new Promise(function (resolve) { resolvePagesPromise = resolve; }); this.pagesPromise = pagesPromise; pagesPromise.then(function () { var event = document.createEvent('CustomEvent'); event.initCustomEvent('pagesloaded', true, true, { pagesCount: pagesCount }); self.container.dispatchEvent(event); }); var isOnePageRenderedResolved = false; var resolveOnePageRendered = null; var onePageRendered = new Promise(function (resolve) { resolveOnePageRendered = resolve; }); this.onePageRendered = onePageRendered; var bindOnAfterAndBeforeDraw = function (pageView) { pageView.onBeforeDraw = function pdfViewLoadOnBeforeDraw() { // Add the page to the buffer at the start of drawing. That way it can // be evicted from the buffer and destroyed even if we pause its // rendering. self._buffer.push(this); }; // when page is painted, using the image as thumbnail base pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { if (!isOnePageRenderedResolved) { isOnePageRenderedResolved = true; resolveOnePageRendered(); } }; }; var firstPagePromise = pdfDocument.getPage(1); this.firstPagePromise = firstPagePromise; // Fetch a single page so we can get a viewport that will be the default // viewport for all pages return firstPagePromise.then(function(pdfPage) { var scale = this.currentScale; var viewport = pdfPage.getViewport(scale * CSS_UNITS); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { var textLayerFactory = null; if (!PDFJS.disableTextLayer) { textLayerFactory = this; } var pageView = new PDFPageView({ container: this.viewer, id: pageNum, scale: scale, defaultViewport: viewport.clone(), renderingQueue: this.renderingQueue, textLayerFactory: textLayerFactory, annotationsLayerFactory: this }); bindOnAfterAndBeforeDraw(pageView); this._pages.push(pageView); } var linkService = this.linkService; // Fetch all the pages since the viewport is needed before printing // starts to create the correct size canvas. Wait until one page is // rendered so we don't tie up too many resources early on. onePageRendered.then(function () { if (!PDFJS.disableAutoFetch) { var getPagesLeft = pagesCount; for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) { var pageView = self._pages[pageNum - 1]; if (!pageView.pdfPage) { pageView.setPdfPage(pdfPage); } linkService.cachePageRef(pageNum, pdfPage.ref); getPagesLeft--; if (!getPagesLeft) { resolvePagesPromise(); } }.bind(null, pageNum)); } } else { // XXX: Printing is semi-broken with auto fetch disabled. resolvePagesPromise(); } }); var event = document.createEvent('CustomEvent'); event.initCustomEvent('pagesinit', true, true, null); self.container.dispatchEvent(event); if (this.defaultRenderingQueue) { this.update(); } if (this.findController) { this.findController.resolveFirstPage(); } }.bind(this)); }, _resetView: function () { this._pages = []; this._currentPageNumber = 1; this._currentScale = UNKNOWN_SCALE; this._currentScaleValue = null; this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); this._location = null; this._pagesRotation = 0; this._pagesRequests = []; var container = this.viewer; while (container.hasChildNodes()) { container.removeChild(container.lastChild); } }, _scrollUpdate: function PDFViewer_scrollUpdate() { if (this.pagesCount === 0) { return; } this.update(); for (var i = 0, ii = this._pages.length; i < ii; i++) { this._pages[i].updatePosition(); } }, _setScaleDispatchEvent: function pdfViewer_setScaleDispatchEvent( newScale, newValue, preset) { var event = document.createEvent('UIEvents'); event.initUIEvent('scalechange', true, true, window, 0); event.scale = newScale; if (preset) { event.presetValue = newValue; } this.container.dispatchEvent(event); }, _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages( newScale, newValue, noScroll, preset) { this._currentScaleValue = newValue; if (isSameScale(this._currentScale, newScale)) { if (preset) { this._setScaleDispatchEvent(newScale, newValue, true); } return; } for (var i = 0, ii = this._pages.length; i < ii; i++) { this._pages[i].update(newScale); } this._currentScale = newScale; if (!noScroll) { var page = this._currentPageNumber, dest; if (this._location && !IGNORE_CURRENT_POSITION_ON_ZOOM && !(this.isInPresentationMode || this.isChangingPresentationMode)) { page = this._location.pageNumber; dest = [null, { name: 'XYZ' }, this._location.left, this._location.top, null]; } this.scrollPageIntoView(page, dest); } this._setScaleDispatchEvent(newScale, newValue, preset); if (this.defaultRenderingQueue) { this.update(); } }, _setScale: function pdfViewer_setScale(value, noScroll) { var scale = parseFloat(value); if (scale > 0) { this._setScaleUpdatePages(scale, value, noScroll, false); } else { var currentPage = this._pages[this._currentPageNumber - 1]; if (!currentPage) { return; } var hPadding = (this.isInPresentationMode || this.removePageBorders) ? 0 : SCROLLBAR_PADDING; var vPadding = (this.isInPresentationMode || this.removePageBorders) ? 0 : VERTICAL_PADDING; var pageWidthScale = (this.container.clientWidth - hPadding) / currentPage.width * currentPage.scale; var pageHeightScale = (this.container.clientHeight - vPadding) / currentPage.height * currentPage.scale; switch (value) { case 'page-actual': scale = 1; break; case 'page-width': scale = pageWidthScale; break; case 'page-height': scale = pageHeightScale; break; case 'page-fit': scale = Math.min(pageWidthScale, pageHeightScale); break; case 'auto': var isLandscape = (currentPage.width > currentPage.height); // For pages in landscape mode, fit the page height to the viewer // *unless* the page would thus become too wide to fit horizontally. var horizontalScale = isLandscape ? Math.min(pageHeightScale, pageWidthScale) : pageWidthScale; scale = Math.min(MAX_AUTO_SCALE, horizontalScale); break; default: console.error('pdfViewSetScale: \'' + value + '\' is an unknown zoom value.'); return; } this._setScaleUpdatePages(scale, value, noScroll, true); } }, /** * Scrolls page into view. * @param {number} pageNumber * @param {Array} dest - (optional) original PDF destination array: * */ scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber, dest) { var pageView = this._pages[pageNumber - 1]; if (this.isInPresentationMode) { if (this._currentPageNumber !== pageView.id) { // Avoid breaking getVisiblePages in presentation mode. this.currentPageNumber = pageView.id; return; } dest = null; // Fixes the case when PDF has different page sizes. this._setScale(this._currentScaleValue, true); } if (!dest) { scrollIntoView(pageView.div); return; } var x = 0, y = 0; var width = 0, height = 0, widthScale, heightScale; var changeOrientation = (pageView.rotation % 180 === 0 ? false : true); var pageWidth = (changeOrientation ? pageView.height : pageView.width) / pageView.scale / CSS_UNITS; var pageHeight = (changeOrientation ? pageView.width : pageView.height) / pageView.scale / CSS_UNITS; var scale = 0; switch (dest[1].name) { case 'XYZ': x = dest[2]; y = dest[3]; scale = dest[4]; // If x and/or y coordinates are not supplied, default to // _top_ left of the page (not the obvious bottom left, // since aligning the bottom of the intended page with the // top of the window is rarely helpful). x = x !== null ? x : 0; y = y !== null ? y : pageHeight; break; case 'Fit': case 'FitB': scale = 'page-fit'; break; case 'FitH': case 'FitBH': y = dest[2]; scale = 'page-width'; break; case 'FitV': case 'FitBV': x = dest[2]; width = pageWidth; height = pageHeight; scale = 'page-height'; break; case 'FitR': x = dest[2]; y = dest[3]; width = dest[4] - x; height = dest[5] - y; var hPadding = this.removePageBorders ? 0 : SCROLLBAR_PADDING; var vPadding = this.removePageBorders ? 0 : VERTICAL_PADDING; widthScale = (this.container.clientWidth - hPadding) / width / CSS_UNITS; heightScale = (this.container.clientHeight - vPadding) / height / CSS_UNITS; scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); break; default: return; } if (scale && scale !== this._currentScale) { this.currentScaleValue = scale; } else if (this._currentScale === UNKNOWN_SCALE) { this.currentScaleValue = DEFAULT_SCALE_VALUE; } if (scale === 'page-fit' && !dest[4]) { scrollIntoView(pageView.div); return; } var boundingRect = [ pageView.viewport.convertToViewportPoint(x, y), pageView.viewport.convertToViewportPoint(x + width, y + height) ]; var left = Math.min(boundingRect[0][0], boundingRect[1][0]); var top = Math.min(boundingRect[0][1], boundingRect[1][1]); scrollIntoView(pageView.div, { left: left, top: top }); }, _updateLocation: function (firstPage) { var currentScale = this._currentScale; var currentScaleValue = this._currentScaleValue; var normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue; var pageNumber = firstPage.id; var pdfOpenParams = '#page=' + pageNumber; pdfOpenParams += '&zoom=' + normalizedScaleValue; var currentPageView = this._pages[pageNumber - 1]; var container = this.container; var topLeft = currentPageView.getPagePoint( (container.scrollLeft - firstPage.x), (container.scrollTop - firstPage.y)); var intLeft = Math.round(topLeft[0]); var intTop = Math.round(topLeft[1]); pdfOpenParams += ',' + intLeft + ',' + intTop; this._location = { pageNumber: pageNumber, scale: normalizedScaleValue, top: intTop, left: intLeft, pdfOpenParams: pdfOpenParams }; }, update: function PDFViewer_update() { var visible = this._getVisiblePages(); var visiblePages = visible.views; if (visiblePages.length === 0) { return; } this.updateInProgress = true; var suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * visiblePages.length + 1); this._buffer.resize(suggestedCacheSize); this.renderingQueue.renderHighestPriority(visible); var currentId = this._currentPageNumber; var firstPage = visible.first; for (var i = 0, ii = visiblePages.length, stillFullyVisible = false; i < ii; ++i) { var page = visiblePages[i]; if (page.percent < 100) { break; } if (page.id === currentId) { stillFullyVisible = true; break; } } if (!stillFullyVisible) { currentId = visiblePages[0].id; } if (!this.isInPresentationMode) { this.currentPageNumber = currentId; } this._updateLocation(firstPage); this.updateInProgress = false; var event = document.createEvent('UIEvents'); event.initUIEvent('updateviewarea', true, true, window, 0); event.location = this._location; this.container.dispatchEvent(event); }, containsElement: function (element) { return this.container.contains(element); }, focus: function () { this.container.focus(); }, get isInPresentationMode() { return this.presentationModeState === PresentationModeState.FULLSCREEN; }, get isChangingPresentationMode() { return this.PresentationModeState === PresentationModeState.CHANGING; }, get isHorizontalScrollbarEnabled() { return (this.isInPresentationMode ? false : (this.container.scrollWidth > this.container.clientWidth)); }, _getVisiblePages: function () { if (!this.isInPresentationMode) { return getVisibleElements(this.container, this._pages, true); } else { // The algorithm in getVisibleElements doesn't work in all browsers and // configurations when presentation mode is active. var visible = []; var currentPage = this._pages[this._currentPageNumber - 1]; visible.push({ id: currentPage.id, view: currentPage }); return { first: currentPage, last: currentPage, views: visible }; } }, cleanup: function () { for (var i = 0, ii = this._pages.length; i < ii; i++) { if (this._pages[i] && this._pages[i].renderingState !== RenderingStates.FINISHED) { this._pages[i].reset(); } } }, /** * @param {PDFPageView} pageView * @returns {PDFPage} * @private */ _ensurePdfPageLoaded: function (pageView) { if (pageView.pdfPage) { return Promise.resolve(pageView.pdfPage); } var pageNumber = pageView.id; if (this._pagesRequests[pageNumber]) { return this._pagesRequests[pageNumber]; } var promise = this.pdfDocument.getPage(pageNumber).then( function (pdfPage) { pageView.setPdfPage(pdfPage); this._pagesRequests[pageNumber] = null; return pdfPage; }.bind(this)); this._pagesRequests[pageNumber] = promise; return promise; }, forceRendering: function (currentlyVisiblePages) { var visiblePages = currentlyVisiblePages || this._getVisiblePages(); var pageView = this.renderingQueue.getHighestPriority(visiblePages, this._pages, this.scroll.down); if (pageView) { this._ensurePdfPageLoaded(pageView).then(function () { this.renderingQueue.renderView(pageView); }.bind(this)); return true; } return false; }, getPageTextContent: function (pageIndex) { return this.pdfDocument.getPage(pageIndex + 1).then(function (page) { return page.getTextContent(); }); }, /** * @param {HTMLDivElement} textLayerDiv * @param {number} pageIndex * @param {PageViewport} viewport * @returns {TextLayerBuilder} */ createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { return new TextLayerBuilder({ textLayerDiv: textLayerDiv, pageIndex: pageIndex, viewport: viewport, findController: this.isInPresentationMode ? null : this.findController }); }, /** * @param {HTMLDivElement} pageDiv * @param {PDFPage} pdfPage * @returns {AnnotationsLayerBuilder} */ createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { return new AnnotationsLayerBuilder({ pageDiv: pageDiv, pdfPage: pdfPage, linkService: this.linkService }); }, setFindController: function (findController) { this.findController = findController; }, }; return PDFViewer; })(); var SimpleLinkService = (function SimpleLinkServiceClosure() { function SimpleLinkService() {} SimpleLinkService.prototype = { /** * @returns {number} */ get page() { return 0; }, /** * @param {number} value */ set page(value) {}, /** * @param dest - The PDF destination object. */ navigateTo: function (dest) {}, /** * @param dest - The PDF destination object. * @returns {string} The hyperlink to the PDF object. */ getDestinationHash: function (dest) { return '#'; }, /** * @param hash - The PDF parameters/hash. * @returns {string} The hyperlink to the PDF object. */ getAnchorUrl: function (hash) { return '#'; }, /** * @param {string} hash */ setHash: function (hash) {}, /** * @param {string} action */ executeNamedAction: function (action) {}, /** * @param {number} pageNum - page number. * @param {Object} pageRef - reference to the page. */ cachePageRef: function (pageNum, pageRef) {} }; return SimpleLinkService; })();