diff --git a/bower.json b/bower.json index d4702253f..4f94f2fe2 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "pdfjs-dist", - "version": "1.5.211", + "version": "1.5.214", "main": [ "build/pdf.js", "build/pdf.worker.js" diff --git a/build/pdf.combined.js b/build/pdf.combined.js index 45cb7bdeb..d71f577c4 100644 --- a/build/pdf.combined.js +++ b/build/pdf.combined.js @@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfCombined = {})); // Use strict in our context only - users might not want it 'use strict'; -var pdfjsVersion = '1.5.211'; -var pdfjsBuild = 'bd49973'; +var pdfjsVersion = '1.5.214'; +var pdfjsBuild = '61a4c74'; var pdfjsFilePath = typeof document !== 'undefined' && document.currentScript ? diff --git a/build/pdf.js b/build/pdf.js index a612c194a..656087d55 100644 --- a/build/pdf.js +++ b/build/pdf.js @@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdf = {})); // Use strict in our context only - users might not want it 'use strict'; -var pdfjsVersion = '1.5.211'; -var pdfjsBuild = 'bd49973'; +var pdfjsVersion = '1.5.214'; +var pdfjsBuild = '61a4c74'; var pdfjsFilePath = typeof document !== 'undefined' && document.currentScript ? diff --git a/build/pdf.worker.js b/build/pdf.worker.js index ed3e4e165..003b320c8 100644 --- a/build/pdf.worker.js +++ b/build/pdf.worker.js @@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfWorker = {})); // Use strict in our context only - users might not want it 'use strict'; -var pdfjsVersion = '1.5.211'; -var pdfjsBuild = 'bd49973'; +var pdfjsVersion = '1.5.214'; +var pdfjsBuild = '61a4c74'; var pdfjsFilePath = typeof document !== 'undefined' && document.currentScript ? diff --git a/package.json b/package.json index 2e3269343..1c6aab1ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pdfjs-dist", - "version": "1.5.211", + "version": "1.5.214", "main": "build/pdf.js", "description": "Generic build of Mozilla's PDF.js library.", "keywords": [ diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 9cdc62086..5c8ec6087 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -37,1878 +37,1713 @@ (function (root, factory) { { - factory((root.pdfjsWebPDFHistory = {})); + factory((root.pdfjsWebPDFRenderingQueue = {})); } }(this, function (exports) { - function PDFHistory(options) { - this.linkService = options.linkService; +var CLEANUP_TIMEOUT = 30000; - this.initialized = false; - this.initialDestination = null; - this.initialBookmark = null; +var RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; + +/** + * Controls rendering of the views for pages and thumbnails. + * @class + */ +var PDFRenderingQueue = (function PDFRenderingQueueClosure() { + /** + * @constructs + */ + function PDFRenderingQueue() { + this.pdfViewer = null; + this.pdfThumbnailViewer = null; + this.onIdle = null; + + this.highestPriorityPage = null; + this.idleTimeout = null; + this.printing = false; + this.isThumbnailViewEnabled = false; } - PDFHistory.prototype = { + PDFRenderingQueue.prototype = /** @lends PDFRenderingQueue.prototype */ { /** - * @param {string} fingerprint - * @param {IPDFLinkService} linkService + * @param {PDFViewer} pdfViewer */ - initialize: function pdfHistoryInitialize(fingerprint) { - this.initialized = true; - this.reInitialized = false; - this.allowHashChange = true; - this.historyUnlocked = true; - this.isViewerInPresentationMode = false; + setViewer: function PDFRenderingQueue_setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + }, - this.previousHash = window.location.hash.substring(1); - this.currentBookmark = ''; - this.currentPage = 0; - this.updatePreviousBookmark = false; - this.previousBookmark = ''; - this.previousPage = 0; - this.nextHashParam = ''; + /** + * @param {PDFThumbnailViewer} pdfThumbnailViewer + */ + setThumbnailViewer: + function PDFRenderingQueue_setThumbnailViewer(pdfThumbnailViewer) { + this.pdfThumbnailViewer = pdfThumbnailViewer; + }, - this.fingerprint = fingerprint; - this.currentUid = this.uid = 0; - this.current = {}; + /** + * @param {IRenderableView} view + * @returns {boolean} + */ + isHighestPriority: function PDFRenderingQueue_isHighestPriority(view) { + return this.highestPriorityPage === view.renderingId; + }, - var state = window.history.state; - if (this._isStateObjectDefined(state)) { - // This corresponds to navigating back to the document - // from another page in the browser history. - if (state.target.dest) { - this.initialDestination = state.target.dest; - } else { - this.initialBookmark = state.target.hash; - } - this.currentUid = state.uid; - this.uid = state.uid + 1; - this.current = state.target; - } else { - // This corresponds to the loading of a new document. - if (state && state.fingerprint && - this.fingerprint !== state.fingerprint) { - // Reinitialize the browsing history when a new document - // is opened in the web viewer. - this.reInitialized = true; - } - this._pushOrReplaceState({fingerprint: this.fingerprint}, true); + renderHighestPriority: function + PDFRenderingQueue_renderHighestPriority(currentlyVisiblePages) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; } - var self = this; - window.addEventListener('popstate', function pdfHistoryPopstate(evt) { - if (!self.historyUnlocked) { - return; - } - if (evt.state) { - // Move back/forward in the history. - self._goTo(evt.state); + // Pages have a higher priority than thumbnails, so check them first. + if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { + return; + } + // No pages needed rendering so check thumbnails. + if (this.pdfThumbnailViewer && this.isThumbnailViewEnabled) { + if (this.pdfThumbnailViewer.forceRendering()) { return; } - - // If the state is not set, then the user tried to navigate to a - // different hash by manually editing the URL and pressing Enter, or by - // clicking on an in-page link (e.g. the "current view" link). - // Save the current view state to the browser history. - - // Note: In Firefox, history.null could also be null after an in-page - // navigation to the same URL, and without dispatching the popstate - // event: https://bugzilla.mozilla.org/show_bug.cgi?id=1183881 - - if (self.uid === 0) { - // Replace the previous state if it was not explicitly set. - var previousParams = (self.previousHash && self.currentBookmark && - self.previousHash !== self.currentBookmark) ? - {hash: self.currentBookmark, page: self.currentPage} : - {page: 1}; - replacePreviousHistoryState(previousParams, function() { - updateHistoryWithCurrentHash(); - }); - } else { - updateHistoryWithCurrentHash(); - } - }, false); - - - function updateHistoryWithCurrentHash() { - self.previousHash = window.location.hash.slice(1); - self._pushToHistory({hash: self.previousHash}, false, true); - self._updatePreviousBookmark(); } - function replacePreviousHistoryState(params, callback) { - // To modify the previous history entry, the following happens: - // 1. history.back() - // 2. _pushToHistory, which calls history.replaceState( ... ) - // 3. history.forward() - // Because a navigation via the history API does not immediately update - // the history state, the popstate event is used for synchronization. - self.historyUnlocked = false; - - // Suppress the hashchange event to avoid side effects caused by - // navigating back and forward. - self.allowHashChange = false; - window.addEventListener('popstate', rewriteHistoryAfterBack); - history.back(); - - function rewriteHistoryAfterBack() { - window.removeEventListener('popstate', rewriteHistoryAfterBack); - window.addEventListener('popstate', rewriteHistoryAfterForward); - self._pushToHistory(params, false, true); - history.forward(); - } - function rewriteHistoryAfterForward() { - window.removeEventListener('popstate', rewriteHistoryAfterForward); - self.allowHashChange = true; - self.historyUnlocked = true; - callback(); - } + if (this.printing) { + // If printing is currently ongoing do not reschedule cleanup. + return; } - function pdfHistoryBeforeUnload() { - var previousParams = self._getPreviousParams(null, true); - if (previousParams) { - var replacePrevious = (!self.current.dest && - self.current.hash !== self.previousHash); - self._pushToHistory(previousParams, false, replacePrevious); - self._updatePreviousBookmark(); - } - // Remove the event listener when navigating away from the document, - // since 'beforeunload' prevents Firefox from caching the document. - window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, - false); + if (this.onIdle) { + this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); } - - window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); - - window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { - // If the entire viewer (including the PDF file) is cached in - // the browser, we need to reattach the 'beforeunload' event listener - // since the 'DOMContentLoaded' event is not fired on 'pageshow'. - window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); - }, false); - - window.addEventListener('presentationmodechanged', function(e) { - self.isViewerInPresentationMode = !!e.detail.active; - }); }, - clearHistoryState: function pdfHistory_clearHistoryState() { - this._pushOrReplaceState(null, true); - }, + getHighestPriority: function + PDFRenderingQueue_getHighestPriority(visible, views, scrolledDown) { + // The state has changed figure out which page has the highest priority to + // render next (if any). + // Priority: + // 1 visible pages + // 2 if last scrolled down page after the visible pages + // 2 if last scrolled up page before the visible pages + var visibleViews = visible.views; - _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { - return (state && state.uid >= 0 && - state.fingerprint && this.fingerprint === state.fingerprint && - state.target && state.target.hash) ? true : false; - }, + var numVisible = visibleViews.length; + if (numVisible === 0) { + return false; + } + for (var i = 0; i < numVisible; ++i) { + var view = visibleViews[i].view; + if (!this.isViewFinished(view)) { + return view; + } + } - _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, - replace) { - if (replace) { - window.history.replaceState(stateObj, '', document.URL); + // All the visible views have rendered, try to render next/previous pages. + if (scrolledDown) { + var nextPageIndex = visible.last.id; + // ID's start at 1 so no need to add 1. + if (views[nextPageIndex] && + !this.isViewFinished(views[nextPageIndex])) { + return views[nextPageIndex]; + } } else { - window.history.pushState(stateObj, '', document.URL); + var previousPageIndex = visible.first.id - 2; + if (views[previousPageIndex] && + !this.isViewFinished(views[previousPageIndex])) { + return views[previousPageIndex]; + } } + // Everything that needs to be rendered has been. + return null; }, - get isHashChangeUnlocked() { - if (!this.initialized) { - return true; - } - return this.allowHashChange; + /** + * @param {IRenderableView} view + * @returns {boolean} + */ + isViewFinished: function PDFRenderingQueue_isViewFinished(view) { + return view.renderingState === RenderingStates.FINISHED; }, - _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { - if (this.updatePreviousBookmark && - this.currentBookmark && this.currentPage) { - this.previousBookmark = this.currentBookmark; - this.previousPage = this.currentPage; - this.updatePreviousBookmark = false; + /** + * Render a page or thumbnail view. This calls the appropriate function + * based on the views state. If the view is already rendered it will return + * false. + * @param {IRenderableView} view + */ + renderView: function PDFRenderingQueue_renderView(view) { + var state = view.renderingState; + switch (state) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + this.highestPriorityPage = view.renderingId; + view.resume(); + break; + case RenderingStates.RUNNING: + this.highestPriorityPage = view.renderingId; + break; + case RenderingStates.INITIAL: + this.highestPriorityPage = view.renderingId; + var continueRendering = function () { + this.renderHighestPriority(); + }.bind(this); + view.draw().then(continueRendering, continueRendering); + break; } + return true; }, + }; - updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, - pageNum) { - if (this.initialized) { - this.currentBookmark = bookmark.substring(1); - this.currentPage = pageNum | 0; - this._updatePreviousBookmark(); - } - }, + return PDFRenderingQueue; +})(); - updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { - if (this.initialized) { - this.nextHashParam = param; - } - }, +exports.RenderingStates = RenderingStates; +exports.PDFRenderingQueue = PDFRenderingQueue; +})); - push: function pdfHistoryPush(params, isInitialBookmark) { - if (!(this.initialized && this.historyUnlocked)) { - return; - } - if (params.dest && !params.hash) { - params.hash = (this.current.hash && this.current.dest && - this.current.dest === params.dest) ? - this.current.hash : - this.linkService.getDestinationHash(params.dest).split('#')[1]; - } - if (params.page) { - params.page |= 0; - } - if (isInitialBookmark) { - var target = window.history.state.target; - if (!target) { - // Invoked when the user specifies an initial bookmark, - // thus setting initialBookmark, when the document is loaded. - this._pushToHistory(params, false); - this.previousHash = window.location.hash.substring(1); - } - this.updatePreviousBookmark = this.nextHashParam ? false : true; - if (target) { - // If the current document is reloaded, - // avoid creating duplicate entries in the history. - this._updatePreviousBookmark(); - } - return; + +(function (root, factory) { + { + factory((root.pdfjsWebDownloadManager = {}), root.pdfjsWebPDFJS); + } +}(this, function (exports, pdfjsLib) { + function download(blobUrl, filename) { + var a = document.createElement('a'); + if (a.click) { + // Use a.click() if available. Otherwise, Chrome might show + // "Unsafe JavaScript attempt to initiate a navigation change + // for frame with URL" and not open the PDF at all. + // Supported by (not mentioned = untested): + // - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click) + // - Chrome 19 - 26 (18- does not support a.click) + // - Opera 9 - 12.15 + // - Internet Explorer 6 - 10 + // - Safari 6 (5.1- does not support a.click) + a.href = blobUrl; + a.target = '_parent'; + // Use a.download if available. This increases the likelihood that + // the file is downloaded instead of opened by another PDF plugin. + if ('download' in a) { + a.download = filename; } - if (this.nextHashParam) { - if (this.nextHashParam === params.hash) { - this.nextHashParam = null; - this.updatePreviousBookmark = true; - return; - } else { - this.nextHashParam = null; - } + // must be in the document for IE and recent Firefox versions. + // (otherwise .click() is ignored) + (document.body || document.documentElement).appendChild(a); + a.click(); + a.parentNode.removeChild(a); + } else { + if (window.top === window && + blobUrl.split('#')[0] === window.location.href.split('#')[0]) { + // If _parent == self, then opening an identical URL with different + // location hash will only cause a navigation, not a download. + var padCharacter = blobUrl.indexOf('?') === -1 ? '?' : '&'; + blobUrl = blobUrl.replace(/#|$/, padCharacter + '$&'); } + window.open(blobUrl, '_parent'); + } + } - if (params.hash) { - if (this.current.hash) { - if (this.current.hash !== params.hash) { - this._pushToHistory(params, true); - } else { - if (!this.current.page && params.page) { - this._pushToHistory(params, false, true); - } - this.updatePreviousBookmark = true; - } - } else { - this._pushToHistory(params, true); - } - } else if (this.current.page && params.page && - this.current.page !== params.page) { - this._pushToHistory(params, true); - } - }, + function DownloadManager() {} - _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, - beforeUnload) { - if (!(this.currentBookmark && this.currentPage)) { - return null; - } else if (this.updatePreviousBookmark) { - this.updatePreviousBookmark = false; - } - if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { - // Prevent the history from getting stuck in the current state, - // effectively preventing the user from going back/forward in - // the history. - // - // This happens if the current position in the document didn't change - // when the history was previously updated. The reasons for this are - // either: - // 1. The current zoom value is such that the document does not need to, - // or cannot, be scrolled to display the destination. - // 2. The previous destination is broken, and doesn't actally point to a - // position within the document. - // (This is either due to a bad PDF generator, or the user making a - // mistake when entering a destination in the hash parameters.) - return null; - } - if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { - if (this.previousBookmark === this.currentBookmark) { - return null; - } - } else if (this.current.page || onlyCheckPage) { - if (this.previousPage === this.currentPage) { - return null; - } - } else { - return null; - } - var params = {hash: this.currentBookmark, page: this.currentPage}; - if (this.isViewerInPresentationMode) { - params.hash = null; + DownloadManager.prototype = { + downloadUrl: function DownloadManager_downloadUrl(url, filename) { + if (!pdfjsLib.isValidUrl(url, true)) { + return; // restricted/invalid URL } - return params; - }, - _stateObj: function pdfHistory_stateObj(params) { - return {fingerprint: this.fingerprint, uid: this.uid, target: params}; + download(url + '#pdfjs.action=download', filename); }, - _pushToHistory: function pdfHistory_pushToHistory(params, - addPrevious, overwrite) { - if (!this.initialized) { - return; - } - if (!params.hash && params.page) { - params.hash = ('page=' + params.page); - } - if (addPrevious && !overwrite) { - var previousParams = this._getPreviousParams(); - if (previousParams) { - var replacePrevious = (!this.current.dest && - this.current.hash !== this.previousHash); - this._pushToHistory(previousParams, false, replacePrevious); - } + downloadData: function DownloadManager_downloadData(data, filename, + contentType) { + if (navigator.msSaveBlob) { // IE10 and above + return navigator.msSaveBlob(new Blob([data], { type: contentType }), + filename); } - this._pushOrReplaceState(this._stateObj(params), - (overwrite || this.uid === 0)); - this.currentUid = this.uid++; - this.current = params; - this.updatePreviousBookmark = true; + + var blobUrl = pdfjsLib.createObjectURL(data, contentType, + pdfjsLib.PDFJS.disableCreateObjectURL); + download(blobUrl, filename); }, - _goTo: function pdfHistory_goTo(state) { - if (!(this.initialized && this.historyUnlocked && - this._isStateObjectDefined(state))) { + download: function DownloadManager_download(blob, url, filename) { + if (!URL) { + // URL.createObjectURL is not supported + this.downloadUrl(url, filename); return; } - if (!this.reInitialized && state.uid < this.currentUid) { - var previousParams = this._getPreviousParams(true); - if (previousParams) { - this._pushToHistory(this.current, false); - this._pushToHistory(previousParams, false); - this.currentUid = state.uid; - window.history.back(); - return; + + if (navigator.msSaveBlob) { + // IE10 / IE11 + if (!navigator.msSaveBlob(blob, filename)) { + this.downloadUrl(url, filename); } + return; } - this.historyUnlocked = false; - if (state.target.dest) { - this.linkService.navigateTo(state.target.dest); - } else { - this.linkService.setHash(state.target.hash); - } - this.currentUid = state.uid; - if (state.uid > this.uid) { - this.uid = state.uid; - } - this.current = state.target; - this.updatePreviousBookmark = true; + var blobUrl = URL.createObjectURL(blob); + download(blobUrl, filename); + } + }; - var currentHash = window.location.hash.substring(1); - if (this.previousHash !== currentHash) { - this.allowHashChange = false; - } - this.previousHash = currentHash; - - this.historyUnlocked = true; - }, - - back: function pdfHistoryBack() { - this.go(-1); - }, - - forward: function pdfHistoryForward() { - this.go(1); - }, - - go: function pdfHistoryGo(direction) { - if (this.initialized && this.historyUnlocked) { - var state = window.history.state; - if (direction === -1 && state && state.uid > 0) { - window.history.back(); - } else if (direction === 1 && state && state.uid < (this.uid - 1)) { - window.history.forward(); - } - } - } - }; - - exports.PDFHistory = PDFHistory; + exports.DownloadManager = DownloadManager; })); (function (root, factory) { { - factory((root.pdfjsWebPDFRenderingQueue = {})); + factory((root.pdfjsWebUIUtils = {}), root.pdfjsWebPDFJS); } -}(this, function (exports) { +}(this, function (exports, pdfjsLib) { -var CLEANUP_TIMEOUT = 30000; +var CSS_UNITS = 96.0 / 72.0; +var DEFAULT_SCALE_VALUE = 'auto'; +var DEFAULT_SCALE = 1.0; +var UNKNOWN_SCALE = 0; +var MAX_AUTO_SCALE = 1.25; +var SCROLLBAR_PADDING = 40; +var VERTICAL_PADDING = 5; -var RenderingStates = { - INITIAL: 0, - RUNNING: 1, - PAUSED: 2, - FINISHED: 3 -}; +var mozL10n = document.mozL10n || document.webL10n; + +var PDFJS = pdfjsLib.PDFJS; /** - * Controls rendering of the views for pages and thumbnails. - * @class + * Disables fullscreen support, and by extension Presentation Mode, + * in browsers which support the fullscreen API. + * @var {boolean} */ -var PDFRenderingQueue = (function PDFRenderingQueueClosure() { - /** - * @constructs - */ - function PDFRenderingQueue() { - this.pdfViewer = null; - this.pdfThumbnailViewer = null; - this.onIdle = null; +PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ? + false : PDFJS.disableFullscreen); - this.highestPriorityPage = null; - this.idleTimeout = null; - this.printing = false; - this.isThumbnailViewEnabled = false; - } +/** + * Enables CSS only zooming. + * @var {boolean} + */ +PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ? + false : PDFJS.useOnlyCssZoom); - PDFRenderingQueue.prototype = /** @lends PDFRenderingQueue.prototype */ { - /** - * @param {PDFViewer} pdfViewer - */ - setViewer: function PDFRenderingQueue_setViewer(pdfViewer) { - this.pdfViewer = pdfViewer; - }, +/** + * The maximum supported canvas size in total pixels e.g. width * height. + * The default value is 4096 * 4096. Use -1 for no limit. + * @var {number} + */ +PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? + 16777216 : PDFJS.maxCanvasPixels); - /** - * @param {PDFThumbnailViewer} pdfThumbnailViewer - */ - setThumbnailViewer: - function PDFRenderingQueue_setThumbnailViewer(pdfThumbnailViewer) { - this.pdfThumbnailViewer = pdfThumbnailViewer; - }, +/** + * Disables saving of the last position of the viewed PDF. + * @var {boolean} + */ +PDFJS.disableHistory = (PDFJS.disableHistory === undefined ? + false : PDFJS.disableHistory); - /** - * @param {IRenderableView} view - * @returns {boolean} - */ - isHighestPriority: function PDFRenderingQueue_isHighestPriority(view) { - return this.highestPriorityPage === view.renderingId; - }, +/** + * Disables creation of the text layer that used for text selection and search. + * @var {boolean} + */ +PDFJS.disableTextLayer = (PDFJS.disableTextLayer === undefined ? + false : PDFJS.disableTextLayer); - renderHighestPriority: function - PDFRenderingQueue_renderHighestPriority(currentlyVisiblePages) { - if (this.idleTimeout) { - clearTimeout(this.idleTimeout); - this.idleTimeout = null; - } +/** + * Disables maintaining the current position in the document when zooming. + */ +PDFJS.ignoreCurrentPositionOnZoom = (PDFJS.ignoreCurrentPositionOnZoom === + undefined ? false : PDFJS.ignoreCurrentPositionOnZoom); - // Pages have a higher priority than thumbnails, so check them first. - if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { - return; - } - // No pages needed rendering so check thumbnails. - if (this.pdfThumbnailViewer && this.isThumbnailViewEnabled) { - if (this.pdfThumbnailViewer.forceRendering()) { - return; - } - } +/** + * Interface locale settings. + * @var {string} + */ +PDFJS.locale = (PDFJS.locale === undefined ? navigator.language : PDFJS.locale); - if (this.printing) { - // If printing is currently ongoing do not reschedule cleanup. - return; - } +/** + * Returns scale factor for the canvas. It makes sense for the HiDPI displays. + * @return {Object} The object with horizontal (sx) and vertical (sy) + scales. The scaled property is set to false if scaling is + not required, true otherwise. + */ +function getOutputScale(ctx) { + var devicePixelRatio = window.devicePixelRatio || 1; + var backingStoreRatio = ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1; + var pixelRatio = devicePixelRatio / backingStoreRatio; + return { + sx: pixelRatio, + sy: pixelRatio, + scaled: pixelRatio !== 1 + }; +} - if (this.onIdle) { - this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); - } - }, +/** + * Scrolls specified element into view of its parent. + * @param {Object} element - The element to be visible. + * @param {Object} spot - An object with optional top and left properties, + * specifying the offset from the top left edge. + * @param {boolean} skipOverflowHiddenElements - Ignore elements that have + * the CSS rule `overflow: hidden;` set. The default is false. + */ +function scrollIntoView(element, spot, skipOverflowHiddenElements) { + // Assuming offsetParent is available (it's not available when viewer is in + // hidden iframe or object). We have to scroll: if the offsetParent is not set + // producing the error. See also animationStartedClosure. + var parent = element.offsetParent; + if (!parent) { + console.error('offsetParent is not set -- cannot scroll'); + return; + } + var checkOverflow = skipOverflowHiddenElements || false; + var offsetY = element.offsetTop + element.clientTop; + var offsetX = element.offsetLeft + element.clientLeft; + while (parent.clientHeight === parent.scrollHeight || + (checkOverflow && getComputedStyle(parent).overflow === 'hidden')) { + if (parent.dataset._scaleY) { + offsetY /= parent.dataset._scaleY; + offsetX /= parent.dataset._scaleX; + } + offsetY += parent.offsetTop; + offsetX += parent.offsetLeft; + parent = parent.offsetParent; + if (!parent) { + return; // no need to scroll + } + } + if (spot) { + if (spot.top !== undefined) { + offsetY += spot.top; + } + if (spot.left !== undefined) { + offsetX += spot.left; + parent.scrollLeft = offsetX; + } + } + parent.scrollTop = offsetY; +} - getHighestPriority: function - PDFRenderingQueue_getHighestPriority(visible, views, scrolledDown) { - // The state has changed figure out which page has the highest priority to - // render next (if any). - // Priority: - // 1 visible pages - // 2 if last scrolled down page after the visible pages - // 2 if last scrolled up page before the visible pages - var visibleViews = visible.views; +/** + * Helper function to start monitoring the scroll event and converting them into + * PDF.js friendly one: with scroll debounce and scroll direction. + */ +function watchScroll(viewAreaElement, callback) { + var debounceScroll = function debounceScroll(evt) { + if (rAF) { + return; + } + // schedule an invocation of scroll for next animation frame. + rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { + rAF = null; - var numVisible = visibleViews.length; - if (numVisible === 0) { - return false; - } - for (var i = 0; i < numVisible; ++i) { - var view = visibleViews[i].view; - if (!this.isViewFinished(view)) { - return view; - } + var currentY = viewAreaElement.scrollTop; + var lastY = state.lastY; + if (currentY !== lastY) { + state.down = currentY > lastY; } + state.lastY = currentY; + callback(state); + }); + }; - // All the visible views have rendered, try to render next/previous pages. - if (scrolledDown) { - var nextPageIndex = visible.last.id; - // ID's start at 1 so no need to add 1. - if (views[nextPageIndex] && - !this.isViewFinished(views[nextPageIndex])) { - return views[nextPageIndex]; - } - } else { - var previousPageIndex = visible.first.id - 2; - if (views[previousPageIndex] && - !this.isViewFinished(views[previousPageIndex])) { - return views[previousPageIndex]; - } - } - // Everything that needs to be rendered has been. - return null; - }, - - /** - * @param {IRenderableView} view - * @returns {boolean} - */ - isViewFinished: function PDFRenderingQueue_isViewFinished(view) { - return view.renderingState === RenderingStates.FINISHED; - }, - - /** - * Render a page or thumbnail view. This calls the appropriate function - * based on the views state. If the view is already rendered it will return - * false. - * @param {IRenderableView} view - */ - renderView: function PDFRenderingQueue_renderView(view) { - var state = view.renderingState; - switch (state) { - case RenderingStates.FINISHED: - return false; - case RenderingStates.PAUSED: - this.highestPriorityPage = view.renderingId; - view.resume(); - break; - case RenderingStates.RUNNING: - this.highestPriorityPage = view.renderingId; - break; - case RenderingStates.INITIAL: - this.highestPriorityPage = view.renderingId; - var continueRendering = function () { - this.renderHighestPriority(); - }.bind(this); - view.draw().then(continueRendering, continueRendering); - break; - } - return true; - }, + var state = { + down: true, + lastY: viewAreaElement.scrollTop, + _eventHandler: debounceScroll }; - return PDFRenderingQueue; -})(); + var rAF = null; + viewAreaElement.addEventListener('scroll', debounceScroll, true); + return state; +} -exports.RenderingStates = RenderingStates; -exports.PDFRenderingQueue = PDFRenderingQueue; -})); +/** + * Helper function to parse query string (e.g. ?param1=value&parm2=...). + */ +function parseQueryString(query) { + var parts = query.split('&'); + var params = {}; + for (var i = 0, ii = parts.length; i < ii; ++i) { + var param = parts[i].split('='); + var key = param[0].toLowerCase(); + var value = param.length > 1 ? param[1] : null; + params[decodeURIComponent(key)] = decodeURIComponent(value); + } + return params; +} +/** + * Use binary search to find the index of the first item in a given array which + * passes a given condition. The items are expected to be sorted in the sense + * that if the condition is true for one item in the array, then it is also true + * for all following items. + * + * @returns {Number} Index of the first array element to pass the test, + * or |items.length| if no such element exists. + */ +function binarySearchFirstItem(items, condition) { + var minIndex = 0; + var maxIndex = items.length - 1; -(function (root, factory) { - { - factory((root.pdfjsWebDownloadManager = {}), root.pdfjsWebPDFJS); + if (items.length === 0 || !condition(items[maxIndex])) { + return items.length; } -}(this, function (exports, pdfjsLib) { - function download(blobUrl, filename) { - var a = document.createElement('a'); - if (a.click) { - // Use a.click() if available. Otherwise, Chrome might show - // "Unsafe JavaScript attempt to initiate a navigation change - // for frame with URL" and not open the PDF at all. - // Supported by (not mentioned = untested): - // - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click) - // - Chrome 19 - 26 (18- does not support a.click) - // - Opera 9 - 12.15 - // - Internet Explorer 6 - 10 - // - Safari 6 (5.1- does not support a.click) - a.href = blobUrl; - a.target = '_parent'; - // Use a.download if available. This increases the likelihood that - // the file is downloaded instead of opened by another PDF plugin. - if ('download' in a) { - a.download = filename; - } - // must be in the document for IE and recent Firefox versions. - // (otherwise .click() is ignored) - (document.body || document.documentElement).appendChild(a); - a.click(); - a.parentNode.removeChild(a); + if (condition(items[minIndex])) { + return minIndex; + } + + while (minIndex < maxIndex) { + var currentIndex = (minIndex + maxIndex) >> 1; + var currentItem = items[currentIndex]; + if (condition(currentItem)) { + maxIndex = currentIndex; } else { - if (window.top === window && - blobUrl.split('#')[0] === window.location.href.split('#')[0]) { - // If _parent == self, then opening an identical URL with different - // location hash will only cause a navigation, not a download. - var padCharacter = blobUrl.indexOf('?') === -1 ? '?' : '&'; - blobUrl = blobUrl.replace(/#|$/, padCharacter + '$&'); - } - window.open(blobUrl, '_parent'); + minIndex = currentIndex + 1; } } + return minIndex; /* === maxIndex */ +} - function DownloadManager() {} +/** + * Approximates float number as a fraction using Farey sequence (max order + * of 8). + * @param {number} x - Positive float number. + * @returns {Array} Estimated fraction: the first array item is a numerator, + * the second one is a denominator. + */ +function approximateFraction(x) { + // Fast paths for int numbers or their inversions. + if (Math.floor(x) === x) { + return [x, 1]; + } + var xinv = 1 / x; + var limit = 8; + if (xinv > limit) { + return [1, limit]; + } else if (Math.floor(xinv) === xinv) { + return [1, xinv]; + } - DownloadManager.prototype = { - downloadUrl: function DownloadManager_downloadUrl(url, filename) { - if (!pdfjsLib.isValidUrl(url, true)) { - return; // restricted/invalid URL - } + var x_ = x > 1 ? xinv : x; + // a/b and c/d are neighbours in Farey sequence. + var a = 0, b = 1, c = 1, d = 1; + // Limiting search to order 8. + while (true) { + // Generating next term in sequence (order of q). + var p = a + c, q = b + d; + if (q > limit) { + break; + } + if (x_ <= p / q) { + c = p; d = q; + } else { + a = p; b = q; + } + } + // Select closest of the neighbours to x. + if (x_ - a / b < c / d - x_) { + return x_ === x ? [a, b] : [b, a]; + } else { + return x_ === x ? [c, d] : [d, c]; + } +} - download(url + '#pdfjs.action=download', filename); - }, +function roundToDivide(x, div) { + var r = x % div; + return r === 0 ? x : Math.round(x - r + div); +} - downloadData: function DownloadManager_downloadData(data, filename, - contentType) { - if (navigator.msSaveBlob) { // IE10 and above - return navigator.msSaveBlob(new Blob([data], { type: contentType }), - filename); - } +/** + * Generic helper to find out what elements are visible within a scroll pane. + */ +function getVisibleElements(scrollEl, views, sortByVisibility) { + var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; + var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; - var blobUrl = pdfjsLib.createObjectURL(data, contentType, - pdfjsLib.PDFJS.disableCreateObjectURL); - download(blobUrl, filename); - }, + function isElementBottomBelowViewTop(view) { + var element = view.div; + var elementBottom = + element.offsetTop + element.clientTop + element.clientHeight; + return elementBottom > top; + } - download: function DownloadManager_download(blob, url, filename) { - if (!URL) { - // URL.createObjectURL is not supported - this.downloadUrl(url, filename); - return; - } + var visible = [], view, element; + var currentHeight, viewHeight, hiddenHeight, percentHeight; + var currentWidth, viewWidth; + var firstVisibleElementInd = (views.length === 0) ? 0 : + binarySearchFirstItem(views, isElementBottomBelowViewTop); - if (navigator.msSaveBlob) { - // IE10 / IE11 - if (!navigator.msSaveBlob(blob, filename)) { - this.downloadUrl(url, filename); - } - return; - } + for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) { + view = views[i]; + element = view.div; + currentHeight = element.offsetTop + element.clientTop; + viewHeight = element.clientHeight; - var blobUrl = URL.createObjectURL(blob); - download(blobUrl, filename); + if (currentHeight > bottom) { + break; } - }; - - exports.DownloadManager = DownloadManager; -})); + currentWidth = element.offsetLeft + element.clientLeft; + viewWidth = element.clientWidth; + if (currentWidth + viewWidth < left || currentWidth > right) { + continue; + } + hiddenHeight = Math.max(0, top - currentHeight) + + Math.max(0, currentHeight + viewHeight - bottom); + percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; -(function (root, factory) { - { - factory((root.pdfjsWebTextLayerBuilder = {}), root.pdfjsWebPDFJS); + visible.push({ + id: view.id, + x: currentWidth, + y: currentHeight, + view: view, + percent: percentHeight + }); } -}(this, function (exports, pdfjsLib) { + + var first = visible[0]; + var last = visible[visible.length - 1]; + + if (sortByVisibility) { + visible.sort(function(a, b) { + var pc = a.percent - b.percent; + if (Math.abs(pc) > 0.001) { + return -pc; + } + return a.id - b.id; // ensure stability + }); + } + return {first: first, last: last, views: visible}; +} /** - * @typedef {Object} TextLayerBuilderOptions - * @property {HTMLDivElement} textLayerDiv - The text layer container. - * @property {number} pageIndex - The page index. - * @property {PageViewport} viewport - The viewport of the text layer. - * @property {PDFFindController} findController + * Event handler to suppress context menu. */ +function noContextMenuHandler(e) { + e.preventDefault(); +} /** - * TextLayerBuilder provides text-selection functionality for the PDF. - * It does this by creating overlay divs over the PDF text. These divs - * contain text that matches the PDF text they are overlaying. This object - * also provides a way to highlight text that is being searched for. - * @class + * Returns the filename or guessed filename from the url (see issue 3455). + * url {String} The original PDF location. + * @return {String} Guessed PDF file name. */ -var TextLayerBuilder = (function TextLayerBuilderClosure() { - function TextLayerBuilder(options) { - this.textLayerDiv = options.textLayerDiv; - this.renderingDone = false; - this.divContentDone = false; - this.pageIdx = options.pageIndex; - this.pageNumber = this.pageIdx + 1; - this.matches = []; - this.viewport = options.viewport; - this.textDivs = []; - this.findController = options.findController || null; - this.textLayerRenderTask = null; - this._bindMouse(); +function getPDFFileNameFromURL(url) { + var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; + // SCHEME HOST 1.PATH 2.QUERY 3.REF + // Pattern to get last matching NAME.pdf + var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; + var splitURI = reURI.exec(url); + var suggestedFilename = reFilename.exec(splitURI[1]) || + reFilename.exec(splitURI[2]) || + reFilename.exec(splitURI[3]); + if (suggestedFilename) { + suggestedFilename = suggestedFilename[0]; + if (suggestedFilename.indexOf('%') !== -1) { + // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf + try { + suggestedFilename = + reFilename.exec(decodeURIComponent(suggestedFilename))[0]; + } catch(e) { // Possible (extremely rare) errors: + // URIError "Malformed URI", e.g. for "%AA.pdf" + // TypeError "null has no properties", e.g. for "%2F.pdf" + } + } } + return suggestedFilename || 'document.pdf'; +} - TextLayerBuilder.prototype = { - _finishRendering: function TextLayerBuilder_finishRendering() { - this.renderingDone = true; - - var endOfContent = document.createElement('div'); - endOfContent.className = 'endOfContent'; - this.textLayerDiv.appendChild(endOfContent); - - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('textlayerrendered', true, true, { - pageNumber: this.pageNumber - }); - this.textLayerDiv.dispatchEvent(event); - }, - - /** - * Renders the text layer. - * @param {number} timeout (optional) if specified, the rendering waits - * for specified amount of ms. - */ - render: function TextLayerBuilder_render(timeout) { - if (!this.divContentDone || this.renderingDone) { +/** + * Simple event bus for an application. Listeners are attached using the + * `on` and `off` methods. To raise an event, the `dispatch` method shall be + * used. + */ +var EventBus = (function EventBusClosure() { + function EventBus() { + this._listeners = Object.create(null); + } + EventBus.prototype = { + on: function EventBus_on(eventName, listener) { + var eventListeners = this._listeners[eventName]; + if (!eventListeners) { + eventListeners = []; + this._listeners[eventName] = eventListeners; + } + eventListeners.push(listener); + }, + off: function EventBus_on(eventName, listener) { + var eventListeners = this._listeners[eventName]; + var i; + if (!eventListeners || ((i = eventListeners.indexOf(listener)) < 0)) { return; } - - if (this.textLayerRenderTask) { - this.textLayerRenderTask.cancel(); - this.textLayerRenderTask = null; + eventListeners.splice(i, 1); + }, + dispatch: function EventBus_dispath(eventName) { + var eventListeners = this._listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; } - - this.textDivs = []; - var textLayerFrag = document.createDocumentFragment(); - this.textLayerRenderTask = pdfjsLib.renderTextLayer({ - textContent: this.textContent, - container: textLayerFrag, - viewport: this.viewport, - textDivs: this.textDivs, - timeout: timeout - }); - this.textLayerRenderTask.promise.then(function () { - this.textLayerDiv.appendChild(textLayerFrag); - this._finishRendering(); - this.updateMatches(); - }.bind(this), function (reason) { - // canceled or failed to render text layer -- skipping errors + // Passing all arguments after the eventName to the listeners. + var args = Array.prototype.slice.call(arguments, 1); + // Making copy of the listeners array in case if it will be modified + // during dispatch. + eventListeners.slice(0).forEach(function (listener) { + listener.apply(null, args); }); - }, + } + }; + return EventBus; +})(); - setTextContent: function TextLayerBuilder_setTextContent(textContent) { - if (this.textLayerRenderTask) { - this.textLayerRenderTask.cancel(); - this.textLayerRenderTask = null; - } - this.textContent = textContent; - this.divContentDone = true; - }, +var ProgressBar = (function ProgressBarClosure() { - convertMatches: function TextLayerBuilder_convertMatches(matches) { - var i = 0; - var iIndex = 0; - var bidiTexts = this.textContent.items; - var end = bidiTexts.length - 1; - var queryLen = (this.findController === null ? - 0 : this.findController.state.query.length); - var ret = []; + function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); + } - for (var m = 0, len = matches.length; m < len; m++) { - // Calculate the start position. - var matchIdx = matches[m]; + function ProgressBar(id, opts) { + this.visible = true; - // Loop over the divIdxs. - while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { - iIndex += bidiTexts[i].str.length; - i++; - } + // Fetch the sub-elements for later. + this.div = document.querySelector(id + ' .progress'); - if (i === bidiTexts.length) { - console.error('Could not find a matching mapping'); - } + // Get the loading bar element, so it can be resized to fit the viewer. + this.bar = this.div.parentNode; - var match = { - begin: { - divIdx: i, - offset: matchIdx - iIndex - } - }; + // Get options, with sensible defaults. + this.height = opts.height || 100; + this.width = opts.width || 100; + this.units = opts.units || '%'; - // Calculate the end position. - matchIdx += queryLen; + // Initialize heights. + this.div.style.height = this.height + this.units; + this.percent = 0; + } - // Somewhat the same array as above, but use > instead of >= to get - // the end position right. - while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { - iIndex += bidiTexts[i].str.length; - i++; - } + ProgressBar.prototype = { - match.end = { - divIdx: i, - offset: matchIdx - iIndex - }; - ret.push(match); + updateBar: function ProgressBar_updateBar() { + if (this._indeterminate) { + this.div.classList.add('indeterminate'); + this.div.style.width = this.width + this.units; + return; } - return ret; + this.div.classList.remove('indeterminate'); + var progressSize = this.width * this._percent / 100; + this.div.style.width = progressSize + this.units; }, - renderMatches: function TextLayerBuilder_renderMatches(matches) { - // Early exit if there is nothing to render. - if (matches.length === 0) { - return; - } - - var bidiTexts = this.textContent.items; - var textDivs = this.textDivs; - var prevEnd = null; - var pageIdx = this.pageIdx; - var isSelectedPage = (this.findController === null ? - false : (pageIdx === this.findController.selected.pageIdx)); - var selectedMatchIdx = (this.findController === null ? - -1 : this.findController.selected.matchIdx); - var highlightAll = (this.findController === null ? - false : this.findController.state.highlightAll); - var infinity = { - divIdx: -1, - offset: undefined - }; + get percent() { + return this._percent; + }, - function beginText(begin, className) { - var divIdx = begin.divIdx; - textDivs[divIdx].textContent = ''; - appendTextToDiv(divIdx, 0, begin.offset, className); - } + set percent(val) { + this._indeterminate = isNaN(val); + this._percent = clamp(val, 0, 100); + this.updateBar(); + }, - function appendTextToDiv(divIdx, fromOffset, toOffset, className) { - var div = textDivs[divIdx]; - var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); - var node = document.createTextNode(content); - if (className) { - var span = document.createElement('span'); - span.className = className; - span.appendChild(node); - div.appendChild(span); - return; + setWidth: function ProgressBar_setWidth(viewer) { + if (viewer) { + var container = viewer.parentNode; + var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; + if (scrollbarWidth > 0) { + this.bar.setAttribute('style', 'width: calc(100% - ' + + scrollbarWidth + 'px);'); } - div.appendChild(node); } + }, - var i0 = selectedMatchIdx, i1 = i0 + 1; - if (highlightAll) { - i0 = 0; - i1 = matches.length; - } else if (!isSelectedPage) { - // Not highlighting all and this isn't the selected page, so do nothing. + hide: function ProgressBar_hide() { + if (!this.visible) { return; } - - for (var i = i0; i < i1; i++) { - var match = matches[i]; - var begin = match.begin; - var end = match.end; - var isSelected = (isSelectedPage && i === selectedMatchIdx); - var highlightSuffix = (isSelected ? ' selected' : ''); - - if (this.findController) { - this.findController.updateMatchPosition(pageIdx, i, textDivs, - begin.divIdx, end.divIdx); - } - - // Match inside new div. - if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { - // If there was a previous div, then add the text at the end. - if (prevEnd !== null) { - appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); - } - // Clear the divs and set the content until the starting point. - beginText(begin); - } else { - appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); - } - - if (begin.divIdx === end.divIdx) { - appendTextToDiv(begin.divIdx, begin.offset, end.offset, - 'highlight' + highlightSuffix); - } else { - appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, - 'highlight begin' + highlightSuffix); - for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { - textDivs[n0].className = 'highlight middle' + highlightSuffix; - } - beginText(end, 'highlight end' + highlightSuffix); - } - prevEnd = end; - } - - if (prevEnd) { - appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); - } + this.visible = false; + this.bar.classList.add('hidden'); + document.body.classList.remove('loadingInProgress'); }, - updateMatches: function TextLayerBuilder_updateMatches() { - // Only show matches when all rendering is done. - if (!this.renderingDone) { + show: function ProgressBar_show() { + if (this.visible) { return; } + this.visible = true; + document.body.classList.add('loadingInProgress'); + this.bar.classList.remove('hidden'); + } + }; - // Clear all matches. - var matches = this.matches; - var textDivs = this.textDivs; - var bidiTexts = this.textContent.items; - var clearedUntilDivIdx = -1; + return ProgressBar; +})(); - // Clear all current matches. - for (var i = 0, len = matches.length; i < len; i++) { - var match = matches[i]; - var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); - for (var n = begin, end = match.end.divIdx; n <= end; n++) { - var div = textDivs[n]; - div.textContent = bidiTexts[n].str; - div.className = ''; - } - clearedUntilDivIdx = match.end.divIdx + 1; - } +exports.CSS_UNITS = CSS_UNITS; +exports.DEFAULT_SCALE_VALUE = DEFAULT_SCALE_VALUE; +exports.DEFAULT_SCALE = DEFAULT_SCALE; +exports.UNKNOWN_SCALE = UNKNOWN_SCALE; +exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE; +exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING; +exports.VERTICAL_PADDING = VERTICAL_PADDING; +exports.mozL10n = mozL10n; +exports.EventBus = EventBus; +exports.ProgressBar = ProgressBar; +exports.getPDFFileNameFromURL = getPDFFileNameFromURL; +exports.noContextMenuHandler = noContextMenuHandler; +exports.parseQueryString = parseQueryString; +exports.getVisibleElements = getVisibleElements; +exports.roundToDivide = roundToDivide; +exports.approximateFraction = approximateFraction; +exports.getOutputScale = getOutputScale; +exports.scrollIntoView = scrollIntoView; +exports.watchScroll = watchScroll; +exports.binarySearchFirstItem = binarySearchFirstItem; +})); - if (this.findController === null || !this.findController.active) { - return; - } - // Convert the matches on the page controller into the match format - // used for the textLayer. - this.matches = this.convertMatches(this.findController === null ? - [] : (this.findController.pageMatches[this.pageIdx] || [])); - this.renderMatches(this.matches); - }, +(function (root, factory) { + { + factory((root.pdfjsWebDOMEvents = {}), root.pdfjsWebUIUtils); + } +}(this, function (exports, uiUtils) { + var EventBus = uiUtils.EventBus; - /** - * Fixes text selection: adds additional div where mouse was clicked. - * This reduces flickering of the content if mouse slowly dragged down/up. - * @private - */ - _bindMouse: function TextLayerBuilder_bindMouse() { - var div = this.textLayerDiv; - div.addEventListener('mousedown', function (e) { - var end = div.querySelector('.endOfContent'); - if (!end) { - return; - } - // On non-Firefox browsers, the selection will feel better if the height - // of the endOfContent div will be adjusted to start at mouse click - // location -- this will avoid flickering when selections moves up. - // However it does not work when selection started on empty space. - var adjustTop = e.target !== div; - adjustTop = adjustTop && window.getComputedStyle(end). - getPropertyValue('-moz-user-select') !== 'none'; - if (adjustTop) { - var divBounds = div.getBoundingClientRect(); - var r = Math.max(0, (e.pageY - divBounds.top) / divBounds.height); - end.style.top = (r * 100).toFixed(2) + '%'; - } - end.classList.add('active'); + // Attaching to the application event bus to dispatch events to the DOM for + // backwards viewer API compatibility. + function attachDOMEventsToEventBus(eventBus) { + eventBus.on('documentload', function () { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('documentload', true, true, {}); + window.dispatchEvent(event); + }); + eventBus.on('pagerendered', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagerendered', true, true, { + pageNumber: e.pageNumber, + cssTransform: e.cssTransform, }); - div.addEventListener('mouseup', function (e) { - var end = div.querySelector('.endOfContent'); - if (!end) { - return; - } - end.style.top = ''; - end.classList.remove('active'); + e.source.div.dispatchEvent(event); + }); + eventBus.on('textlayerrendered', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('textlayerrendered', true, true, { + pageNumber: e.pageNumber }); - }, - }; - return TextLayerBuilder; -})(); - -/** - * @constructor - * @implements IPDFTextLayerFactory - */ -function DefaultTextLayerFactory() {} -DefaultTextLayerFactory.prototype = { - /** - * @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 + e.source.textLayerDiv.dispatchEvent(event); + }); + eventBus.on('pagechange', function (e) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('pagechange', true, true, window, 0); + event.updateInProgress = e.updateInProgress; + event.pageNumber = e.pageNumber; + event.previousPageNumber = e.previousPageNumber; + e.source.container.dispatchEvent(event); + }); + eventBus.on('pagesinit', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagesinit', true, true, null); + e.source.container.dispatchEvent(event); + }); + eventBus.on('pagesloaded', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagesloaded', true, true, { + pagesCount: e.pagesCount + }); + e.source.container.dispatchEvent(event); + }); + eventBus.on('scalechange', function (e) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('scalechange', true, true, window, 0); + event.scale = e.scale; + event.presetValue = e.presetValue; + e.source.container.dispatchEvent(event); + }); + eventBus.on('updateviewarea', function (e) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('updateviewarea', true, true, window, 0); + event.location = e.location; + e.source.container.dispatchEvent(event); + }); + eventBus.on('find', function (e) { + if (e.source === window) { + return; // event comes from FirefoxCom, no need to replicate + } + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('find' + e.type, true, true, { + query: e.query, + caseSensitive: e.caseSensitive, + highlightAll: e.highlightAll, + findPrevious: e.findPrevious + }); + window.dispatchEvent(event); + }); + eventBus.on('attachmentsloaded', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('attachmentsloaded', true, true, { + attachmentsCount: e.attachmentsCount + }); + e.source.container.dispatchEvent(event); + }); + eventBus.on('sidebarviewchanged', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('sidebarviewchanged', true, true, { + view: e.view, + }); + e.source.outerContainer.dispatchEvent(event); + }); + eventBus.on('pagemode', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagemode', true, true, { + mode: e.mode, + }); + e.source.pdfViewer.container.dispatchEvent(event); + }); + eventBus.on('namedaction', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('namedaction', true, true, { + action: e.action + }); + e.source.pdfViewer.container.dispatchEvent(event); + }); + eventBus.on('presentationmodechanged', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('presentationmodechanged', true, true, { + active: e.active, + switchInProgress: e.switchInProgress + }); + window.dispatchEvent(event); + }); + eventBus.on('outlineloaded', function (e) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('outlineloaded', true, true, { + outlineCount: e.outlineCount + }); + e.source.container.dispatchEvent(event); }); } -}; -exports.TextLayerBuilder = TextLayerBuilder; -exports.DefaultTextLayerFactory = DefaultTextLayerFactory; + var globalEventBus = null; + function getGlobalEventBus() { + if (globalEventBus) { + return globalEventBus; + } + globalEventBus = new EventBus(); + attachDOMEventsToEventBus(globalEventBus); + return globalEventBus; + } + + exports.attachDOMEventsToEventBus = attachDOMEventsToEventBus; + exports.getGlobalEventBus = getGlobalEventBus; })); (function (root, factory) { { - factory((root.pdfjsWebUIUtils = {}), root.pdfjsWebPDFJS); + factory((root.pdfjsWebPDFFindController = {}), root.pdfjsWebUIUtils); } -}(this, function (exports, pdfjsLib) { +}(this, function (exports, uiUtils, firefoxCom) { -var CSS_UNITS = 96.0 / 72.0; -var DEFAULT_SCALE_VALUE = 'auto'; -var DEFAULT_SCALE = 1.0; -var UNKNOWN_SCALE = 0; -var MAX_AUTO_SCALE = 1.25; -var SCROLLBAR_PADDING = 40; -var VERTICAL_PADDING = 5; +var scrollIntoView = uiUtils.scrollIntoView; -var mozL10n = document.mozL10n || document.webL10n; +var FindStates = { + FIND_FOUND: 0, + FIND_NOTFOUND: 1, + FIND_WRAPPED: 2, + FIND_PENDING: 3 +}; -var PDFJS = pdfjsLib.PDFJS; +var FIND_SCROLL_OFFSET_TOP = -50; +var FIND_SCROLL_OFFSET_LEFT = -400; -/** - * Disables fullscreen support, and by extension Presentation Mode, - * in browsers which support the fullscreen API. - * @var {boolean} - */ -PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ? - false : PDFJS.disableFullscreen); +var CHARACTERS_TO_NORMALIZE = { + '\u2018': '\'', // Left single quotation mark + '\u2019': '\'', // Right single quotation mark + '\u201A': '\'', // Single low-9 quotation mark + '\u201B': '\'', // Single high-reversed-9 quotation mark + '\u201C': '"', // Left double quotation mark + '\u201D': '"', // Right double quotation mark + '\u201E': '"', // Double low-9 quotation mark + '\u201F': '"', // Double high-reversed-9 quotation mark + '\u00BC': '1/4', // Vulgar fraction one quarter + '\u00BD': '1/2', // Vulgar fraction one half + '\u00BE': '3/4', // Vulgar fraction three quarters +}; /** - * Enables CSS only zooming. - * @var {boolean} - */ -PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ? - false : PDFJS.useOnlyCssZoom); - -/** - * The maximum supported canvas size in total pixels e.g. width * height. - * The default value is 4096 * 4096. Use -1 for no limit. - * @var {number} - */ -PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? - 16777216 : PDFJS.maxCanvasPixels); - -/** - * Disables saving of the last position of the viewed PDF. - * @var {boolean} - */ -PDFJS.disableHistory = (PDFJS.disableHistory === undefined ? - false : PDFJS.disableHistory); - -/** - * Disables creation of the text layer that used for text selection and search. - * @var {boolean} - */ -PDFJS.disableTextLayer = (PDFJS.disableTextLayer === undefined ? - false : PDFJS.disableTextLayer); - -/** - * Disables maintaining the current position in the document when zooming. - */ -PDFJS.ignoreCurrentPositionOnZoom = (PDFJS.ignoreCurrentPositionOnZoom === - undefined ? false : PDFJS.ignoreCurrentPositionOnZoom); - -/** - * Interface locale settings. - * @var {string} - */ -PDFJS.locale = (PDFJS.locale === undefined ? navigator.language : PDFJS.locale); - -/** - * Returns scale factor for the canvas. It makes sense for the HiDPI displays. - * @return {Object} The object with horizontal (sx) and vertical (sy) - scales. The scaled property is set to false if scaling is - not required, true otherwise. - */ -function getOutputScale(ctx) { - var devicePixelRatio = window.devicePixelRatio || 1; - var backingStoreRatio = ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1; - var pixelRatio = devicePixelRatio / backingStoreRatio; - return { - sx: pixelRatio, - sy: pixelRatio, - scaled: pixelRatio !== 1 - }; -} - -/** - * Scrolls specified element into view of its parent. - * @param {Object} element - The element to be visible. - * @param {Object} spot - An object with optional top and left properties, - * specifying the offset from the top left edge. - * @param {boolean} skipOverflowHiddenElements - Ignore elements that have - * the CSS rule `overflow: hidden;` set. The default is false. - */ -function scrollIntoView(element, spot, skipOverflowHiddenElements) { - // Assuming offsetParent is available (it's not available when viewer is in - // hidden iframe or object). We have to scroll: if the offsetParent is not set - // producing the error. See also animationStartedClosure. - var parent = element.offsetParent; - if (!parent) { - console.error('offsetParent is not set -- cannot scroll'); - return; - } - var checkOverflow = skipOverflowHiddenElements || false; - var offsetY = element.offsetTop + element.clientTop; - var offsetX = element.offsetLeft + element.clientLeft; - while (parent.clientHeight === parent.scrollHeight || - (checkOverflow && getComputedStyle(parent).overflow === 'hidden')) { - if (parent.dataset._scaleY) { - offsetY /= parent.dataset._scaleY; - offsetX /= parent.dataset._scaleX; - } - offsetY += parent.offsetTop; - offsetX += parent.offsetLeft; - parent = parent.offsetParent; - if (!parent) { - return; // no need to scroll - } - } - if (spot) { - if (spot.top !== undefined) { - offsetY += spot.top; - } - if (spot.left !== undefined) { - offsetX += spot.left; - parent.scrollLeft = offsetX; - } - } - parent.scrollTop = offsetY; -} - -/** - * Helper function to start monitoring the scroll event and converting them into - * PDF.js friendly one: with scroll debounce and scroll direction. + * Provides "search" or "find" functionality for the PDF. + * This object actually performs the search for a given string. */ -function watchScroll(viewAreaElement, callback) { - var debounceScroll = function debounceScroll(evt) { - if (rAF) { - return; - } - // schedule an invocation of scroll for next animation frame. - rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { - rAF = null; - - var currentY = viewAreaElement.scrollTop; - var lastY = state.lastY; - if (currentY !== lastY) { - state.down = currentY > lastY; - } - state.lastY = currentY; - callback(state); - }); - }; +var PDFFindController = (function PDFFindControllerClosure() { + function PDFFindController(options) { + this.pdfViewer = options.pdfViewer || null; - var state = { - down: true, - lastY: viewAreaElement.scrollTop, - _eventHandler: debounceScroll - }; + this.onUpdateResultsCount = null; + this.onUpdateState = null; - var rAF = null; - viewAreaElement.addEventListener('scroll', debounceScroll, true); - return state; -} + this.reset(); -/** - * Helper function to parse query string (e.g. ?param1=value&parm2=...). - */ -function parseQueryString(query) { - var parts = query.split('&'); - var params = {}; - for (var i = 0, ii = parts.length; i < ii; ++i) { - var param = parts[i].split('='); - var key = param[0].toLowerCase(); - var value = param.length > 1 ? param[1] : null; - params[decodeURIComponent(key)] = decodeURIComponent(value); + // Compile the regular expression for text normalization once. + var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''); + this.normalizationRegex = new RegExp('[' + replace + ']', 'g'); } - return params; -} - -/** - * Use binary search to find the index of the first item in a given array which - * passes a given condition. The items are expected to be sorted in the sense - * that if the condition is true for one item in the array, then it is also true - * for all following items. - * - * @returns {Number} Index of the first array element to pass the test, - * or |items.length| if no such element exists. - */ -function binarySearchFirstItem(items, condition) { - var minIndex = 0; - var maxIndex = items.length - 1; - if (items.length === 0 || !condition(items[maxIndex])) { - return items.length; - } - if (condition(items[minIndex])) { - return minIndex; - } + PDFFindController.prototype = { + reset: function PDFFindController_reset() { + this.startedTextExtraction = false; + this.extractTextPromises = []; + this.pendingFindMatches = Object.create(null); + this.active = false; // If active, find results will be highlighted. + this.pageContents = []; // Stores the text for each page. + this.pageMatches = []; + this.matchCount = 0; + this.selected = { // Currently selected match. + pageIdx: -1, + matchIdx: -1 + }; + this.offset = { // Where the find algorithm currently is in the document. + pageIdx: null, + matchIdx: null + }; + this.pagesToSearch = null; + this.resumePageIdx = null; + this.state = null; + this.dirtyMatch = false; + this.findTimeout = null; - while (minIndex < maxIndex) { - var currentIndex = (minIndex + maxIndex) >> 1; - var currentItem = items[currentIndex]; - if (condition(currentItem)) { - maxIndex = currentIndex; - } else { - minIndex = currentIndex + 1; - } - } - return minIndex; /* === maxIndex */ -} + this.firstPagePromise = new Promise(function (resolve) { + this.resolveFirstPage = resolve; + }.bind(this)); + }, -/** - * Approximates float number as a fraction using Farey sequence (max order - * of 8). - * @param {number} x - Positive float number. - * @returns {Array} Estimated fraction: the first array item is a numerator, - * the second one is a denominator. - */ -function approximateFraction(x) { - // Fast paths for int numbers or their inversions. - if (Math.floor(x) === x) { - return [x, 1]; - } - var xinv = 1 / x; - var limit = 8; - if (xinv > limit) { - return [1, limit]; - } else if (Math.floor(xinv) === xinv) { - return [1, xinv]; - } + normalize: function PDFFindController_normalize(text) { + return text.replace(this.normalizationRegex, function (ch) { + return CHARACTERS_TO_NORMALIZE[ch]; + }); + }, - var x_ = x > 1 ? xinv : x; - // a/b and c/d are neighbours in Farey sequence. - var a = 0, b = 1, c = 1, d = 1; - // Limiting search to order 8. - while (true) { - // Generating next term in sequence (order of q). - var p = a + c, q = b + d; - if (q > limit) { - break; - } - if (x_ <= p / q) { - c = p; d = q; - } else { - a = p; b = q; - } - } - // Select closest of the neighbours to x. - if (x_ - a / b < c / d - x_) { - return x_ === x ? [a, b] : [b, a]; - } else { - return x_ === x ? [c, d] : [d, c]; - } -} + calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) { + var pageContent = this.normalize(this.pageContents[pageIndex]); + var query = this.normalize(this.state.query); + var caseSensitive = this.state.caseSensitive; + var queryLen = query.length; -function roundToDivide(x, div) { - var r = x % div; - return r === 0 ? x : Math.round(x - r + div); -} + if (queryLen === 0) { + // Do nothing: the matches should be wiped out already. + return; + } -/** - * Generic helper to find out what elements are visible within a scroll pane. - */ -function getVisibleElements(scrollEl, views, sortByVisibility) { - var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; - var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; + if (!caseSensitive) { + pageContent = pageContent.toLowerCase(); + query = query.toLowerCase(); + } - function isElementBottomBelowViewTop(view) { - var element = view.div; - var elementBottom = - element.offsetTop + element.clientTop + element.clientHeight; - return elementBottom > top; - } + var matches = []; + var matchIdx = -queryLen; + while (true) { + matchIdx = pageContent.indexOf(query, matchIdx + queryLen); + if (matchIdx === -1) { + break; + } + matches.push(matchIdx); + } + this.pageMatches[pageIndex] = matches; + this.updatePage(pageIndex); + if (this.resumePageIdx === pageIndex) { + this.resumePageIdx = null; + this.nextPageMatch(); + } - var visible = [], view, element; - var currentHeight, viewHeight, hiddenHeight, percentHeight; - var currentWidth, viewWidth; - var firstVisibleElementInd = (views.length === 0) ? 0 : - binarySearchFirstItem(views, isElementBottomBelowViewTop); + // Update the matches count + if (matches.length > 0) { + this.matchCount += matches.length; + this.updateUIResultsCount(); + } + }, - for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) { - view = views[i]; - element = view.div; - currentHeight = element.offsetTop + element.clientTop; - viewHeight = element.clientHeight; + extractText: function PDFFindController_extractText() { + if (this.startedTextExtraction) { + return; + } + this.startedTextExtraction = true; - if (currentHeight > bottom) { - break; - } + this.pageContents = []; + var extractTextPromisesResolves = []; + var numPages = this.pdfViewer.pagesCount; + for (var i = 0; i < numPages; i++) { + this.extractTextPromises.push(new Promise(function (resolve) { + extractTextPromisesResolves.push(resolve); + })); + } - currentWidth = element.offsetLeft + element.clientLeft; - viewWidth = element.clientWidth; - if (currentWidth + viewWidth < left || currentWidth > right) { - continue; - } - hiddenHeight = Math.max(0, top - currentHeight) + - Math.max(0, currentHeight + viewHeight - bottom); - percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; + var self = this; + function extractPageText(pageIndex) { + self.pdfViewer.getPageTextContent(pageIndex).then( + function textContentResolved(textContent) { + var textItems = textContent.items; + var str = []; - visible.push({ - id: view.id, - x: currentWidth, - y: currentHeight, - view: view, - percent: percentHeight - }); - } + for (var i = 0, len = textItems.length; i < len; i++) { + str.push(textItems[i].str); + } - var first = visible[0]; - var last = visible[visible.length - 1]; + // Store the pageContent as a string. + self.pageContents.push(str.join('')); - if (sortByVisibility) { - visible.sort(function(a, b) { - var pc = a.percent - b.percent; - if (Math.abs(pc) > 0.001) { - return -pc; + extractTextPromisesResolves[pageIndex](pageIndex); + if ((pageIndex + 1) < self.pdfViewer.pagesCount) { + extractPageText(pageIndex + 1); + } + } + ); } - return a.id - b.id; // ensure stability - }); - } - return {first: first, last: last, views: visible}; -} + extractPageText(0); + }, -/** - * Event handler to suppress context menu. - */ -function noContextMenuHandler(e) { - e.preventDefault(); -} + executeCommand: function PDFFindController_executeCommand(cmd, state) { + if (this.state === null || cmd !== 'findagain') { + this.dirtyMatch = true; + } + this.state = state; + this.updateUIState(FindStates.FIND_PENDING); -/** - * Returns the filename or guessed filename from the url (see issue 3455). - * url {String} The original PDF location. - * @return {String} Guessed PDF file name. - */ -function getPDFFileNameFromURL(url) { - var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; - // SCHEME HOST 1.PATH 2.QUERY 3.REF - // Pattern to get last matching NAME.pdf - var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; - var splitURI = reURI.exec(url); - var suggestedFilename = reFilename.exec(splitURI[1]) || - reFilename.exec(splitURI[2]) || - reFilename.exec(splitURI[3]); - if (suggestedFilename) { - suggestedFilename = suggestedFilename[0]; - if (suggestedFilename.indexOf('%') !== -1) { - // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf - try { - suggestedFilename = - reFilename.exec(decodeURIComponent(suggestedFilename))[0]; - } catch(e) { // Possible (extremely rare) errors: - // URIError "Malformed URI", e.g. for "%AA.pdf" - // TypeError "null has no properties", e.g. for "%2F.pdf" + this.firstPagePromise.then(function() { + this.extractText(); + + clearTimeout(this.findTimeout); + if (cmd === 'find') { + // Only trigger the find action after 250ms of silence. + this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); + } else { + this.nextMatch(); + } + }.bind(this)); + }, + + updatePage: function PDFFindController_updatePage(index) { + if (this.selected.pageIdx === index) { + // If the page is selected, scroll the page into view, which triggers + // rendering the page, which adds the textLayer. Once the textLayer is + // build, it will scroll onto the selected match. + this.pdfViewer.scrollPageIntoView(index + 1); } - } - } - return suggestedFilename || 'document.pdf'; -} -var ProgressBar = (function ProgressBarClosure() { + var page = this.pdfViewer.getPageView(index); + if (page.textLayer) { + page.textLayer.updateMatches(); + } + }, - function clamp(v, min, max) { - return Math.min(Math.max(v, min), max); - } + nextMatch: function PDFFindController_nextMatch() { + var previous = this.state.findPrevious; + var currentPageIndex = this.pdfViewer.currentPageNumber - 1; + var numPages = this.pdfViewer.pagesCount; - function ProgressBar(id, opts) { - this.visible = true; + this.active = true; - // Fetch the sub-elements for later. - this.div = document.querySelector(id + ' .progress'); + if (this.dirtyMatch) { + // Need to recalculate the matches, reset everything. + this.dirtyMatch = false; + this.selected.pageIdx = this.selected.matchIdx = -1; + this.offset.pageIdx = currentPageIndex; + this.offset.matchIdx = null; + this.hadMatch = false; + this.resumePageIdx = null; + this.pageMatches = []; + this.matchCount = 0; + var self = this; - // Get the loading bar element, so it can be resized to fit the viewer. - this.bar = this.div.parentNode; + for (var i = 0; i < numPages; i++) { + // Wipe out any previous highlighted matches. + this.updatePage(i); - // Get options, with sensible defaults. - this.height = opts.height || 100; - this.width = opts.width || 100; - this.units = opts.units || '%'; + // As soon as the text is extracted start finding the matches. + if (!(i in this.pendingFindMatches)) { + this.pendingFindMatches[i] = true; + this.extractTextPromises[i].then(function(pageIdx) { + delete self.pendingFindMatches[pageIdx]; + self.calcFindMatch(pageIdx); + }); + } + } + } - // Initialize heights. - this.div.style.height = this.height + this.units; - this.percent = 0; - } + // If there's no query there's no point in searching. + if (this.state.query === '') { + this.updateUIState(FindStates.FIND_FOUND); + return; + } + + // If we're waiting on a page, we return since we can't do anything else. + if (this.resumePageIdx) { + return; + } + + var offset = this.offset; + // Keep track of how many pages we should maximally iterate through. + this.pagesToSearch = numPages; + // If there's already a matchIdx that means we are iterating through a + // page's matches. + if (offset.matchIdx !== null) { + var numPageMatches = this.pageMatches[offset.pageIdx].length; + if ((!previous && offset.matchIdx + 1 < numPageMatches) || + (previous && offset.matchIdx > 0)) { + // The simple case; we just have advance the matchIdx to select + // the next match on the page. + this.hadMatch = true; + offset.matchIdx = (previous ? offset.matchIdx - 1 : + offset.matchIdx + 1); + this.updateMatch(true); + return; + } + // We went beyond the current page's matches, so we advance to + // the next page. + this.advanceOffsetPage(previous); + } + // Start searching through the page. + this.nextPageMatch(); + }, - ProgressBar.prototype = { + matchesReady: function PDFFindController_matchesReady(matches) { + var offset = this.offset; + var numMatches = matches.length; + var previous = this.state.findPrevious; - updateBar: function ProgressBar_updateBar() { - if (this._indeterminate) { - this.div.classList.add('indeterminate'); - this.div.style.width = this.width + this.units; - return; + if (numMatches) { + // There were matches for the page, so initialize the matchIdx. + this.hadMatch = true; + offset.matchIdx = (previous ? numMatches - 1 : 0); + this.updateMatch(true); + return true; + } else { + // No matches, so attempt to search the next page. + this.advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (this.pagesToSearch < 0) { + // No point in wrapping again, there were no matches. + this.updateMatch(false); + // while matches were not found, searching for a page + // with matches should nevertheless halt. + return true; + } + } + // Matches were not found (and searching is not done). + return false; } + }, - this.div.classList.remove('indeterminate'); - var progressSize = this.width * this._percent / 100; - this.div.style.width = progressSize + this.units; + /** + * The method is called back from the text layer when match presentation + * is updated. + * @param {number} pageIndex - page index. + * @param {number} index - match index. + * @param {Array} elements - text layer div elements array. + * @param {number} beginIdx - start index of the div array for the match. + * @param {number} endIdx - end index of the div array for the match. + */ + updateMatchPosition: function PDFFindController_updateMatchPosition( + pageIndex, index, elements, beginIdx, endIdx) { + if (this.selected.matchIdx === index && + this.selected.pageIdx === pageIndex) { + var spot = { + top: FIND_SCROLL_OFFSET_TOP, + left: FIND_SCROLL_OFFSET_LEFT + }; + scrollIntoView(elements[beginIdx], spot, + /* skipOverflowHiddenElements = */ true); + } }, - get percent() { - return this._percent; + nextPageMatch: function PDFFindController_nextPageMatch() { + if (this.resumePageIdx !== null) { + console.error('There can only be one pending page.'); + } + do { + var pageIdx = this.offset.pageIdx; + var matches = this.pageMatches[pageIdx]; + if (!matches) { + // The matches don't exist yet for processing by "matchesReady", + // so set a resume point for when they do exist. + this.resumePageIdx = pageIdx; + break; + } + } while (!this.matchesReady(matches)); }, - set percent(val) { - this._indeterminate = isNaN(val); - this._percent = clamp(val, 0, 100); - this.updateBar(); + advanceOffsetPage: function PDFFindController_advanceOffsetPage(previous) { + var offset = this.offset; + var numPages = this.extractTextPromises.length; + offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1); + offset.matchIdx = null; + + this.pagesToSearch--; + + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = (previous ? numPages - 1 : 0); + offset.wrapped = true; + } }, - setWidth: function ProgressBar_setWidth(viewer) { - if (viewer) { - var container = viewer.parentNode; - var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; - if (scrollbarWidth > 0) { - this.bar.setAttribute('style', 'width: calc(100% - ' + - scrollbarWidth + 'px);'); + updateMatch: function PDFFindController_updateMatch(found) { + var state = FindStates.FIND_NOTFOUND; + var wrapped = this.offset.wrapped; + this.offset.wrapped = false; + + if (found) { + var previousPage = this.selected.pageIdx; + this.selected.pageIdx = this.offset.pageIdx; + this.selected.matchIdx = this.offset.matchIdx; + state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND); + // Update the currently selected page to wipe out any selected matches. + if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { + this.updatePage(previousPage); } } + + this.updateUIState(state, this.state.findPrevious); + if (this.selected.pageIdx !== -1) { + this.updatePage(this.selected.pageIdx); + } }, - hide: function ProgressBar_hide() { - if (!this.visible) { - return; + updateUIResultsCount: + function PDFFindController_updateUIResultsCount() { + if (this.onUpdateResultsCount) { + this.onUpdateResultsCount(this.matchCount); } - this.visible = false; - this.bar.classList.add('hidden'); - document.body.classList.remove('loadingInProgress'); }, - show: function ProgressBar_show() { - if (this.visible) { - return; + updateUIState: function PDFFindController_updateUIState(state, previous) { + if (this.onUpdateState) { + this.onUpdateState(state, previous, this.matchCount); } - this.visible = true; - document.body.classList.add('loadingInProgress'); - this.bar.classList.remove('hidden'); } }; - - return ProgressBar; + return PDFFindController; })(); -exports.CSS_UNITS = CSS_UNITS; -exports.DEFAULT_SCALE_VALUE = DEFAULT_SCALE_VALUE; -exports.DEFAULT_SCALE = DEFAULT_SCALE; -exports.UNKNOWN_SCALE = UNKNOWN_SCALE; -exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE; -exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING; -exports.VERTICAL_PADDING = VERTICAL_PADDING; -exports.mozL10n = mozL10n; -exports.ProgressBar = ProgressBar; -exports.getPDFFileNameFromURL = getPDFFileNameFromURL; -exports.noContextMenuHandler = noContextMenuHandler; -exports.parseQueryString = parseQueryString; -exports.getVisibleElements = getVisibleElements; -exports.roundToDivide = roundToDivide; -exports.approximateFraction = approximateFraction; -exports.getOutputScale = getOutputScale; -exports.scrollIntoView = scrollIntoView; -exports.watchScroll = watchScroll; -exports.binarySearchFirstItem = binarySearchFirstItem; +exports.FindStates = FindStates; +exports.PDFFindController = PDFFindController; })); (function (root, factory) { { - factory((root.pdfjsWebPDFFindController = {}), root.pdfjsWebUIUtils); + factory((root.pdfjsWebPDFHistory = {}), root.pdfjsWebDOMEvents); } -}(this, function (exports, uiUtils, firefoxCom) { - -var scrollIntoView = uiUtils.scrollIntoView; - -var FindStates = { - FIND_FOUND: 0, - FIND_NOTFOUND: 1, - FIND_WRAPPED: 2, - FIND_PENDING: 3 -}; - -var FIND_SCROLL_OFFSET_TOP = -50; -var FIND_SCROLL_OFFSET_LEFT = -400; - -var CHARACTERS_TO_NORMALIZE = { - '\u2018': '\'', // Left single quotation mark - '\u2019': '\'', // Right single quotation mark - '\u201A': '\'', // Single low-9 quotation mark - '\u201B': '\'', // Single high-reversed-9 quotation mark - '\u201C': '"', // Left double quotation mark - '\u201D': '"', // Right double quotation mark - '\u201E': '"', // Double low-9 quotation mark - '\u201F': '"', // Double high-reversed-9 quotation mark - '\u00BC': '1/4', // Vulgar fraction one quarter - '\u00BD': '1/2', // Vulgar fraction one half - '\u00BE': '3/4', // Vulgar fraction three quarters -}; +}(this, function (exports, domEvents) { -/** - * Provides "search" or "find" functionality for the PDF. - * This object actually performs the search for a given string. - */ -var PDFFindController = (function PDFFindControllerClosure() { - function PDFFindController(options) { - this.pdfViewer = options.pdfViewer || null; + function PDFHistory(options) { + this.linkService = options.linkService; + this.eventBus = options.eventBus || domEvents.getGlobalEventBus(); - this.onUpdateResultsCount = null; - this.onUpdateState = null; + this.initialized = false; + this.initialDestination = null; + this.initialBookmark = null; + } - this.reset(); + PDFHistory.prototype = { + /** + * @param {string} fingerprint + * @param {IPDFLinkService} linkService + */ + initialize: function pdfHistoryInitialize(fingerprint) { + this.initialized = true; + this.reInitialized = false; + this.allowHashChange = true; + this.historyUnlocked = true; + this.isViewerInPresentationMode = false; - // Compile the regular expression for text normalization once. - var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''); - this.normalizationRegex = new RegExp('[' + replace + ']', 'g'); - } + this.previousHash = window.location.hash.substring(1); + this.currentBookmark = ''; + this.currentPage = 0; + this.updatePreviousBookmark = false; + this.previousBookmark = ''; + this.previousPage = 0; + this.nextHashParam = ''; - PDFFindController.prototype = { - listenWindowEvents: function PDFFindController_listenWindowEvents() { - var events = [ - 'find', - 'findagain', - 'findhighlightallchange', - 'findcasesensitivitychange' - ]; - var handleEvent = function (e) { - this.executeCommand(e.type, e.detail); - }.bind(this); + this.fingerprint = fingerprint; + this.currentUid = this.uid = 0; + this.current = {}; - for (var i = 0, len = events.length; i < len; i++) { - window.addEventListener(events[i], handleEvent); + var state = window.history.state; + if (this._isStateObjectDefined(state)) { + // This corresponds to navigating back to the document + // from another page in the browser history. + if (state.target.dest) { + this.initialDestination = state.target.dest; + } else { + this.initialBookmark = state.target.hash; + } + this.currentUid = state.uid; + this.uid = state.uid + 1; + this.current = state.target; + } else { + // This corresponds to the loading of a new document. + if (state && state.fingerprint && + this.fingerprint !== state.fingerprint) { + // Reinitialize the browsing history when a new document + // is opened in the web viewer. + this.reInitialized = true; + } + this._pushOrReplaceState({fingerprint: this.fingerprint}, true); } - }, - reset: function PDFFindController_reset() { - this.startedTextExtraction = false; - this.extractTextPromises = []; - this.pendingFindMatches = Object.create(null); - this.active = false; // If active, find results will be highlighted. - this.pageContents = []; // Stores the text for each page. - this.pageMatches = []; - this.matchCount = 0; - this.selected = { // Currently selected match. - pageIdx: -1, - matchIdx: -1 - }; - this.offset = { // Where the find algorithm currently is in the document. - pageIdx: null, - matchIdx: null - }; - this.pagesToSearch = null; - this.resumePageIdx = null; - this.state = null; - this.dirtyMatch = false; - this.findTimeout = null; + var self = this; + window.addEventListener('popstate', function pdfHistoryPopstate(evt) { + if (!self.historyUnlocked) { + return; + } + if (evt.state) { + // Move back/forward in the history. + self._goTo(evt.state); + return; + } - this.firstPagePromise = new Promise(function (resolve) { - this.resolveFirstPage = resolve; - }.bind(this)); - }, + // If the state is not set, then the user tried to navigate to a + // different hash by manually editing the URL and pressing Enter, or by + // clicking on an in-page link (e.g. the "current view" link). + // Save the current view state to the browser history. - normalize: function PDFFindController_normalize(text) { - return text.replace(this.normalizationRegex, function (ch) { - return CHARACTERS_TO_NORMALIZE[ch]; - }); - }, + // Note: In Firefox, history.null could also be null after an in-page + // navigation to the same URL, and without dispatching the popstate + // event: https://bugzilla.mozilla.org/show_bug.cgi?id=1183881 - calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) { - var pageContent = this.normalize(this.pageContents[pageIndex]); - var query = this.normalize(this.state.query); - var caseSensitive = this.state.caseSensitive; - var queryLen = query.length; + if (self.uid === 0) { + // Replace the previous state if it was not explicitly set. + var previousParams = (self.previousHash && self.currentBookmark && + self.previousHash !== self.currentBookmark) ? + {hash: self.currentBookmark, page: self.currentPage} : + {page: 1}; + replacePreviousHistoryState(previousParams, function() { + updateHistoryWithCurrentHash(); + }); + } else { + updateHistoryWithCurrentHash(); + } + }, false); - if (queryLen === 0) { - // Do nothing: the matches should be wiped out already. - return; - } - if (!caseSensitive) { - pageContent = pageContent.toLowerCase(); - query = query.toLowerCase(); + function updateHistoryWithCurrentHash() { + self.previousHash = window.location.hash.slice(1); + self._pushToHistory({hash: self.previousHash}, false, true); + self._updatePreviousBookmark(); } - var matches = []; - var matchIdx = -queryLen; - while (true) { - matchIdx = pageContent.indexOf(query, matchIdx + queryLen); - if (matchIdx === -1) { - break; + function replacePreviousHistoryState(params, callback) { + // To modify the previous history entry, the following happens: + // 1. history.back() + // 2. _pushToHistory, which calls history.replaceState( ... ) + // 3. history.forward() + // Because a navigation via the history API does not immediately update + // the history state, the popstate event is used for synchronization. + self.historyUnlocked = false; + + // Suppress the hashchange event to avoid side effects caused by + // navigating back and forward. + self.allowHashChange = false; + window.addEventListener('popstate', rewriteHistoryAfterBack); + history.back(); + + function rewriteHistoryAfterBack() { + window.removeEventListener('popstate', rewriteHistoryAfterBack); + window.addEventListener('popstate', rewriteHistoryAfterForward); + self._pushToHistory(params, false, true); + history.forward(); + } + function rewriteHistoryAfterForward() { + window.removeEventListener('popstate', rewriteHistoryAfterForward); + self.allowHashChange = true; + self.historyUnlocked = true; + callback(); } - matches.push(matchIdx); - } - this.pageMatches[pageIndex] = matches; - this.updatePage(pageIndex); - if (this.resumePageIdx === pageIndex) { - this.resumePageIdx = null; - this.nextPageMatch(); } - // Update the matches count - if (matches.length > 0) { - this.matchCount += matches.length; - this.updateUIResultsCount(); + function pdfHistoryBeforeUnload() { + var previousParams = self._getPreviousParams(null, true); + if (previousParams) { + var replacePrevious = (!self.current.dest && + self.current.hash !== self.previousHash); + self._pushToHistory(previousParams, false, replacePrevious); + self._updatePreviousBookmark(); + } + // Remove the event listener when navigating away from the document, + // since 'beforeunload' prevents Firefox from caching the document. + window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, + false); } - }, - extractText: function PDFFindController_extractText() { - if (this.startedTextExtraction) { - return; - } - this.startedTextExtraction = true; + window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); - this.pageContents = []; - var extractTextPromisesResolves = []; - var numPages = this.pdfViewer.pagesCount; - for (var i = 0; i < numPages; i++) { - this.extractTextPromises.push(new Promise(function (resolve) { - extractTextPromisesResolves.push(resolve); - })); - } + window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { + // If the entire viewer (including the PDF file) is cached in + // the browser, we need to reattach the 'beforeunload' event listener + // since the 'DOMContentLoaded' event is not fired on 'pageshow'. + window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); + }, false); - var self = this; - function extractPageText(pageIndex) { - self.pdfViewer.getPageTextContent(pageIndex).then( - function textContentResolved(textContent) { - var textItems = textContent.items; - var str = []; + self.eventBus.on('presentationmodechanged', function(e) { + self.isViewerInPresentationMode = e.active; + }); + }, - for (var i = 0, len = textItems.length; i < len; i++) { - str.push(textItems[i].str); - } + clearHistoryState: function pdfHistory_clearHistoryState() { + this._pushOrReplaceState(null, true); + }, - // Store the pageContent as a string. - self.pageContents.push(str.join('')); + _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { + return (state && state.uid >= 0 && + state.fingerprint && this.fingerprint === state.fingerprint && + state.target && state.target.hash) ? true : false; + }, - extractTextPromisesResolves[pageIndex](pageIndex); - if ((pageIndex + 1) < self.pdfViewer.pagesCount) { - extractPageText(pageIndex + 1); - } - } - ); + _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, + replace) { + if (replace) { + window.history.replaceState(stateObj, '', document.URL); + } else { + window.history.pushState(stateObj, '', document.URL); } - extractPageText(0); }, - executeCommand: function PDFFindController_executeCommand(cmd, state) { - if (this.state === null || cmd !== 'findagain') { - this.dirtyMatch = true; + get isHashChangeUnlocked() { + if (!this.initialized) { + return true; } - this.state = state; - this.updateUIState(FindStates.FIND_PENDING); - - this.firstPagePromise.then(function() { - this.extractText(); - - clearTimeout(this.findTimeout); - if (cmd === 'find') { - // Only trigger the find action after 250ms of silence. - this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); - } else { - this.nextMatch(); - } - }.bind(this)); + return this.allowHashChange; }, - updatePage: function PDFFindController_updatePage(index) { - if (this.selected.pageIdx === index) { - // If the page is selected, scroll the page into view, which triggers - // rendering the page, which adds the textLayer. Once the textLayer is - // build, it will scroll onto the selected match. - this.pdfViewer.scrollPageIntoView(index + 1); + _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { + if (this.updatePreviousBookmark && + this.currentBookmark && this.currentPage) { + this.previousBookmark = this.currentBookmark; + this.previousPage = this.currentPage; + this.updatePreviousBookmark = false; } + }, - var page = this.pdfViewer.getPageView(index); - if (page.textLayer) { - page.textLayer.updateMatches(); + updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, + pageNum) { + if (this.initialized) { + this.currentBookmark = bookmark.substring(1); + this.currentPage = pageNum | 0; + this._updatePreviousBookmark(); } }, - nextMatch: function PDFFindController_nextMatch() { - var previous = this.state.findPrevious; - var currentPageIndex = this.pdfViewer.currentPageNumber - 1; - var numPages = this.pdfViewer.pagesCount; - - this.active = true; - - if (this.dirtyMatch) { - // Need to recalculate the matches, reset everything. - this.dirtyMatch = false; - this.selected.pageIdx = this.selected.matchIdx = -1; - this.offset.pageIdx = currentPageIndex; - this.offset.matchIdx = null; - this.hadMatch = false; - this.resumePageIdx = null; - this.pageMatches = []; - this.matchCount = 0; - var self = this; - - for (var i = 0; i < numPages; i++) { - // Wipe out any previous highlighted matches. - this.updatePage(i); - - // As soon as the text is extracted start finding the matches. - if (!(i in this.pendingFindMatches)) { - this.pendingFindMatches[i] = true; - this.extractTextPromises[i].then(function(pageIdx) { - delete self.pendingFindMatches[pageIdx]; - self.calcFindMatch(pageIdx); - }); - } - } + updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { + if (this.initialized) { + this.nextHashParam = param; } + }, - // If there's no query there's no point in searching. - if (this.state.query === '') { - this.updateUIState(FindStates.FIND_FOUND); + push: function pdfHistoryPush(params, isInitialBookmark) { + if (!(this.initialized && this.historyUnlocked)) { return; } - - // If we're waiting on a page, we return since we can't do anything else. - if (this.resumePageIdx) { + if (params.dest && !params.hash) { + params.hash = (this.current.hash && this.current.dest && + this.current.dest === params.dest) ? + this.current.hash : + this.linkService.getDestinationHash(params.dest).split('#')[1]; + } + if (params.page) { + params.page |= 0; + } + if (isInitialBookmark) { + var target = window.history.state.target; + if (!target) { + // Invoked when the user specifies an initial bookmark, + // thus setting initialBookmark, when the document is loaded. + this._pushToHistory(params, false); + this.previousHash = window.location.hash.substring(1); + } + this.updatePreviousBookmark = this.nextHashParam ? false : true; + if (target) { + // If the current document is reloaded, + // avoid creating duplicate entries in the history. + this._updatePreviousBookmark(); + } return; } - - var offset = this.offset; - // Keep track of how many pages we should maximally iterate through. - this.pagesToSearch = numPages; - // If there's already a matchIdx that means we are iterating through a - // page's matches. - if (offset.matchIdx !== null) { - var numPageMatches = this.pageMatches[offset.pageIdx].length; - if ((!previous && offset.matchIdx + 1 < numPageMatches) || - (previous && offset.matchIdx > 0)) { - // The simple case; we just have advance the matchIdx to select - // the next match on the page. - this.hadMatch = true; - offset.matchIdx = (previous ? offset.matchIdx - 1 : - offset.matchIdx + 1); - this.updateMatch(true); + if (this.nextHashParam) { + if (this.nextHashParam === params.hash) { + this.nextHashParam = null; + this.updatePreviousBookmark = true; return; + } else { + this.nextHashParam = null; } - // We went beyond the current page's matches, so we advance to - // the next page. - this.advanceOffsetPage(previous); } - // Start searching through the page. - this.nextPageMatch(); - }, - matchesReady: function PDFFindController_matchesReady(matches) { - var offset = this.offset; - var numMatches = matches.length; - var previous = this.state.findPrevious; - - if (numMatches) { - // There were matches for the page, so initialize the matchIdx. - this.hadMatch = true; - offset.matchIdx = (previous ? numMatches - 1 : 0); - this.updateMatch(true); - return true; - } else { - // No matches, so attempt to search the next page. - this.advanceOffsetPage(previous); - if (offset.wrapped) { - offset.matchIdx = null; - if (this.pagesToSearch < 0) { - // No point in wrapping again, there were no matches. - this.updateMatch(false); - // while matches were not found, searching for a page - // with matches should nevertheless halt. - return true; + if (params.hash) { + if (this.current.hash) { + if (this.current.hash !== params.hash) { + this._pushToHistory(params, true); + } else { + if (!this.current.page && params.page) { + this._pushToHistory(params, false, true); + } + this.updatePreviousBookmark = true; } + } else { + this._pushToHistory(params, true); } - // Matches were not found (and searching is not done). - return false; + } else if (this.current.page && params.page && + this.current.page !== params.page) { + this._pushToHistory(params, true); } }, - /** - * The method is called back from the text layer when match presentation - * is updated. - * @param {number} pageIndex - page index. - * @param {number} index - match index. - * @param {Array} elements - text layer div elements array. - * @param {number} beginIdx - start index of the div array for the match. - * @param {number} endIdx - end index of the div array for the match. - */ - updateMatchPosition: function PDFFindController_updateMatchPosition( - pageIndex, index, elements, beginIdx, endIdx) { - if (this.selected.matchIdx === index && - this.selected.pageIdx === pageIndex) { - var spot = { - top: FIND_SCROLL_OFFSET_TOP, - left: FIND_SCROLL_OFFSET_LEFT - }; - scrollIntoView(elements[beginIdx], spot, - /* skipOverflowHiddenElements = */ true); + _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, + beforeUnload) { + if (!(this.currentBookmark && this.currentPage)) { + return null; + } else if (this.updatePreviousBookmark) { + this.updatePreviousBookmark = false; } - }, - - nextPageMatch: function PDFFindController_nextPageMatch() { - if (this.resumePageIdx !== null) { - console.error('There can only be one pending page.'); + if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { + // Prevent the history from getting stuck in the current state, + // effectively preventing the user from going back/forward in + // the history. + // + // This happens if the current position in the document didn't change + // when the history was previously updated. The reasons for this are + // either: + // 1. The current zoom value is such that the document does not need to, + // or cannot, be scrolled to display the destination. + // 2. The previous destination is broken, and doesn't actally point to a + // position within the document. + // (This is either due to a bad PDF generator, or the user making a + // mistake when entering a destination in the hash parameters.) + return null; } - do { - var pageIdx = this.offset.pageIdx; - var matches = this.pageMatches[pageIdx]; - if (!matches) { - // The matches don't exist yet for processing by "matchesReady", - // so set a resume point for when they do exist. - this.resumePageIdx = pageIdx; - break; + if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { + if (this.previousBookmark === this.currentBookmark) { + return null; } - } while (!this.matchesReady(matches)); + } else if (this.current.page || onlyCheckPage) { + if (this.previousPage === this.currentPage) { + return null; + } + } else { + return null; + } + var params = {hash: this.currentBookmark, page: this.currentPage}; + if (this.isViewerInPresentationMode) { + params.hash = null; + } + return params; }, - advanceOffsetPage: function PDFFindController_advanceOffsetPage(previous) { - var offset = this.offset; - var numPages = this.extractTextPromises.length; - offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1); - offset.matchIdx = null; - - this.pagesToSearch--; + _stateObj: function pdfHistory_stateObj(params) { + return {fingerprint: this.fingerprint, uid: this.uid, target: params}; + }, - if (offset.pageIdx >= numPages || offset.pageIdx < 0) { - offset.pageIdx = (previous ? numPages - 1 : 0); - offset.wrapped = true; + _pushToHistory: function pdfHistory_pushToHistory(params, + addPrevious, overwrite) { + if (!this.initialized) { + return; + } + if (!params.hash && params.page) { + params.hash = ('page=' + params.page); + } + if (addPrevious && !overwrite) { + var previousParams = this._getPreviousParams(); + if (previousParams) { + var replacePrevious = (!this.current.dest && + this.current.hash !== this.previousHash); + this._pushToHistory(previousParams, false, replacePrevious); + } } + this._pushOrReplaceState(this._stateObj(params), + (overwrite || this.uid === 0)); + this.currentUid = this.uid++; + this.current = params; + this.updatePreviousBookmark = true; }, - updateMatch: function PDFFindController_updateMatch(found) { - var state = FindStates.FIND_NOTFOUND; - var wrapped = this.offset.wrapped; - this.offset.wrapped = false; - - if (found) { - var previousPage = this.selected.pageIdx; - this.selected.pageIdx = this.offset.pageIdx; - this.selected.matchIdx = this.offset.matchIdx; - state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND); - // Update the currently selected page to wipe out any selected matches. - if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { - this.updatePage(previousPage); + _goTo: function pdfHistory_goTo(state) { + if (!(this.initialized && this.historyUnlocked && + this._isStateObjectDefined(state))) { + return; + } + if (!this.reInitialized && state.uid < this.currentUid) { + var previousParams = this._getPreviousParams(true); + if (previousParams) { + this._pushToHistory(this.current, false); + this._pushToHistory(previousParams, false); + this.currentUid = state.uid; + window.history.back(); + return; } } + this.historyUnlocked = false; - this.updateUIState(state, this.state.findPrevious); - if (this.selected.pageIdx !== -1) { - this.updatePage(this.selected.pageIdx); + if (state.target.dest) { + this.linkService.navigateTo(state.target.dest); + } else { + this.linkService.setHash(state.target.hash); + } + this.currentUid = state.uid; + if (state.uid > this.uid) { + this.uid = state.uid; + } + this.current = state.target; + this.updatePreviousBookmark = true; + + var currentHash = window.location.hash.substring(1); + if (this.previousHash !== currentHash) { + this.allowHashChange = false; } + this.previousHash = currentHash; + + this.historyUnlocked = true; }, - updateUIResultsCount: - function PDFFindController_updateUIResultsCount() { - if (this.onUpdateResultsCount) { - this.onUpdateResultsCount(this.matchCount); - } + back: function pdfHistoryBack() { + this.go(-1); }, - updateUIState: function PDFFindController_updateUIState(state, previous) { - if (this.onUpdateState) { - this.onUpdateState(state, previous, this.matchCount); + forward: function pdfHistoryForward() { + this.go(1); + }, + + go: function pdfHistoryGo(direction) { + if (this.initialized && this.historyUnlocked) { + var state = window.history.state; + if (direction === -1 && state && state.uid > 0) { + window.history.back(); + } else if (direction === 1 && state && state.uid < (this.uid - 1)) { + window.history.forward(); + } } } }; - return PDFFindController; -})(); -exports.FindStates = FindStates; -exports.PDFFindController = PDFFindController; + exports.PDFHistory = PDFHistory; })); (function (root, factory) { { - factory((root.pdfjsWebPDFLinkService = {}), root.pdfjsWebUIUtils); + factory((root.pdfjsWebPDFLinkService = {}), root.pdfjsWebUIUtils, + root.pdfjsWebDOMEvents); } -}(this, function (exports, uiUtils) { +}(this, function (exports, uiUtils, domEvents) { var parseQueryString = uiUtils.parseQueryString; +/** + * @typedef {Object} PDFLinkServiceOptions + * @property {EventBus} eventBus - The application event bus. + */ + /** * Performs navigation functions inside PDF, such as opening specified page, * or destination. @@ -1918,8 +1753,11 @@ var parseQueryString = uiUtils.parseQueryString; var PDFLinkService = (function () { /** * @constructs PDFLinkService + * @param {PDFLinkServiceOptions} options */ - function PDFLinkService() { + function PDFLinkService(options) { + options = options || {}; + this.eventBus = options.eventBus || domEvents.getGlobalEventBus(); this.baseUrl = null; this.pdfDocument = null; this.pdfViewer = null; @@ -2119,11 +1957,10 @@ var PDFLinkService = (function () { this.page = pageNumber; // simple page } if ('pagemode' in params) { - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagemode', true, true, { - mode: params.pagemode, + this.eventBus.dispatch('pagemode', { + source: this, + mode: params.pagemode }); - this.pdfViewer.container.dispatchEvent(event); } } else if (/^\d+$/.test(hash)) { // page number this.page = hash; @@ -2173,11 +2010,10 @@ var PDFLinkService = (function () { break; // No action according to spec } - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('namedaction', true, true, { + this.eventBus.dispatch('namedaction', { + source: this, action: action }); - this.pdfViewer.container.dispatchEvent(event); }, /** @@ -2250,9 +2086,10 @@ exports.SimpleLinkService = SimpleLinkService; (function (root, factory) { { factory((root.pdfjsWebPDFPageView = {}), root.pdfjsWebUIUtils, - root.pdfjsWebPDFRenderingQueue, root.pdfjsWebPDFJS); + root.pdfjsWebPDFRenderingQueue, root.pdfjsWebDOMEvents, + root.pdfjsWebPDFJS); } -}(this, function (exports, uiUtils, pdfRenderingQueue, pdfjsLib) { +}(this, function (exports, uiUtils, pdfRenderingQueue, domEvents, pdfjsLib) { var CSS_UNITS = uiUtils.CSS_UNITS; var DEFAULT_SCALE = uiUtils.DEFAULT_SCALE; @@ -2261,559 +2098,896 @@ var approximateFraction = uiUtils.approximateFraction; var roundToDivide = uiUtils.roundToDivide; var RenderingStates = pdfRenderingQueue.RenderingStates; -var TEXT_LAYER_RENDER_DELAY = 200; // ms +var TEXT_LAYER_RENDER_DELAY = 200; // ms + +/** + * @typedef {Object} PDFPageViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {EventBus} eventBus - The application event bus. + * @property {number} id - The page unique ID (normally its number). + * @property {number} scale - The page scale display. + * @property {PageViewport} defaultViewport - The page viewport. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + * @property {IPDFTextLayerFactory} textLayerFactory + * @property {IPDFAnnotationLayerFactory} annotationLayerFactory + */ + +/** + * @class + * @implements {IRenderableView} + */ +var PDFPageView = (function PDFPageViewClosure() { + /** + * @constructs PDFPageView + * @param {PDFPageViewOptions} options + */ + function PDFPageView(options) { + var container = options.container; + var id = options.id; + var scale = options.scale; + var defaultViewport = options.defaultViewport; + var renderingQueue = options.renderingQueue; + var textLayerFactory = options.textLayerFactory; + var annotationLayerFactory = options.annotationLayerFactory; + + this.id = id; + this.renderingId = 'page' + id; + + this.rotation = 0; + this.scale = scale || DEFAULT_SCALE; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this.hasRestrictedScaling = false; + + this.eventBus = options.eventBus || domEvents.getGlobalEventBus(); + this.renderingQueue = renderingQueue; + this.textLayerFactory = textLayerFactory; + this.annotationLayerFactory = annotationLayerFactory; + + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + + this.onBeforeDraw = null; + this.onAfterDraw = null; + + this.textLayer = null; + + this.zoomLayer = null; + + this.annotationLayer = null; + + var div = document.createElement('div'); + div.id = 'pageContainer' + this.id; + div.className = 'page'; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + div.setAttribute('data-page-number', this.id); + this.div = div; + + container.appendChild(div); + } + + PDFPageView.prototype = { + setPdfPage: function PDFPageView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, + totalRotation); + this.stats = pdfPage.stats; + this.reset(); + }, + + destroy: function PDFPageView_destroy() { + this.zoomLayer = null; + this.reset(); + if (this.pdfPage) { + this.pdfPage.cleanup(); + } + }, + + reset: function PDFPageView_reset(keepZoomLayer, keepAnnotations) { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + var div = this.div; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + + var childNodes = div.childNodes; + var currentZoomLayerNode = (keepZoomLayer && this.zoomLayer) || null; + var currentAnnotationNode = (keepAnnotations && this.annotationLayer && + this.annotationLayer.div) || null; + for (var i = childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if (currentZoomLayerNode === node || currentAnnotationNode === node) { + continue; + } + div.removeChild(node); + } + div.removeAttribute('data-loaded'); + + if (currentAnnotationNode) { + // Hide annotationLayer until all elements are resized + // so they are not displayed on the already-resized page + this.annotationLayer.hide(); + } else { + this.annotationLayer = null; + } + + if (this.canvas && !currentZoomLayerNode) { + // 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; + } + + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }, + + update: function PDFPageView_update(scale, rotation) { + this.scale = scale || this.scale; + + if (typeof rotation !== 'undefined') { + this.rotation = rotation; + } + + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * CSS_UNITS, + rotation: totalRotation + }); + + var isScalingRestricted = false; + if (this.canvas && pdfjsLib.PDFJS.maxCanvasPixels > 0) { + var outputScale = this.outputScale; + var pixelsInViewport = this.viewport.width * this.viewport.height; + if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * + ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > + pdfjsLib.PDFJS.maxCanvasPixels) { + isScalingRestricted = true; + } + } + + if (this.canvas) { + if (pdfjsLib.PDFJS.useOnlyCssZoom || + (this.hasRestrictedScaling && isScalingRestricted)) { + this.cssTransform(this.canvas, true); + + this.eventBus.dispatch('pagerendered', { + source: this, + pageNumber: this.id, + cssTransform: true, + }); + return; + } + if (!this.zoomLayer) { + this.zoomLayer = this.canvas.parentNode; + this.zoomLayer.style.position = 'absolute'; + } + } + if (this.zoomLayer) { + this.cssTransform(this.zoomLayer.firstChild); + } + this.reset(/* keepZoomLayer = */ true, /* keepAnnotations = */ true); + }, + + /** + * Called when moved in the parent's container. + */ + updatePosition: function PDFPageView_updatePosition() { + if (this.textLayer) { + this.textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + }, + + cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + var CustomStyle = pdfjsLib.CustomStyle; + + // Scale canvas, canvas wrapper, and page container. + var width = this.viewport.width; + var height = this.viewport.height; + var div = this.div; + canvas.style.width = canvas.parentNode.style.width = div.style.width = + Math.floor(width) + 'px'; + canvas.style.height = canvas.parentNode.style.height = div.style.height = + Math.floor(height) + 'px'; + // The canvas may have been originally rotated, rotate relative to that. + var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; + var absRotation = Math.abs(relativeRotation); + var scaleX = 1, scaleY = 1; + if (absRotation === 90 || absRotation === 270) { + // Scale x and y because of the rotation. + scaleX = height / width; + scaleY = width / height; + } + var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + + 'scale(' + scaleX + ',' + scaleY + ')'; + CustomStyle.setProp('transform', canvas, cssTransform); + + if (this.textLayer) { + // Rotating the text layer is more complicated since the divs inside the + // the text layer are rotated. + // TODO: This could probably be simplified by drawing the text layer in + // one orientation then rotating overall. + var textLayerViewport = this.textLayer.viewport; + var textRelativeRotation = this.viewport.rotation - + textLayerViewport.rotation; + var textAbsRotation = Math.abs(textRelativeRotation); + var scale = width / textLayerViewport.width; + if (textAbsRotation === 90 || textAbsRotation === 270) { + scale = width / textLayerViewport.height; + } + var textLayerDiv = this.textLayer.textLayerDiv; + var transX, transY; + switch (textAbsRotation) { + case 0: + transX = transY = 0; + break; + case 90: + transX = 0; + transY = '-' + textLayerDiv.style.height; + break; + case 180: + transX = '-' + textLayerDiv.style.width; + transY = '-' + textLayerDiv.style.height; + break; + case 270: + transX = '-' + textLayerDiv.style.width; + transY = 0; + break; + default: + console.error('Bad rotation value.'); + break; + } + CustomStyle.setProp('transform', textLayerDiv, + 'rotate(' + textAbsRotation + 'deg) ' + + 'scale(' + scale + ', ' + scale + ') ' + + 'translate(' + transX + ', ' + transY + ')'); + CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); + } + + if (redrawAnnotations && this.annotationLayer) { + this.annotationLayer.render(this.viewport, 'display'); + } + }, -/** - * @typedef {Object} PDFPageViewOptions - * @property {HTMLDivElement} container - The viewer element. - * @property {number} id - The page unique ID (normally its number). - * @property {number} scale - The page scale display. - * @property {PageViewport} defaultViewport - The page viewport. - * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. - * @property {IPDFTextLayerFactory} textLayerFactory - * @property {IPDFAnnotationLayerFactory} annotationLayerFactory - */ + get width() { + return this.viewport.width; + }, -/** - * @class - * @implements {IRenderableView} - */ -var PDFPageView = (function PDFPageViewClosure() { - /** - * @constructs PDFPageView - * @param {PDFPageViewOptions} options - */ - function PDFPageView(options) { - var container = options.container; - var id = options.id; - var scale = options.scale; - var defaultViewport = options.defaultViewport; - var renderingQueue = options.renderingQueue; - var textLayerFactory = options.textLayerFactory; - var annotationLayerFactory = options.annotationLayerFactory; + get height() { + return this.viewport.height; + }, - this.id = id; - this.renderingId = 'page' + id; + getPagePoint: function PDFPageView_getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }, - this.rotation = 0; - this.scale = scale || DEFAULT_SCALE; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotation; - this.hasRestrictedScaling = false; + draw: function PDFPageView_draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } - this.renderingQueue = renderingQueue; - this.textLayerFactory = textLayerFactory; - this.annotationLayerFactory = annotationLayerFactory; + this.renderingState = RenderingStates.RUNNING; - this.renderingState = RenderingStates.INITIAL; - this.resume = null; + var pdfPage = this.pdfPage; + var viewport = this.viewport; + var div = this.div; + // Wrap the canvas so if it has a css transform for highdpi the overflow + // will be hidden in FF. + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = div.style.width; + canvasWrapper.style.height = div.style.height; + canvasWrapper.classList.add('canvasWrapper'); - this.onBeforeDraw = null; - this.onAfterDraw = null; + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + // Keep the canvas hidden until the first draw callback, or until drawing + // is complete when `!this.renderingQueue`, to prevent black flickering. + canvas.setAttribute('hidden', 'hidden'); + var isCanvasHidden = true; - this.textLayer = null; + canvasWrapper.appendChild(canvas); + if (this.annotationLayer && this.annotationLayer.div) { + // annotationLayer needs to stay on top + div.insertBefore(canvasWrapper, this.annotationLayer.div); + } else { + div.appendChild(canvasWrapper); + } + this.canvas = canvas; - this.zoomLayer = null; + canvas.mozOpaque = true; + var ctx = canvas.getContext('2d', {alpha: false}); + var outputScale = getOutputScale(ctx); + this.outputScale = outputScale; - this.annotationLayer = null; + if (pdfjsLib.PDFJS.useOnlyCssZoom) { + var actualSizeViewport = viewport.clone({scale: CSS_UNITS}); + // Use a scale that will make the canvas be the original intended size + // of the page. + outputScale.sx *= actualSizeViewport.width / viewport.width; + outputScale.sy *= actualSizeViewport.height / viewport.height; + outputScale.scaled = true; + } - var div = document.createElement('div'); - div.id = 'pageContainer' + this.id; - div.className = 'page'; - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; - div.setAttribute('data-page-number', this.id); - this.div = div; + if (pdfjsLib.PDFJS.maxCanvasPixels > 0) { + var pixelsInViewport = viewport.width * viewport.height; + var maxScale = + Math.sqrt(pdfjsLib.PDFJS.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + outputScale.scaled = true; + this.hasRestrictedScaling = true; + } else { + this.hasRestrictedScaling = false; + } + } - container.appendChild(div); - } + var sfx = approximateFraction(outputScale.sx); + var sfy = approximateFraction(outputScale.sy); + canvas.width = roundToDivide(viewport.width * outputScale.sx, sfx[0]); + canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]); + canvas.style.width = roundToDivide(viewport.width, sfx[1]) + 'px'; + canvas.style.height = roundToDivide(viewport.height, sfy[1]) + 'px'; + // Add the viewport so it's known what it was originally drawn with. + canvas._viewport = viewport; - PDFPageView.prototype = { - setPdfPage: function PDFPageView_setPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, - totalRotation); - this.stats = pdfPage.stats; - this.reset(); - }, + var textLayerDiv = null; + var textLayer = null; + if (this.textLayerFactory) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvasWrapper.style.width; + textLayerDiv.style.height = canvasWrapper.style.height; + if (this.annotationLayer && this.annotationLayer.div) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer.div); + } else { + div.appendChild(textLayerDiv); + } - destroy: function PDFPageView_destroy() { - this.zoomLayer = null; - this.reset(); - if (this.pdfPage) { - this.pdfPage.cleanup(); + textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv, + this.id - 1, + this.viewport); } - }, + this.textLayer = textLayer; - reset: function PDFPageView_reset(keepZoomLayer, keepAnnotations) { - if (this.renderTask) { - this.renderTask.cancel(); - } - this.resume = null; - this.renderingState = RenderingStates.INITIAL; + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); - var div = this.div; - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; + // Rendering area - var childNodes = div.childNodes; - var currentZoomLayerNode = (keepZoomLayer && this.zoomLayer) || null; - var currentAnnotationNode = (keepAnnotations && this.annotationLayer && - this.annotationLayer.div) || null; - for (var i = childNodes.length - 1; i >= 0; i--) { - var node = childNodes[i]; - if (currentZoomLayerNode === node || currentAnnotationNode === node) { - continue; + var self = this; + function pageViewDrawCallback(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; } - div.removeChild(node); - } - div.removeAttribute('data-loaded'); - if (currentAnnotationNode) { - // Hide annotationLayer until all elements are resized - // so they are not displayed on the already-resized page - this.annotationLayer.hide(); - } else { - this.annotationLayer = null; - } + if (error === 'cancelled') { + rejectRenderPromise(error); + return; + } - if (this.canvas && !currentZoomLayerNode) { - // 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; - } + self.renderingState = RenderingStates.FINISHED; - this.loadingIconDiv = document.createElement('div'); - this.loadingIconDiv.className = 'loadingIcon'; - div.appendChild(this.loadingIconDiv); - }, + if (isCanvasHidden) { + self.canvas.removeAttribute('hidden'); + isCanvasHidden = false; + } + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } - update: function PDFPageView_update(scale, rotation) { - this.scale = scale || this.scale; + if (self.zoomLayer) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + var zoomLayerCanvas = self.zoomLayer.firstChild; + zoomLayerCanvas.width = 0; + zoomLayerCanvas.height = 0; - if (typeof rotation !== 'undefined') { - this.rotation = rotation; - } + div.removeChild(self.zoomLayer); + self.zoomLayer = null; + } - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = this.viewport.clone({ - scale: this.scale * CSS_UNITS, - rotation: totalRotation - }); + self.error = error; + self.stats = pdfPage.stats; + if (self.onAfterDraw) { + self.onAfterDraw(); + } + self.eventBus.dispatch('pagerendered', { + source: self, + pageNumber: self.id, + cssTransform: false, + }); - var isScalingRestricted = false; - if (this.canvas && pdfjsLib.PDFJS.maxCanvasPixels > 0) { - var outputScale = this.outputScale; - var pixelsInViewport = this.viewport.width * this.viewport.height; - if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * - ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > - pdfjsLib.PDFJS.maxCanvasPixels) { - isScalingRestricted = true; + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); } } - if (this.canvas) { - if (pdfjsLib.PDFJS.useOnlyCssZoom || - (this.hasRestrictedScaling && isScalingRestricted)) { - this.cssTransform(this.canvas, true); + var renderContinueCallback = null; + if (this.renderingQueue) { + renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + if (isCanvasHidden) { + self.canvas.removeAttribute('hidden'); + isCanvasHidden = false; + } + cont(); + }; + } - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerendered', true, true, { - pageNumber: this.id, - cssTransform: true, - }); - this.div.dispatchEvent(event); + var transform = !outputScale.scaled ? null : + [outputScale.sx, 0, 0, outputScale.sy, 0, 0]; + var renderContext = { + canvasContext: ctx, + transform: transform, + viewport: this.viewport, + // intent: 'default', // === 'display' + }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + renderTask.onContinue = renderContinueCallback; - return; + this.renderTask.promise.then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + if (textLayer) { + self.pdfPage.getTextContent({ normalizeWhitespace: true }).then( + function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + ); + } + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); } - if (!this.zoomLayer) { - this.zoomLayer = this.canvas.parentNode; - this.zoomLayer.style.position = 'absolute'; + ); + + if (this.annotationLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationLayerFactory. + createAnnotationLayerBuilder(div, this.pdfPage); } + this.annotationLayer.render(this.viewport, 'display'); } - if (this.zoomLayer) { - this.cssTransform(this.zoomLayer.firstChild); - } - this.reset(/* keepZoomLayer = */ true, /* keepAnnotations = */ true); - }, + div.setAttribute('data-loaded', true); - /** - * Called when moved in the parent's container. - */ - updatePosition: function PDFPageView_updatePosition() { - if (this.textLayer) { - this.textLayer.render(TEXT_LAYER_RENDER_DELAY); + if (self.onBeforeDraw) { + self.onBeforeDraw(); } + return promise; }, - cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + beforePrint: function PDFPageView_beforePrint(printContainer) { var CustomStyle = pdfjsLib.CustomStyle; + var pdfPage = this.pdfPage; - // Scale canvas, canvas wrapper, and page container. - var width = this.viewport.width; - var height = this.viewport.height; - var div = this.div; - canvas.style.width = canvas.parentNode.style.width = div.style.width = - Math.floor(width) + 'px'; - canvas.style.height = canvas.parentNode.style.height = div.style.height = - Math.floor(height) + 'px'; - // The canvas may have been originally rotated, rotate relative to that. - var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; - var absRotation = Math.abs(relativeRotation); - var scaleX = 1, scaleY = 1; - if (absRotation === 90 || absRotation === 270) { - // Scale x and y because of the rotation. - scaleX = height / width; - scaleY = width / height; - } - var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + - 'scale(' + scaleX + ',' + scaleY + ')'; - CustomStyle.setProp('transform', canvas, cssTransform); + var viewport = pdfPage.getViewport(1); + // Use the same hack we use for high dpi displays for printing to get + // better output until bug 811002 is fixed in FF. + var PRINT_OUTPUT_SCALE = 2; + var canvas = document.createElement('canvas'); - if (this.textLayer) { - // Rotating the text layer is more complicated since the divs inside the - // the text layer are rotated. - // TODO: This could probably be simplified by drawing the text layer in - // one orientation then rotating overall. - var textLayerViewport = this.textLayer.viewport; - var textRelativeRotation = this.viewport.rotation - - textLayerViewport.rotation; - var textAbsRotation = Math.abs(textRelativeRotation); - var scale = width / textLayerViewport.width; - if (textAbsRotation === 90 || textAbsRotation === 270) { - scale = width / textLayerViewport.height; - } - var textLayerDiv = this.textLayer.textLayerDiv; - var transX, transY; - switch (textAbsRotation) { - case 0: - transX = transY = 0; - break; - case 90: - transX = 0; - transY = '-' + textLayerDiv.style.height; - break; - case 180: - transX = '-' + textLayerDiv.style.width; - transY = '-' + textLayerDiv.style.height; - break; - case 270: - transX = '-' + textLayerDiv.style.width; - transY = 0; - break; - default: - console.error('Bad rotation value.'); - break; - } - CustomStyle.setProp('transform', textLayerDiv, - 'rotate(' + textAbsRotation + 'deg) ' + - 'scale(' + scale + ', ' + scale + ') ' + - 'translate(' + transX + ', ' + transY + ')'); - CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); - } + // The logical size of the canvas. + canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; + canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; - if (redrawAnnotations && this.annotationLayer) { - this.annotationLayer.render(this.viewport, 'display'); - } - }, + // The rendered size of the canvas, relative to the size of canvasWrapper. + canvas.style.width = (PRINT_OUTPUT_SCALE * 100) + '%'; - get width() { - return this.viewport.width; - }, + var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + + (1 / PRINT_OUTPUT_SCALE) + ')'; + CustomStyle.setProp('transform' , canvas, cssScale); + CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); - get height() { - return this.viewport.height; - }, + var canvasWrapper = document.createElement('div'); + canvasWrapper.appendChild(canvas); + printContainer.appendChild(canvasWrapper); - getPagePoint: function PDFPageView_getPagePoint(x, y) { - return this.viewport.convertToPdfPoint(x, y); + canvas.mozPrintCallback = function(obj) { + var ctx = obj.context; + + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + // Used by the mozCurrentTransform polyfill in src/display/canvas.js. + ctx._transformMatrix = + [PRINT_OUTPUT_SCALE, 0, 0, PRINT_OUTPUT_SCALE, 0, 0]; + ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + + var renderContext = { + canvasContext: ctx, + viewport: viewport, + intent: 'print' + }; + + pdfPage.render(renderContext).promise.then(function() { + // Tell the printEngine that rendering this canvas/page has finished. + obj.done(); + }, function(error) { + console.error(error); + // Tell the printEngine that rendering this canvas/page has failed. + // This will make the print proces stop. + if ('abort' in obj) { + obj.abort(); + } else { + obj.done(); + } + }); + }; }, + }; - draw: function PDFPageView_draw() { - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } + return PDFPageView; +})(); + +exports.PDFPageView = PDFPageView; +})); + + +(function (root, factory) { + { + factory((root.pdfjsWebTextLayerBuilder = {}), root.pdfjsWebDOMEvents, + root.pdfjsWebPDFJS); + } +}(this, function (exports, domEvents, pdfjsLib) { - this.renderingState = RenderingStates.RUNNING; +/** + * @typedef {Object} TextLayerBuilderOptions + * @property {HTMLDivElement} textLayerDiv - The text layer container. + * @property {EventBus} eventBus - The application event bus. + * @property {number} pageIndex - The page index. + * @property {PageViewport} viewport - The viewport of the text layer. + * @property {PDFFindController} findController + */ - var pdfPage = this.pdfPage; - var viewport = this.viewport; - var div = this.div; - // Wrap the canvas so if it has a css transform for highdpi the overflow - // will be hidden in FF. - var canvasWrapper = document.createElement('div'); - canvasWrapper.style.width = div.style.width; - canvasWrapper.style.height = div.style.height; - canvasWrapper.classList.add('canvasWrapper'); +/** + * TextLayerBuilder provides text-selection functionality for the PDF. + * It does this by creating overlay divs over the PDF text. These divs + * contain text that matches the PDF text they are overlaying. This object + * also provides a way to highlight text that is being searched for. + * @class + */ +var TextLayerBuilder = (function TextLayerBuilderClosure() { + function TextLayerBuilder(options) { + this.textLayerDiv = options.textLayerDiv; + this.eventBus = options.eventBus || domEvents.getGlobalEventBus(); + this.renderingDone = false; + this.divContentDone = false; + this.pageIdx = options.pageIndex; + this.pageNumber = this.pageIdx + 1; + this.matches = []; + this.viewport = options.viewport; + this.textDivs = []; + this.findController = options.findController || null; + this.textLayerRenderTask = null; + this._bindMouse(); + } - var canvas = document.createElement('canvas'); - canvas.id = 'page' + this.id; - // Keep the canvas hidden until the first draw callback, or until drawing - // is complete when `!this.renderingQueue`, to prevent black flickering. - canvas.setAttribute('hidden', 'hidden'); - var isCanvasHidden = true; + TextLayerBuilder.prototype = { + _finishRendering: function TextLayerBuilder_finishRendering() { + this.renderingDone = true; - canvasWrapper.appendChild(canvas); - if (this.annotationLayer && this.annotationLayer.div) { - // annotationLayer needs to stay on top - div.insertBefore(canvasWrapper, this.annotationLayer.div); - } else { - div.appendChild(canvasWrapper); - } - this.canvas = canvas; + var endOfContent = document.createElement('div'); + endOfContent.className = 'endOfContent'; + this.textLayerDiv.appendChild(endOfContent); - canvas.mozOpaque = true; - var ctx = canvas.getContext('2d', {alpha: false}); - var outputScale = getOutputScale(ctx); - this.outputScale = outputScale; + this.eventBus.dispatch('textlayerrendered', { + source: this, + pageNumber: this.pageNumber + }); + }, - if (pdfjsLib.PDFJS.useOnlyCssZoom) { - var actualSizeViewport = viewport.clone({scale: CSS_UNITS}); - // Use a scale that will make the canvas be the original intended size - // of the page. - outputScale.sx *= actualSizeViewport.width / viewport.width; - outputScale.sy *= actualSizeViewport.height / viewport.height; - outputScale.scaled = true; + /** + * Renders the text layer. + * @param {number} timeout (optional) if specified, the rendering waits + * for specified amount of ms. + */ + render: function TextLayerBuilder_render(timeout) { + if (!this.divContentDone || this.renderingDone) { + return; } - if (pdfjsLib.PDFJS.maxCanvasPixels > 0) { - var pixelsInViewport = viewport.width * viewport.height; - var maxScale = - Math.sqrt(pdfjsLib.PDFJS.maxCanvasPixels / pixelsInViewport); - if (outputScale.sx > maxScale || outputScale.sy > maxScale) { - outputScale.sx = maxScale; - outputScale.sy = maxScale; - outputScale.scaled = true; - this.hasRestrictedScaling = true; - } else { - this.hasRestrictedScaling = false; - } + if (this.textLayerRenderTask) { + this.textLayerRenderTask.cancel(); + this.textLayerRenderTask = null; } - var sfx = approximateFraction(outputScale.sx); - var sfy = approximateFraction(outputScale.sy); - canvas.width = roundToDivide(viewport.width * outputScale.sx, sfx[0]); - canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]); - canvas.style.width = roundToDivide(viewport.width, sfx[1]) + 'px'; - canvas.style.height = roundToDivide(viewport.height, sfy[1]) + 'px'; - // Add the viewport so it's known what it was originally drawn with. - canvas._viewport = viewport; - - var textLayerDiv = null; - var textLayer = null; - if (this.textLayerFactory) { - textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvasWrapper.style.width; - textLayerDiv.style.height = canvasWrapper.style.height; - if (this.annotationLayer && this.annotationLayer.div) { - // annotationLayer needs to stay on top - div.insertBefore(textLayerDiv, this.annotationLayer.div); - } else { - div.appendChild(textLayerDiv); - } + this.textDivs = []; + var textLayerFrag = document.createDocumentFragment(); + this.textLayerRenderTask = pdfjsLib.renderTextLayer({ + textContent: this.textContent, + container: textLayerFrag, + viewport: this.viewport, + textDivs: this.textDivs, + timeout: timeout + }); + this.textLayerRenderTask.promise.then(function () { + this.textLayerDiv.appendChild(textLayerFrag); + this._finishRendering(); + this.updateMatches(); + }.bind(this), function (reason) { + // canceled or failed to render text layer -- skipping errors + }); + }, - textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv, - this.id - 1, - this.viewport); + setTextContent: function TextLayerBuilder_setTextContent(textContent) { + if (this.textLayerRenderTask) { + this.textLayerRenderTask.cancel(); + this.textLayerRenderTask = null; } - this.textLayer = textLayer; + this.textContent = textContent; + this.divContentDone = true; + }, - var resolveRenderPromise, rejectRenderPromise; - var promise = new Promise(function (resolve, reject) { - resolveRenderPromise = resolve; - rejectRenderPromise = reject; - }); + convertMatches: function TextLayerBuilder_convertMatches(matches) { + var i = 0; + var iIndex = 0; + var bidiTexts = this.textContent.items; + var end = bidiTexts.length - 1; + var queryLen = (this.findController === null ? + 0 : this.findController.state.query.length); + var ret = []; - // Rendering area + for (var m = 0, len = matches.length; m < len; m++) { + // Calculate the start position. + var matchIdx = matches[m]; - var self = this; - function pageViewDrawCallback(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; + // Loop over the divIdxs. + while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; } - if (error === 'cancelled') { - rejectRenderPromise(error); - return; + if (i === bidiTexts.length) { + console.error('Could not find a matching mapping'); } - self.renderingState = RenderingStates.FINISHED; + var match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex + } + }; - if (isCanvasHidden) { - self.canvas.removeAttribute('hidden'); - isCanvasHidden = false; - } + // Calculate the end position. + matchIdx += queryLen; - if (self.loadingIconDiv) { - div.removeChild(self.loadingIconDiv); - delete self.loadingIconDiv; + // Somewhat the same array as above, but use > instead of >= to get + // the end position right. + while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; } - if (self.zoomLayer) { - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - var zoomLayerCanvas = self.zoomLayer.firstChild; - zoomLayerCanvas.width = 0; - zoomLayerCanvas.height = 0; + match.end = { + divIdx: i, + offset: matchIdx - iIndex + }; + ret.push(match); + } - div.removeChild(self.zoomLayer); - self.zoomLayer = null; - } + return ret; + }, - self.error = error; - self.stats = pdfPage.stats; - if (self.onAfterDraw) { - self.onAfterDraw(); - } - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerendered', true, true, { - pageNumber: self.id, - cssTransform: false, - }); - div.dispatchEvent(event); + renderMatches: function TextLayerBuilder_renderMatches(matches) { + // Early exit if there is nothing to render. + if (matches.length === 0) { + return; + } - if (!error) { - resolveRenderPromise(undefined); - } else { - rejectRenderPromise(error); + var bidiTexts = this.textContent.items; + var textDivs = this.textDivs; + var prevEnd = null; + var pageIdx = this.pageIdx; + var isSelectedPage = (this.findController === null ? + false : (pageIdx === this.findController.selected.pageIdx)); + var selectedMatchIdx = (this.findController === null ? + -1 : this.findController.selected.matchIdx); + var highlightAll = (this.findController === null ? + false : this.findController.state.highlightAll); + var infinity = { + divIdx: -1, + offset: undefined + }; + + function beginText(begin, className) { + var divIdx = begin.divIdx; + textDivs[divIdx].textContent = ''; + appendTextToDiv(divIdx, 0, begin.offset, className); + } + + function appendTextToDiv(divIdx, fromOffset, toOffset, className) { + var div = textDivs[divIdx]; + var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); + var node = document.createTextNode(content); + if (className) { + var span = document.createElement('span'); + span.className = className; + span.appendChild(node); + div.appendChild(span); + return; } + div.appendChild(node); } - var renderContinueCallback = null; - if (this.renderingQueue) { - renderContinueCallback = function renderContinueCallback(cont) { - if (!self.renderingQueue.isHighestPriority(self)) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function resumeCallback() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - if (isCanvasHidden) { - self.canvas.removeAttribute('hidden'); - isCanvasHidden = false; - } - cont(); - }; + var i0 = selectedMatchIdx, i1 = i0 + 1; + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + // Not highlighting all and this isn't the selected page, so do nothing. + return; } - var transform = !outputScale.scaled ? null : - [outputScale.sx, 0, 0, outputScale.sy, 0, 0]; - var renderContext = { - canvasContext: ctx, - transform: transform, - viewport: this.viewport, - // intent: 'default', // === 'display' - }; - var renderTask = this.renderTask = this.pdfPage.render(renderContext); - renderTask.onContinue = renderContinueCallback; + for (var i = i0; i < i1; i++) { + var match = matches[i]; + var begin = match.begin; + var end = match.end; + var isSelected = (isSelectedPage && i === selectedMatchIdx); + var highlightSuffix = (isSelected ? ' selected' : ''); - this.renderTask.promise.then( - function pdfPageRenderCallback() { - pageViewDrawCallback(null); - if (textLayer) { - self.pdfPage.getTextContent({ normalizeWhitespace: true }).then( - function textContentResolved(textContent) { - textLayer.setTextContent(textContent); - textLayer.render(TEXT_LAYER_RENDER_DELAY); - } - ); + if (this.findController) { + this.findController.updateMatchPosition(pageIdx, i, textDivs, + begin.divIdx, end.divIdx); + } + + // Match inside new div. + if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { + // If there was a previous div, then add the text at the end. + if (prevEnd !== null) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); } - }, - function pdfPageRenderError(error) { - pageViewDrawCallback(error); + // Clear the divs and set the content until the starting point. + beginText(begin); + } else { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); } - ); - if (this.annotationLayerFactory) { - if (!this.annotationLayer) { - this.annotationLayer = this.annotationLayerFactory. - createAnnotationLayerBuilder(div, this.pdfPage); + if (begin.divIdx === end.divIdx) { + appendTextToDiv(begin.divIdx, begin.offset, end.offset, + 'highlight' + highlightSuffix); + } else { + appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, + 'highlight begin' + highlightSuffix); + for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { + textDivs[n0].className = 'highlight middle' + highlightSuffix; + } + beginText(end, 'highlight end' + highlightSuffix); } - this.annotationLayer.render(this.viewport, 'display'); + prevEnd = end; } - div.setAttribute('data-loaded', true); - if (self.onBeforeDraw) { - self.onBeforeDraw(); + if (prevEnd) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); } - return promise; }, - beforePrint: function PDFPageView_beforePrint(printContainer) { - var CustomStyle = pdfjsLib.CustomStyle; - var pdfPage = this.pdfPage; - - var viewport = pdfPage.getViewport(1); - // Use the same hack we use for high dpi displays for printing to get - // better output until bug 811002 is fixed in FF. - var PRINT_OUTPUT_SCALE = 2; - var canvas = document.createElement('canvas'); - - // The logical size of the canvas. - canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; - canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; - - // The rendered size of the canvas, relative to the size of canvasWrapper. - canvas.style.width = (PRINT_OUTPUT_SCALE * 100) + '%'; - - var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + - (1 / PRINT_OUTPUT_SCALE) + ')'; - CustomStyle.setProp('transform' , canvas, cssScale); - CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); + updateMatches: function TextLayerBuilder_updateMatches() { + // Only show matches when all rendering is done. + if (!this.renderingDone) { + return; + } - var canvasWrapper = document.createElement('div'); - canvasWrapper.appendChild(canvas); - printContainer.appendChild(canvasWrapper); + // Clear all matches. + var matches = this.matches; + var textDivs = this.textDivs; + var bidiTexts = this.textContent.items; + var clearedUntilDivIdx = -1; - canvas.mozPrintCallback = function(obj) { - var ctx = obj.context; + // Clear all current matches. + for (var i = 0, len = matches.length; i < len; i++) { + var match = matches[i]; + var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (var n = begin, end = match.end.divIdx; n <= end; n++) { + var div = textDivs[n]; + div.textContent = bidiTexts[n].str; + div.className = ''; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - // Used by the mozCurrentTransform polyfill in src/display/canvas.js. - ctx._transformMatrix = - [PRINT_OUTPUT_SCALE, 0, 0, PRINT_OUTPUT_SCALE, 0, 0]; - ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + if (this.findController === null || !this.findController.active) { + return; + } - var renderContext = { - canvasContext: ctx, - viewport: viewport, - intent: 'print' - }; + // Convert the matches on the page controller into the match format + // used for the textLayer. + this.matches = this.convertMatches(this.findController === null ? + [] : (this.findController.pageMatches[this.pageIdx] || [])); + this.renderMatches(this.matches); + }, - pdfPage.render(renderContext).promise.then(function() { - // Tell the printEngine that rendering this canvas/page has finished. - obj.done(); - }, function(error) { - console.error(error); - // Tell the printEngine that rendering this canvas/page has failed. - // This will make the print proces stop. - if ('abort' in obj) { - obj.abort(); - } else { - obj.done(); - } - }); - }; + /** + * Fixes text selection: adds additional div where mouse was clicked. + * This reduces flickering of the content if mouse slowly dragged down/up. + * @private + */ + _bindMouse: function TextLayerBuilder_bindMouse() { + var div = this.textLayerDiv; + div.addEventListener('mousedown', function (e) { + var end = div.querySelector('.endOfContent'); + if (!end) { + return; + } + // On non-Firefox browsers, the selection will feel better if the height + // of the endOfContent div will be adjusted to start at mouse click + // location -- this will avoid flickering when selections moves up. + // However it does not work when selection started on empty space. + var adjustTop = e.target !== div; + adjustTop = adjustTop && window.getComputedStyle(end). + getPropertyValue('-moz-user-select') !== 'none'; + if (adjustTop) { + var divBounds = div.getBoundingClientRect(); + var r = Math.max(0, (e.pageY - divBounds.top) / divBounds.height); + end.style.top = (r * 100).toFixed(2) + '%'; + } + end.classList.add('active'); + }); + div.addEventListener('mouseup', function (e) { + var end = div.querySelector('.endOfContent'); + if (!end) { + return; + } + end.style.top = ''; + end.classList.remove('active'); + }); }, }; - - return PDFPageView; + return TextLayerBuilder; })(); -exports.PDFPageView = PDFPageView; +/** + * @constructor + * @implements IPDFTextLayerFactory + */ +function DefaultTextLayerFactory() {} +DefaultTextLayerFactory.prototype = { + /** + * @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 + }); + } +}; + +exports.TextLayerBuilder = TextLayerBuilder; +exports.DefaultTextLayerFactory = DefaultTextLayerFactory; })); @@ -2941,11 +3115,11 @@ exports.DefaultAnnotationLayerFactory = DefaultAnnotationLayerFactory; factory((root.pdfjsWebPDFViewer = {}), root.pdfjsWebUIUtils, root.pdfjsWebPDFPageView, root.pdfjsWebPDFRenderingQueue, root.pdfjsWebTextLayerBuilder, root.pdfjsWebAnnotationLayerBuilder, - root.pdfjsWebPDFLinkService, root.pdfjsWebPDFJS); + root.pdfjsWebPDFLinkService, root.pdfjsWebDOMEvents, root.pdfjsWebPDFJS); } }(this, function (exports, uiUtils, pdfPageView, pdfRenderingQueue, textLayerBuilder, annotationLayerBuilder, pdfLinkService, - pdfjsLib) { + domEvents, pdfjsLib) { var UNKNOWN_SCALE = uiUtils.UNKNOWN_SCALE; var SCROLLBAR_PADDING = uiUtils.SCROLLBAR_PADDING; @@ -2977,6 +3151,7 @@ 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. @@ -3031,6 +3206,7 @@ var PDFViewer = (function pdfViewer() { 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; @@ -3073,21 +3249,23 @@ var PDFViewer = (function pdfViewer() { 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); + this.eventBus.dispatch('pagechange', { + source: this, + updateInProgress: this.updateInProgress, + pageNumber: this._currentPageNumber, + previousPageNumber: val + }); return; } - event.previousPageNumber = this._currentPageNumber; + this.eventBus.dispatch('pagechange', { + source: this, + updateInProgress: this.updateInProgress, + pageNumber: val, + 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) { @@ -3185,11 +3363,10 @@ var PDFViewer = (function pdfViewer() { }); this.pagesPromise = pagesPromise; pagesPromise.then(function () { - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagesloaded', true, true, { + self.eventBus.dispatch('pagesloaded', { + source: self, pagesCount: pagesCount }); - self.container.dispatchEvent(event); }); var isOnePageRenderedResolved = false; @@ -3230,6 +3407,7 @@ var PDFViewer = (function pdfViewer() { } var pageView = new PDFPageView({ container: this.viewer, + eventBus: this.eventBus, id: pageNum, scale: scale, defaultViewport: viewport.clone(), @@ -3268,9 +3446,7 @@ var PDFViewer = (function pdfViewer() { } }); - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagesinit', true, true, null); - self.container.dispatchEvent(event); + self.eventBus.dispatch('pagesinit', {source: self}); if (this.defaultRenderingQueue) { this.update(); @@ -3310,13 +3486,11 @@ var PDFViewer = (function pdfViewer() { _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); + this.eventBus.dispatch('scalechange', { + source: this, + scale: newScale, + presetValue: preset ? newValue : undefined + }); }, _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages( @@ -3582,10 +3756,10 @@ var PDFViewer = (function pdfViewer() { this.updateInProgress = false; - var event = document.createEvent('UIEvents'); - event.initUIEvent('updateviewarea', true, true, window, 0); - event.location = this._location; - this.container.dispatchEvent(event); + this.eventBus.dispatch('updateviewarea', { + source: this, + location: this._location + }); }, containsElement: function (element) { @@ -3683,6 +3857,7 @@ var PDFViewer = (function pdfViewer() { createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { return new TextLayerBuilder({ textLayerDiv: textLayerDiv, + eventBus: this.eventBus, pageIndex: pageIndex, viewport: viewport, findController: this.isInPresentationMode ? null : this.findController @@ -3732,6 +3907,7 @@ exports.PDFViewer = PDFViewer; PDFJS.PDFHistory = pdfViewerLibs.pdfjsWebPDFHistory.PDFHistory; PDFJS.PDFFindController = pdfViewerLibs.pdfjsWebPDFFindController.PDFFindController; + PDFJS.EventBus = pdfViewerLibs.pdfjsWebUIUtils.EventBus; PDFJS.DownloadManager = pdfViewerLibs.pdfjsWebDownloadManager.DownloadManager; PDFJS.ProgressBar = pdfViewerLibs.pdfjsWebUIUtils.ProgressBar;