/* 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. */ 'use strict'; (function (root, factory) { if (typeof define === 'function' && define.amd) { define('pdfjs-web/pdf_viewer', ['exports', 'pdfjs-web/ui_utils', 'pdfjs-web/pdf_page_view', 'pdfjs-web/pdf_rendering_queue', 'pdfjs-web/text_layer_builder', 'pdfjs-web/annotation_layer_builder', 'pdfjs-web/pdf_link_service', 'pdfjs-web/dom_events', 'pdfjs-web/pdfjs'], factory); } else if (typeof exports !== 'undefined') { factory(exports, require('./ui_utils.js'), require('./pdf_page_view.js'), require('./pdf_rendering_queue.js'), require('./text_layer_builder.js'), require('./annotation_layer_builder.js'), require('./pdf_link_service.js'), require('./dom_events.js'), require('./pdfjs.js')); } else { factory((root.pdfjsWebPDFViewer = {}), root.pdfjsWebUIUtils, root.pdfjsWebPDFPageView, root.pdfjsWebPDFRenderingQueue, root.pdfjsWebTextLayerBuilder, root.pdfjsWebAnnotationLayerBuilder, root.pdfjsWebPDFLinkService, root.pdfjsWebDOMEvents, root.pdfjsWebPDFJS); } }(this, function (exports, uiUtils, pdfPageView, pdfRenderingQueue, textLayerBuilder, annotationLayerBuilder, pdfLinkService, domEvents, pdfjsLib) { var UNKNOWN_SCALE = uiUtils.UNKNOWN_SCALE; var SCROLLBAR_PADDING = uiUtils.SCROLLBAR_PADDING; var VERTICAL_PADDING = uiUtils.VERTICAL_PADDING; var MAX_AUTO_SCALE = uiUtils.MAX_AUTO_SCALE; var CSS_UNITS = uiUtils.CSS_UNITS; var DEFAULT_SCALE = uiUtils.DEFAULT_SCALE; var DEFAULT_SCALE_VALUE = uiUtils.DEFAULT_SCALE_VALUE; var scrollIntoView = uiUtils.scrollIntoView; var watchScroll = uiUtils.watchScroll; var getVisibleElements = uiUtils.getVisibleElements; var PDFPageView = pdfPageView.PDFPageView; var RenderingStates = pdfRenderingQueue.RenderingStates; var PDFRenderingQueue = pdfRenderingQueue.PDFRenderingQueue; var TextLayerBuilder = textLayerBuilder.TextLayerBuilder; var AnnotationLayerBuilder = annotationLayerBuilder.AnnotationLayerBuilder; var SimpleLinkService = pdfLinkService.SimpleLinkService; var PresentationModeState = { UNKNOWN: 0, NORMAL: 1, CHANGING: 2, FULLSCREEN: 3, }; var DEFAULT_CACHE_SIZE = 10; /** * @typedef {Object} PDFViewerOptions * @property {HTMLDivElement} container - The container for the viewer element. * @property {HTMLDivElement} viewer - (optional) The viewer element. * @property {EventBus} eventBus - The application event bus. * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {DownloadManager} downloadManager - (optional) The download * manager component. * @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.eventBus = options.eventBus || domEvents.getGlobalEventBus(); this.linkService = options.linkService || new SimpleLinkService(); this.downloadManager = options.downloadManager || null; 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 arg; if (!(0 < val && val <= this.pagesCount)) { arg = { source: this, updateInProgress: this.updateInProgress, pageNumber: this._currentPageNumber, previousPageNumber: val }; this.eventBus.dispatch('pagechanging', arg); this.eventBus.dispatch('pagechange', arg); return; } arg = { source: this, updateInProgress: this.updateInProgress, pageNumber: val, previousPageNumber: this._currentPageNumber }; this.eventBus.dispatch('pagechanging', arg); this._currentPageNumber = val; this.eventBus.dispatch('pagechange', arg); // 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 () { self.eventBus.dispatch('pagesloaded', { source: self, pagesCount: pagesCount }); }); 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 (!pdfjsLib.PDFJS.disableTextLayer) { textLayerFactory = this; } var pageView = new PDFPageView({ container: this.viewer, eventBus: this.eventBus, id: pageNum, scale: scale, defaultViewport: viewport.clone(), renderingQueue: this.renderingQueue, textLayerFactory: textLayerFactory, annotationLayerFactory: 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 (!pdfjsLib.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(); } }); self.eventBus.dispatch('pagesinit', {source: self}); 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 arg = { source: this, scale: newScale, presetValue: preset ? newValue : undefined }; this.eventBus.dispatch('scalechanging', arg); this.eventBus.dispatch('scalechange', arg); }, _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 && !pdfjsLib.PDFJS.ignoreCurrentPositionOnZoom && !(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) { if (!this.pdfDocument) { return; } 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'; // According to the PDF spec, section 12.3.2.2, a `null` value in the // parameter should maintain the position relative to the new page. if (y === null && this._location) { x = this._location.left; y = this._location.top; } 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; this.eventBus.dispatch('updateviewarea', { source: this, location: this._location }); }, 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({ normalizeWhitespace: true }); }); }, /** * @param {HTMLDivElement} textLayerDiv * @param {number} pageIndex * @param {PageViewport} viewport * @returns {TextLayerBuilder} */ createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { return new TextLayerBuilder({ textLayerDiv: textLayerDiv, eventBus: this.eventBus, pageIndex: pageIndex, viewport: viewport, findController: this.isInPresentationMode ? null : this.findController }); }, /** * @param {HTMLDivElement} pageDiv * @param {PDFPage} pdfPage * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder: function (pageDiv, pdfPage) { return new AnnotationLayerBuilder({ pageDiv: pageDiv, pdfPage: pdfPage, linkService: this.linkService, downloadManager: this.downloadManager }); }, setFindController: function (findController) { this.findController = findController; }, }; return PDFViewer; })(); exports.PresentationModeState = PresentationModeState; exports.PDFViewer = PDFViewer; }));