diff -r 000000000000 -r 6474c204b198 browser/extensions/pdfjs/content/web/viewer.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/extensions/pdfjs/content/web/viewer.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,5805 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, PDFFindBar, CustomStyle, + PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager, + getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory, + Preferences, ViewHistory, PageView, ThumbnailView, URL, + noContextMenuHandler, SecondaryToolbar, PasswordPrompt, + PresentationMode, HandTool, Promise, DocumentProperties */ + +'use strict'; + +var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf'; +var DEFAULT_SCALE = 'auto'; +var DEFAULT_SCALE_DELTA = 1.1; +var UNKNOWN_SCALE = 0; +var CACHE_SIZE = 20; +var CSS_UNITS = 96.0 / 72.0; +var SCROLLBAR_PADDING = 40; +var VERTICAL_PADDING = 5; +var MAX_AUTO_SCALE = 1.25; +var MIN_SCALE = 0.25; +var MAX_SCALE = 4.0; +var VIEW_HISTORY_MEMORY = 20; +var SCALE_SELECT_CONTAINER_PADDING = 8; +var SCALE_SELECT_PADDING = 22; +var THUMBNAIL_SCROLL_MARGIN = -19; +var USE_ONLY_CSS_ZOOM = false; +var CLEANUP_TIMEOUT = 30000; +var IGNORE_CURRENT_POSITION_ON_ZOOM = false; +var RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; +var FindStates = { + FIND_FOUND: 0, + FIND_NOTFOUND: 1, + FIND_WRAPPED: 2, + FIND_PENDING: 3 +}; + +PDFJS.imageResourcesPath = './images/'; + PDFJS.workerSrc = '../build/pdf.worker.js'; + PDFJS.cMapUrl = '../web/cmaps/'; + PDFJS.cMapPacked = true; + +var mozL10n = document.mozL10n || document.webL10n; + + +// optimised CSS custom property getter/setter +var CustomStyle = (function CustomStyleClosure() { + + // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ + // animate-css-transforms-firefox-webkit.html + // in some versions of IE9 it is critical that ms appear in this list + // before Moz + var prefixes = ['ms', 'Moz', 'Webkit', 'O']; + var _cache = {}; + + function CustomStyle() {} + + CustomStyle.getProp = function get(propName, element) { + // check cache only when no element is given + if (arguments.length == 1 && typeof _cache[propName] == 'string') { + return _cache[propName]; + } + + element = element || document.documentElement; + var style = element.style, prefixed, uPropName; + + // test standard property first + if (typeof style[propName] == 'string') { + return (_cache[propName] = propName); + } + + // capitalize + uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + + // test vendor specific properties + for (var i = 0, l = prefixes.length; i < l; i++) { + prefixed = prefixes[i] + uPropName; + if (typeof style[prefixed] == 'string') { + return (_cache[propName] = prefixed); + } + } + + //if all fails then set to undefined + return (_cache[propName] = 'undefined'); + }; + + CustomStyle.setProp = function set(propName, element, str) { + var prop = this.getProp(propName); + if (prop != 'undefined') { + element.style[prop] = str; + } + }; + + return CustomStyle; +})(); + +function getFileName(url) { + var anchor = url.indexOf('#'); + var query = url.indexOf('?'); + var end = Math.min( + anchor > 0 ? anchor : url.length, + query > 0 ? query : url.length); + return url.substring(url.lastIndexOf('/', end) + 1, end); +} + +/** + * 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. + * element {Object} The element to be visible. + * spot {Object} An object with optional top and left properties, + * specifying the offset from the top left edge. + */ +function scrollIntoView(element, spot) { + // 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; + var offsetY = element.offsetTop + element.clientTop; + var offsetX = element.offsetLeft + element.clientLeft; + if (!parent) { + console.error('offsetParent is not set -- cannot scroll'); + return; + } + while (parent.clientHeight === parent.scrollHeight) { + 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; +} + +/** + * Event handler to suppress context menu. + */ +function noContextMenuHandler(e) { + e.preventDefault(); +} + +/** + * 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" + } + } + } + return suggestedFilename || 'document.pdf'; +} + +var ProgressBar = (function ProgressBarClosure() { + + function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); + } + + function ProgressBar(id, opts) { + + // Fetch the sub-elements for later. + this.div = document.querySelector(id + ' .progress'); + + // Get the loading bar element, so it can be resized to fit the viewer. + this.bar = this.div.parentNode; + + // Get options, with sensible defaults. + this.height = opts.height || 100; + this.width = opts.width || 100; + this.units = opts.units || '%'; + + // Initialize heights. + this.div.style.height = this.height + this.units; + this.percent = 0; + } + + ProgressBar.prototype = { + + updateBar: function ProgressBar_updateBar() { + if (this._indeterminate) { + this.div.classList.add('indeterminate'); + this.div.style.width = this.width + this.units; + return; + } + + this.div.classList.remove('indeterminate'); + var progressSize = this.width * this._percent / 100; + this.div.style.width = progressSize + this.units; + }, + + get percent() { + return this._percent; + }, + + set percent(val) { + this._indeterminate = isNaN(val); + this._percent = clamp(val, 0, 100); + this.updateBar(); + }, + + 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);'); + } + } + }, + + hide: function ProgressBar_hide() { + this.bar.classList.add('hidden'); + this.bar.removeAttribute('style'); + } + }; + + return ProgressBar; +})(); + +var Cache = function cacheCache(size) { + var data = []; + this.push = function cachePush(view) { + var i = data.indexOf(view); + if (i >= 0) { + data.splice(i); + } + data.push(view); + if (data.length > size) { + data.shift().destroy(); + } + }; +}; + + + + +var DEFAULT_PREFERENCES = { + showPreviousViewOnLoad: true, + defaultZoomValue: '', + ifAvailableShowOutlineOnLoad: false, + enableHandToolOnLoad: false, + enableWebGL: false +}; + + +/** + * Preferences - Utility for storing persistent settings. + * Used for settings that should be applied to all opened documents, + * or every time the viewer is loaded. + */ +var Preferences = { + prefs: Object.create(DEFAULT_PREFERENCES), + isInitializedPromiseResolved: false, + initializedPromise: null, + + /** + * Initialize and fetch the current preference values from storage. + * @return {Promise} A promise that is resolved when the preferences + * have been initialized. + */ + initialize: function preferencesInitialize() { + return this.initializedPromise = + this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { + this.isInitializedPromiseResolved = true; + if (prefObj) { + this.prefs = prefObj; + } + }.bind(this)); + }, + + /** + * Stub function for writing preferences to storage. + * NOTE: This should be overridden by a build-specific function defined below. + * @param {Object} prefObj The preferences that should be written to storage. + * @return {Promise} A promise that is resolved when the preference values + * have been written. + */ + _writeToStorage: function preferences_writeToStorage(prefObj) { + return Promise.resolve(); + }, + + /** + * Stub function for reading preferences from storage. + * NOTE: This should be overridden by a build-specific function defined below. + * @param {Object} prefObj The preferences that should be read from storage. + * @return {Promise} A promise that is resolved with an {Object} containing + * the preferences that have been read. + */ + _readFromStorage: function preferences_readFromStorage(prefObj) { + return Promise.resolve(); + }, + + /** + * Reset the preferences to their default values and update storage. + * @return {Promise} A promise that is resolved when the preference values + * have been reset. + */ + reset: function preferencesReset() { + return this.initializedPromise.then(function() { + this.prefs = Object.create(DEFAULT_PREFERENCES); + return this._writeToStorage(DEFAULT_PREFERENCES); + }.bind(this)); + }, + + /** + * Replace the current preference values with the ones from storage. + * @return {Promise} A promise that is resolved when the preference values + * have been updated. + */ + reload: function preferencesReload() { + return this.initializedPromise.then(function () { + this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { + if (prefObj) { + this.prefs = prefObj; + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set the value of a preference. + * @param {string} name The name of the preference that should be changed. + * @param {boolean|number|string} value The new value of the preference. + * @return {Promise} A promise that is resolved when the value has been set, + * provided that the preference exists and the types match. + */ + set: function preferencesSet(name, value) { + return this.initializedPromise.then(function () { + if (DEFAULT_PREFERENCES[name] === undefined) { + throw new Error('preferencesSet: \'' + name + '\' is undefined.'); + } else if (value === undefined) { + throw new Error('preferencesSet: no value is specified.'); + } + var valueType = typeof value; + var defaultType = typeof DEFAULT_PREFERENCES[name]; + + if (valueType !== defaultType) { + if (valueType === 'number' && defaultType === 'string') { + value = value.toString(); + } else { + throw new Error('Preferences_set: \'' + value + '\' is a \"' + + valueType + '\", expected \"' + defaultType + '\".'); + } + } else { + if (valueType === 'number' && (value | 0) !== value) { + throw new Error('Preferences_set: \'' + value + + '\' must be an \"integer\".'); + } + } + this.prefs[name] = value; + return this._writeToStorage(this.prefs); + }.bind(this)); + }, + + /** + * Get the value of a preference. + * @param {string} name The name of the preference whose value is requested. + * @return {Promise} A promise that is resolved with a {boolean|number|string} + * containing the value of the preference. + */ + get: function preferencesGet(name) { + return this.initializedPromise.then(function () { + var defaultValue = DEFAULT_PREFERENCES[name]; + + if (defaultValue === undefined) { + throw new Error('preferencesGet: \'' + name + '\' is undefined.'); + } else { + var prefValue = this.prefs[name]; + + if (prefValue !== undefined) { + return prefValue; + } + } + return defaultValue; + }.bind(this)); + } +}; + + + + + + + +var FirefoxCom = (function FirefoxComClosure() { + return { + /** + * Creates an event that the extension is listening for and will + * synchronously respond to. + * NOTE: It is reccomended to use request() instead since one day we may not + * be able to synchronously reply. + * @param {String} action The action to trigger. + * @param {String} data Optional data to send. + * @return {*} The response. + */ + requestSync: function(action, data) { + var request = document.createTextNode(''); + document.documentElement.appendChild(request); + + var sender = document.createEvent('CustomEvent'); + sender.initCustomEvent('pdf.js.message', true, false, + {action: action, data: data, sync: true}); + request.dispatchEvent(sender); + var response = sender.detail.response; + document.documentElement.removeChild(request); + return response; + }, + /** + * Creates an event that the extension is listening for and will + * asynchronously respond by calling the callback. + * @param {String} action The action to trigger. + * @param {String} data Optional data to send. + * @param {Function} callback Optional response callback that will be called + * with one data argument. + */ + request: function(action, data, callback) { + var request = document.createTextNode(''); + if (callback) { + document.addEventListener('pdf.js.response', function listener(event) { + var node = event.target; + var response = event.detail.response; + + document.documentElement.removeChild(node); + + document.removeEventListener('pdf.js.response', listener, false); + return callback(response); + }, false); + } + document.documentElement.appendChild(request); + + var sender = document.createEvent('CustomEvent'); + sender.initCustomEvent('pdf.js.message', true, false, + {action: action, data: data, sync: false, + callback: callback}); + return request.dispatchEvent(sender); + } + }; +})(); + +var DownloadManager = (function DownloadManagerClosure() { + function DownloadManager() {} + + DownloadManager.prototype = { + downloadUrl: function DownloadManager_downloadUrl(url, filename) { + FirefoxCom.request('download', { + originalUrl: url, + filename: filename + }); + }, + + downloadData: function DownloadManager_downloadData(data, filename, + contentType) { + var blobUrl = PDFJS.createObjectURL(data, contentType); + + FirefoxCom.request('download', { + blobUrl: blobUrl, + originalUrl: blobUrl, + filename: filename, + isAttachment: true + }); + }, + + download: function DownloadManager_download(blob, url, filename) { + var blobUrl = window.URL.createObjectURL(blob); + + FirefoxCom.request('download', { + blobUrl: blobUrl, + originalUrl: url, + filename: filename + }, + function response(err) { + if (err && this.onerror) { + this.onerror(err); + } + window.URL.revokeObjectURL(blobUrl); + }.bind(this) + ); + } + }; + + return DownloadManager; +})(); + +Preferences._writeToStorage = function (prefObj) { + return new Promise(function (resolve) { + FirefoxCom.request('setPreferences', prefObj, resolve); + }); +}; + +Preferences._readFromStorage = function (prefObj) { + return new Promise(function (resolve) { + FirefoxCom.request('getPreferences', prefObj, function (prefStr) { + var readPrefs = JSON.parse(prefStr); + resolve(readPrefs); + }); + }); +}; + + + +var cache = new Cache(CACHE_SIZE); +var currentPageNumber = 1; + + +/** + * View History - This is a utility for saving various view parameters for + * recently opened files. + * + * The way that the view parameters are stored depends on how PDF.js is built, + * for 'node make ' the following cases exist: + * - FIREFOX or MOZCENTRAL - uses sessionStorage. + * - B2G - uses asyncStorage. + * - GENERIC or CHROME - uses localStorage, if it is available. + */ +var ViewHistory = (function ViewHistoryClosure() { + function ViewHistory(fingerprint) { + this.fingerprint = fingerprint; + var initializedPromiseResolve; + this.isInitializedPromiseResolved = false; + this.initializedPromise = new Promise(function (resolve) { + initializedPromiseResolve = resolve; + }); + + var resolvePromise = (function ViewHistoryResolvePromise(db) { + this.isInitializedPromiseResolved = true; + this.initialize(db || '{}'); + initializedPromiseResolve(); + }).bind(this); + + + var sessionHistory; + try { + // Workaround for security error when the preference + // network.cookie.lifetimePolicy is set to 1, see Mozilla Bug 365772. + sessionHistory = sessionStorage.getItem('pdfjsHistory'); + } catch (ex) {} + resolvePromise(sessionHistory); + + } + + ViewHistory.prototype = { + initialize: function ViewHistory_initialize(database) { + database = JSON.parse(database); + if (!('files' in database)) { + database.files = []; + } + if (database.files.length >= VIEW_HISTORY_MEMORY) { + database.files.shift(); + } + var index; + for (var i = 0, length = database.files.length; i < length; i++) { + var branch = database.files[i]; + if (branch.fingerprint === this.fingerprint) { + index = i; + break; + } + } + if (typeof index !== 'number') { + index = database.files.push({fingerprint: this.fingerprint}) - 1; + } + this.file = database.files[index]; + this.database = database; + }, + + set: function ViewHistory_set(name, val) { + if (!this.isInitializedPromiseResolved) { + return; + } + var file = this.file; + file[name] = val; + var database = JSON.stringify(this.database); + + + try { + // See comment in try-catch block above. + sessionStorage.setItem('pdfjsHistory', database); + } catch (ex) {} + + }, + + get: function ViewHistory_get(name, defaultValue) { + if (!this.isInitializedPromiseResolved) { + return defaultValue; + } + return this.file[name] || defaultValue; + } + }; + + return ViewHistory; +})(); + + +/** + * Creates a "search bar" given set of DOM elements + * that act as controls for searching, or for setting + * search preferences in the UI. This object also sets + * up the appropriate events for the controls. Actual + * searching is done by PDFFindController + */ +var PDFFindBar = { + opened: false, + bar: null, + toggleButton: null, + findField: null, + highlightAll: null, + caseSensitive: null, + findMsg: null, + findStatusIcon: null, + findPreviousButton: null, + findNextButton: null, + + initialize: function(options) { + if(typeof PDFFindController === 'undefined' || PDFFindController === null) { + throw 'PDFFindBar cannot be initialized ' + + 'without a PDFFindController instance.'; + } + + this.bar = options.bar; + this.toggleButton = options.toggleButton; + this.findField = options.findField; + this.highlightAll = options.highlightAllCheckbox; + this.caseSensitive = options.caseSensitiveCheckbox; + this.findMsg = options.findMsg; + this.findStatusIcon = options.findStatusIcon; + this.findPreviousButton = options.findPreviousButton; + this.findNextButton = options.findNextButton; + + var self = this; + this.toggleButton.addEventListener('click', function() { + self.toggle(); + }); + + this.findField.addEventListener('input', function() { + self.dispatchEvent(''); + }); + + this.bar.addEventListener('keydown', function(evt) { + switch (evt.keyCode) { + case 13: // Enter + if (evt.target === self.findField) { + self.dispatchEvent('again', evt.shiftKey); + } + break; + case 27: // Escape + self.close(); + break; + } + }); + + this.findPreviousButton.addEventListener('click', + function() { self.dispatchEvent('again', true); } + ); + + this.findNextButton.addEventListener('click', function() { + self.dispatchEvent('again', false); + }); + + this.highlightAll.addEventListener('click', function() { + self.dispatchEvent('highlightallchange'); + }); + + this.caseSensitive.addEventListener('click', function() { + self.dispatchEvent('casesensitivitychange'); + }); + }, + + dispatchEvent: function(aType, aFindPrevious) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('find' + aType, true, true, { + query: this.findField.value, + caseSensitive: this.caseSensitive.checked, + highlightAll: this.highlightAll.checked, + findPrevious: aFindPrevious + }); + return window.dispatchEvent(event); + }, + + updateUIState: function(state, previous) { + var notFound = false; + var findMsg = ''; + var status = ''; + + switch (state) { + case FindStates.FIND_FOUND: + break; + + case FindStates.FIND_PENDING: + status = 'pending'; + break; + + case FindStates.FIND_NOTFOUND: + findMsg = mozL10n.get('find_not_found', null, 'Phrase not found'); + notFound = true; + break; + + case FindStates.FIND_WRAPPED: + if (previous) { + findMsg = mozL10n.get('find_reached_top', null, + 'Reached top of document, continued from bottom'); + } else { + findMsg = mozL10n.get('find_reached_bottom', null, + 'Reached end of document, continued from top'); + } + break; + } + + if (notFound) { + this.findField.classList.add('notFound'); + } else { + this.findField.classList.remove('notFound'); + } + + this.findField.setAttribute('data-status', status); + this.findMsg.textContent = findMsg; + }, + + open: function() { + if (!this.opened) { + this.opened = true; + this.toggleButton.classList.add('toggled'); + this.bar.classList.remove('hidden'); + } + + this.findField.select(); + this.findField.focus(); + }, + + close: function() { + if (!this.opened) { + return; + } + this.opened = false; + this.toggleButton.classList.remove('toggled'); + this.bar.classList.add('hidden'); + + PDFFindController.active = false; + }, + + toggle: function() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } +}; + + + +/** + * Provides a "search" or "find" functionality for the PDF. + * This object actually performs the search for a given string. + */ + +var PDFFindController = { + startedTextExtraction: false, + + extractTextPromises: [], + + pendingFindMatches: {}, + + // If active, find results will be highlighted. + active: false, + + // Stores the text for each page. + pageContents: [], + + pageMatches: [], + + // Currently selected match. + selected: { + pageIdx: -1, + matchIdx: -1 + }, + + // Where find algorithm currently is in the document. + offset: { + pageIdx: null, + matchIdx: null + }, + + resumePageIdx: null, + + state: null, + + dirtyMatch: false, + + findTimeout: null, + + pdfPageSource: null, + + integratedFind: false, + + initialize: function(options) { + if(typeof PDFFindBar === 'undefined' || PDFFindBar === null) { + throw 'PDFFindController cannot be initialized ' + + 'without a PDFFindBar instance'; + } + + this.pdfPageSource = options.pdfPageSource; + this.integratedFind = options.integratedFind; + + var events = [ + 'find', + 'findagain', + 'findhighlightallchange', + 'findcasesensitivitychange' + ]; + + this.firstPagePromise = new Promise(function (resolve) { + this.resolveFirstPage = resolve; + }.bind(this)); + this.handleEvent = this.handleEvent.bind(this); + + for (var i = 0; i < events.length; i++) { + window.addEventListener(events[i], this.handleEvent); + } + }, + + reset: function pdfFindControllerReset() { + this.startedTextExtraction = false; + this.extractTextPromises = []; + this.active = false; + }, + + calcFindMatch: function(pageIndex) { + var pageContent = this.pageContents[pageIndex]; + var query = this.state.query; + var caseSensitive = this.state.caseSensitive; + var queryLen = query.length; + + if (queryLen === 0) { + // Do nothing the matches should be wiped out already. + return; + } + + if (!caseSensitive) { + pageContent = pageContent.toLowerCase(); + query = query.toLowerCase(); + } + + 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(); + } + }, + + extractText: function() { + if (this.startedTextExtraction) { + return; + } + this.startedTextExtraction = true; + + this.pageContents = []; + var extractTextPromisesResolves = []; + for (var i = 0, ii = this.pdfPageSource.pdfDocument.numPages; i < ii; i++) { + this.extractTextPromises.push(new Promise(function (resolve) { + extractTextPromisesResolves.push(resolve); + })); + } + + var self = this; + function extractPageText(pageIndex) { + self.pdfPageSource.pages[pageIndex].getTextContent().then( + function textContentResolved(textContent) { + var textItems = textContent.items; + var str = ''; + + for (var i = 0; i < textItems.length; i++) { + str += textItems[i].str; + } + + // Store the pageContent as a string. + self.pageContents.push(str); + + extractTextPromisesResolves[pageIndex](pageIndex); + if ((pageIndex + 1) < self.pdfPageSource.pages.length) { + extractPageText(pageIndex + 1); + } + } + ); + } + extractPageText(0); + }, + + handleEvent: function(e) { + if (this.state === null || e.type !== 'findagain') { + this.dirtyMatch = true; + } + this.state = e.detail; + this.updateUIState(FindStates.FIND_PENDING); + + this.firstPagePromise.then(function() { + this.extractText(); + + clearTimeout(this.findTimeout); + if (e.type === '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(idx) { + var page = this.pdfPageSource.pages[idx]; + + if (this.selected.pageIdx === idx) { + // 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. + page.scrollIntoView(); + } + + if (page.textLayer) { + page.textLayer.updateMatches(); + } + }, + + nextMatch: function() { + var previous = this.state.findPrevious; + var currentPageIndex = this.pdfPageSource.page - 1; + var numPages = this.pdfPageSource.pages.length; + + 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 = []; + 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); + }); + } + } + } + + // 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; + // 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(); + }, + + matchesReady: function(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); + // matches were found + return true; + } else { + // No matches attempt to search the next page. + this.advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (!this.hadMatch) { + // No point in wrapping 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; + } + }, + + nextPageMatch: function() { + 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)); + }, + + advanceOffsetPage: function(previous) { + var offset = this.offset; + var numPages = this.extractTextPromises.length; + offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; + offset.matchIdx = null; + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = previous ? numPages - 1 : 0; + offset.wrapped = true; + return; + } + }, + + updateMatch: function(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, true); + } + }, + + updateUIState: function(state, previous) { + if (this.integratedFind) { + FirefoxCom.request('updateFindControlState', + {result: state, findPrevious: previous}); + return; + } + PDFFindBar.updateUIState(state, previous); + } +}; + + + +var PDFHistory = { + initialized: false, + initialDestination: null, + + initialize: function pdfHistoryInitialize(fingerprint) { + if (PDFJS.disableHistory || PDFView.isViewerEmbedded) { + // The browsing history is only enabled when the viewer is standalone, + // i.e. not when it is embedded in a web page. + return; + } + this.initialized = true; + this.reInitialized = false; + this.allowHashChange = true; + this.historyUnlocked = true; + + this.previousHash = window.location.hash.substring(1); + this.currentBookmark = ''; + this.currentPage = 0; + this.updatePreviousBookmark = false; + this.previousBookmark = ''; + this.previousPage = 0; + this.nextHashParam = ''; + + this.fingerprint = fingerprint; + this.currentUid = this.uid = 0; + this.current = {}; + + 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 { + PDFView.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); + } + + var self = this; + window.addEventListener('popstate', function pdfHistoryPopstate(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (!self.historyUnlocked) { + return; + } + if (evt.state) { + // Move back/forward in the history. + self._goTo(evt.state); + } else { + // Handle the user modifying the hash of a loaded document. + self.previousHash = window.location.hash.substring(1); + + // If the history is empty when the hash changes, + // update the previous entry in the browser history. + if (self.uid === 0) { + var previousParams = (self.previousHash && self.currentBookmark && + self.previousHash !== self.currentBookmark) ? + { hash: self.currentBookmark, page: self.currentPage } : + { page: 1 }; + self.historyUnlocked = false; + self.allowHashChange = false; + window.history.back(); + self._pushToHistory(previousParams, false, true); + window.history.forward(); + self.historyUnlocked = true; + } + self._pushToHistory({ hash: self.previousHash }, false, true); + self._updatePreviousBookmark(); + } + }, false); + + 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); + } + 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); + }, + + _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { + return (state && state.uid >= 0 && + state.fingerprint && this.fingerprint === state.fingerprint && + state.target && state.target.hash) ? true : false; + }, + + _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, + replace) { + if (replace) { + window.history.replaceState(stateObj, ''); + } else { + window.history.pushState(stateObj, ''); + } + }, + + get isHashChangeUnlocked() { + if (!this.initialized) { + return true; + } + // If the current hash changes when moving back/forward in the history, + // this will trigger a 'popstate' event *as well* as a 'hashchange' event. + // Since the hash generally won't correspond to the exact the position + // stored in the history's state object, triggering the 'hashchange' event + // can thus corrupt the browser history. + // + // When the hash changes during a 'popstate' event, we *only* prevent the + // first 'hashchange' event and immediately reset allowHashChange. + // If it is not reset, the user would not be able to change the hash. + + var temp = this.allowHashChange; + this.allowHashChange = true; + return temp; + }, + + _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { + if (this.updatePreviousBookmark && + this.currentBookmark && this.currentPage) { + this.previousBookmark = this.currentBookmark; + this.previousPage = this.currentPage; + this.updatePreviousBookmark = false; + } + }, + + updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, + pageNum) { + if (this.initialized) { + this.currentBookmark = bookmark.substring(1); + this.currentPage = pageNum | 0; + this._updatePreviousBookmark(); + } + }, + + updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { + if (this.initialized) { + this.nextHashParam = param; + } + }, + + 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 : + PDFView.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 PDFView.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; + } + if (this.nextHashParam) { + if (this.nextHashParam === params.hash) { + this.nextHashParam = null; + this.updatePreviousBookmark = true; + return; + } else { + this.nextHashParam = null; + } + } + + 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); + } + }, + + _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 (PresentationMode.active) { + params.hash = null; + } + return params; + }, + + _stateObj: function pdfHistory_stateObj(params) { + return { fingerprint: this.fingerprint, uid: this.uid, target: params }; + }, + + _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; + }, + + _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; + + if (state.target.dest) { + PDFView.navigateTo(state.target.dest); + } else { + PDFView.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; + }, + + 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(); + } + } + } +}; + + +var SecondaryToolbar = { + opened: false, + previousContainerHeight: null, + newContainerHeight: null, + + initialize: function secondaryToolbarInitialize(options) { + this.toolbar = options.toolbar; + this.presentationMode = options.presentationMode; + this.documentProperties = options.documentProperties; + this.buttonContainer = this.toolbar.firstElementChild; + + // Define the toolbar buttons. + this.toggleButton = options.toggleButton; + this.presentationModeButton = options.presentationModeButton; + this.openFile = options.openFile; + this.print = options.print; + this.download = options.download; + this.viewBookmark = options.viewBookmark; + this.firstPage = options.firstPage; + this.lastPage = options.lastPage; + this.pageRotateCw = options.pageRotateCw; + this.pageRotateCcw = options.pageRotateCcw; + this.documentPropertiesButton = options.documentPropertiesButton; + + // Attach the event listeners. + var elements = [ + // Button to toggle the visibility of the secondary toolbar: + { element: this.toggleButton, handler: this.toggle }, + // All items within the secondary toolbar + // (except for toggleHandTool, hand_tool.js is responsible for it): + { element: this.presentationModeButton, + handler: this.presentationModeClick }, + { element: this.openFile, handler: this.openFileClick }, + { element: this.print, handler: this.printClick }, + { element: this.download, handler: this.downloadClick }, + { element: this.viewBookmark, handler: this.viewBookmarkClick }, + { element: this.firstPage, handler: this.firstPageClick }, + { element: this.lastPage, handler: this.lastPageClick }, + { element: this.pageRotateCw, handler: this.pageRotateCwClick }, + { element: this.pageRotateCcw, handler: this.pageRotateCcwClick }, + { element: this.documentPropertiesButton, + handler: this.documentPropertiesClick } + ]; + + for (var item in elements) { + var element = elements[item].element; + if (element) { + element.addEventListener('click', elements[item].handler.bind(this)); + } + } + }, + + // Event handling functions. + presentationModeClick: function secondaryToolbarPresentationModeClick(evt) { + this.presentationMode.request(); + this.close(); + }, + + openFileClick: function secondaryToolbarOpenFileClick(evt) { + document.getElementById('fileInput').click(); + this.close(); + }, + + printClick: function secondaryToolbarPrintClick(evt) { + window.print(); + this.close(); + }, + + downloadClick: function secondaryToolbarDownloadClick(evt) { + PDFView.download(); + this.close(); + }, + + viewBookmarkClick: function secondaryToolbarViewBookmarkClick(evt) { + this.close(); + }, + + firstPageClick: function secondaryToolbarFirstPageClick(evt) { + PDFView.page = 1; + this.close(); + }, + + lastPageClick: function secondaryToolbarLastPageClick(evt) { + PDFView.page = PDFView.pdfDocument.numPages; + this.close(); + }, + + pageRotateCwClick: function secondaryToolbarPageRotateCwClick(evt) { + PDFView.rotatePages(90); + }, + + pageRotateCcwClick: function secondaryToolbarPageRotateCcwClick(evt) { + PDFView.rotatePages(-90); + }, + + documentPropertiesClick: function secondaryToolbarDocumentPropsClick(evt) { + this.documentProperties.show(); + this.close(); + }, + + // Misc. functions for interacting with the toolbar. + setMaxHeight: function secondaryToolbarSetMaxHeight(container) { + if (!container || !this.buttonContainer) { + return; + } + this.newContainerHeight = container.clientHeight; + if (this.previousContainerHeight === this.newContainerHeight) { + return; + } + this.buttonContainer.setAttribute('style', + 'max-height: ' + (this.newContainerHeight - SCROLLBAR_PADDING) + 'px;'); + this.previousContainerHeight = this.newContainerHeight; + }, + + open: function secondaryToolbarOpen() { + if (this.opened) { + return; + } + this.opened = true; + this.toggleButton.classList.add('toggled'); + this.toolbar.classList.remove('hidden'); + }, + + close: function secondaryToolbarClose(target) { + if (!this.opened) { + return; + } else if (target && !this.toolbar.contains(target)) { + return; + } + this.opened = false; + this.toolbar.classList.add('hidden'); + this.toggleButton.classList.remove('toggled'); + }, + + toggle: function secondaryToolbarToggle() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } +}; + + +var PasswordPrompt = { + visible: false, + updatePassword: null, + reason: null, + overlayContainer: null, + passwordField: null, + passwordText: null, + passwordSubmit: null, + passwordCancel: null, + + initialize: function secondaryToolbarInitialize(options) { + this.overlayContainer = options.overlayContainer; + this.passwordField = options.passwordField; + this.passwordText = options.passwordText; + this.passwordSubmit = options.passwordSubmit; + this.passwordCancel = options.passwordCancel; + + // Attach the event listeners. + this.passwordSubmit.addEventListener('click', + this.verifyPassword.bind(this)); + + this.passwordCancel.addEventListener('click', this.hide.bind(this)); + + this.passwordField.addEventListener('keydown', + function (e) { + if (e.keyCode === 13) { // Enter key + this.verifyPassword(); + } + }.bind(this)); + + window.addEventListener('keydown', + function (e) { + if (e.keyCode === 27) { // Esc key + this.hide(); + } + }.bind(this)); + }, + + show: function passwordPromptShow() { + if (this.visible) { + return; + } + this.visible = true; + this.overlayContainer.classList.remove('hidden'); + this.overlayContainer.firstElementChild.classList.remove('hidden'); + this.passwordField.focus(); + + var promptString = mozL10n.get('password_label', null, + 'Enter the password to open this PDF file.'); + + if (this.reason === PDFJS.PasswordResponses.INCORRECT_PASSWORD) { + promptString = mozL10n.get('password_invalid', null, + 'Invalid password. Please try again.'); + } + + this.passwordText.textContent = promptString; + }, + + hide: function passwordPromptClose() { + if (!this.visible) { + return; + } + this.visible = false; + this.passwordField.value = ''; + this.overlayContainer.classList.add('hidden'); + this.overlayContainer.firstElementChild.classList.add('hidden'); + }, + + verifyPassword: function passwordPromptVerifyPassword() { + var password = this.passwordField.value; + if (password && password.length > 0) { + this.hide(); + return this.updatePassword(password); + } + } +}; + + +var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms +var SELECTOR = 'presentationControls'; +var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms + +var PresentationMode = { + active: false, + args: null, + contextMenuOpen: false, + + initialize: function presentationModeInitialize(options) { + this.container = options.container; + this.secondaryToolbar = options.secondaryToolbar; + + this.viewer = this.container.firstElementChild; + + this.firstPage = options.firstPage; + this.lastPage = options.lastPage; + this.pageRotateCw = options.pageRotateCw; + this.pageRotateCcw = options.pageRotateCcw; + + this.firstPage.addEventListener('click', function() { + this.contextMenuOpen = false; + this.secondaryToolbar.firstPageClick(); + }.bind(this)); + this.lastPage.addEventListener('click', function() { + this.contextMenuOpen = false; + this.secondaryToolbar.lastPageClick(); + }.bind(this)); + + this.pageRotateCw.addEventListener('click', function() { + this.contextMenuOpen = false; + this.secondaryToolbar.pageRotateCwClick(); + }.bind(this)); + this.pageRotateCcw.addEventListener('click', function() { + this.contextMenuOpen = false; + this.secondaryToolbar.pageRotateCcwClick(); + }.bind(this)); + }, + + get isFullscreen() { + return (document.fullscreenElement || + document.mozFullScreen || + document.webkitIsFullScreen || + document.msFullscreenElement); + }, + + /** + * Initialize a timeout that is used to reset PDFView.currentPosition when the + * browser transitions to fullscreen mode. Since resize events are triggered + * multiple times during the switch to fullscreen mode, this is necessary in + * order to prevent the page from being scrolled partially, or completely, + * out of view when Presentation Mode is enabled. + * Note: This is only an issue at certain zoom levels, e.g. 'page-width'. + */ + _setSwitchInProgress: function presentationMode_setSwitchInProgress() { + if (this.switchInProgress) { + clearTimeout(this.switchInProgress); + } + this.switchInProgress = setTimeout(function switchInProgressTimeout() { + delete this.switchInProgress; + }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS); + + PDFView.currentPosition = null; + }, + + _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() { + if (this.switchInProgress) { + clearTimeout(this.switchInProgress); + delete this.switchInProgress; + } + }, + + request: function presentationModeRequest() { + if (!PDFView.supportsFullscreen || this.isFullscreen || + !this.viewer.hasChildNodes()) { + return false; + } + this._setSwitchInProgress(); + + if (this.container.requestFullscreen) { + this.container.requestFullscreen(); + } else if (this.container.mozRequestFullScreen) { + this.container.mozRequestFullScreen(); + } else if (this.container.webkitRequestFullScreen) { + this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (this.container.msRequestFullscreen) { + this.container.msRequestFullscreen(); + } else { + return false; + } + + this.args = { + page: PDFView.page, + previousScale: PDFView.currentScaleValue + }; + + return true; + }, + + enter: function presentationModeEnter() { + this.active = true; + this._resetSwitchInProgress(); + + // Ensure that the correct page is scrolled into view when entering + // Presentation Mode, by waiting until fullscreen mode in enabled. + // Note: This is only necessary in non-Mozilla browsers. + setTimeout(function enterPresentationModeTimeout() { + PDFView.page = this.args.page; + PDFView.setScale('page-fit', true); + }.bind(this), 0); + + window.addEventListener('mousemove', this.mouseMove, false); + window.addEventListener('mousedown', this.mouseDown, false); + window.addEventListener('contextmenu', this.contextMenu, false); + + this.showControls(); + HandTool.enterPresentationMode(); + this.contextMenuOpen = false; + this.container.setAttribute('contextmenu', 'viewerContextMenu'); + }, + + exit: function presentationModeExit() { + var page = PDFView.page; + + // Ensure that the correct page is scrolled into view when exiting + // Presentation Mode, by waiting until fullscreen mode is disabled. + // Note: This is only necessary in non-Mozilla browsers. + setTimeout(function exitPresentationModeTimeout() { + this.active = false; + PDFView.setScale(this.args.previousScale); + PDFView.page = page; + this.args = null; + }.bind(this), 0); + + window.removeEventListener('mousemove', this.mouseMove, false); + window.removeEventListener('mousedown', this.mouseDown, false); + window.removeEventListener('contextmenu', this.contextMenu, false); + + this.hideControls(); + PDFView.clearMouseScrollState(); + HandTool.exitPresentationMode(); + this.container.removeAttribute('contextmenu'); + this.contextMenuOpen = false; + + // Ensure that the thumbnail of the current page is visible + // when exiting presentation mode. + scrollIntoView(document.getElementById('thumbnailContainer' + page)); + }, + + showControls: function presentationModeShowControls() { + if (this.controlsTimeout) { + clearTimeout(this.controlsTimeout); + } else { + this.container.classList.add(SELECTOR); + } + this.controlsTimeout = setTimeout(function hideControlsTimeout() { + this.container.classList.remove(SELECTOR); + delete this.controlsTimeout; + }.bind(this), DELAY_BEFORE_HIDING_CONTROLS); + }, + + hideControls: function presentationModeHideControls() { + if (!this.controlsTimeout) { + return; + } + this.container.classList.remove(SELECTOR); + clearTimeout(this.controlsTimeout); + delete this.controlsTimeout; + }, + + mouseMove: function presentationModeMouseMove(evt) { + PresentationMode.showControls(); + }, + + mouseDown: function presentationModeMouseDown(evt) { + var self = PresentationMode; + if (self.contextMenuOpen) { + self.contextMenuOpen = false; + evt.preventDefault(); + return; + } + + if (evt.button === 0) { + // Enable clicking of links in presentation mode. Please note: + // Only links pointing to destinations in the current PDF document work. + var isInternalLink = (evt.target.href && + evt.target.classList.contains('internalLink')); + if (!isInternalLink) { + // Unless an internal link was clicked, advance one page. + evt.preventDefault(); + PDFView.page += (evt.shiftKey ? -1 : 1); + } + } + }, + + contextMenu: function presentationModeContextMenu(evt) { + PresentationMode.contextMenuOpen = true; + } +}; + +(function presentationModeClosure() { + function presentationModeChange(e) { + if (PresentationMode.isFullscreen) { + PresentationMode.enter(); + } else { + PresentationMode.exit(); + } + } + + window.addEventListener('fullscreenchange', presentationModeChange, false); + window.addEventListener('mozfullscreenchange', presentationModeChange, false); + window.addEventListener('webkitfullscreenchange', presentationModeChange, + false); + window.addEventListener('MSFullscreenChange', presentationModeChange, false); +})(); + + +/* Copyright 2013 Rob Wu + * https://github.com/Rob--W/grab-to-pan.js + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var GrabToPan = (function GrabToPanClosure() { + /** + * Construct a GrabToPan instance for a given HTML element. + * @param options.element {Element} + * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)` + * @param options.onActiveChanged {function(boolean)} optional. Called + * when grab-to-pan is (de)activated. The first argument is a boolean that + * shows whether grab-to-pan is activated. + */ + function GrabToPan(options) { + this.element = options.element; + this.document = options.element.ownerDocument; + if (typeof options.ignoreTarget === 'function') { + this.ignoreTarget = options.ignoreTarget; + } + this.onActiveChanged = options.onActiveChanged; + + // Bind the contexts to ensure that `this` always points to + // the GrabToPan instance. + this.activate = this.activate.bind(this); + this.deactivate = this.deactivate.bind(this); + this.toggle = this.toggle.bind(this); + this._onmousedown = this._onmousedown.bind(this); + this._onmousemove = this._onmousemove.bind(this); + this._endPan = this._endPan.bind(this); + + // This overlay will be inserted in the document when the mouse moves during + // a grab operation, to ensure that the cursor has the desired appearance. + var overlay = this.overlay = document.createElement('div'); + overlay.className = 'grab-to-pan-grabbing'; + } + GrabToPan.prototype = { + /** + * Class name of element which can be grabbed + */ + CSS_CLASS_GRAB: 'grab-to-pan-grab', + + /** + * Bind a mousedown event to the element to enable grab-detection. + */ + activate: function GrabToPan_activate() { + if (!this.active) { + this.active = true; + this.element.addEventListener('mousedown', this._onmousedown, true); + this.element.classList.add(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(true); + } + } + }, + + /** + * Removes all events. Any pending pan session is immediately stopped. + */ + deactivate: function GrabToPan_deactivate() { + if (this.active) { + this.active = false; + this.element.removeEventListener('mousedown', this._onmousedown, true); + this._endPan(); + this.element.classList.remove(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(false); + } + } + }, + + toggle: function GrabToPan_toggle() { + if (this.active) { + this.deactivate(); + } else { + this.activate(); + } + }, + + /** + * Whether to not pan if the target element is clicked. + * Override this method to change the default behaviour. + * + * @param node {Element} The target of the event + * @return {boolean} Whether to not react to the click event. + */ + ignoreTarget: function GrabToPan_ignoreTarget(node) { + // Use matchesSelector to check whether the clicked element + // is (a child of) an input element / link + return node[matchesSelector]( + 'a[href], a[href] *, input, textarea, button, button *, select, option' + ); + }, + + /** + * @private + */ + _onmousedown: function GrabToPan__onmousedown(event) { + if (event.button !== 0 || this.ignoreTarget(event.target)) { + return; + } + if (event.originalTarget) { + try { + /* jshint expr:true */ + event.originalTarget.tagName; + } catch (e) { + // Mozilla-specific: element is a scrollbar (XUL element) + return; + } + } + + this.scrollLeftStart = this.element.scrollLeft; + this.scrollTopStart = this.element.scrollTop; + this.clientXStart = event.clientX; + this.clientYStart = event.clientY; + this.document.addEventListener('mousemove', this._onmousemove, true); + this.document.addEventListener('mouseup', this._endPan, true); + // When a scroll event occurs before a mousemove, assume that the user + // dragged a scrollbar (necessary for Opera Presto, Safari and IE) + // (not needed for Chrome/Firefox) + this.element.addEventListener('scroll', this._endPan, true); + event.preventDefault(); + event.stopPropagation(); + this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING); + }, + + /** + * @private + */ + _onmousemove: function GrabToPan__onmousemove(event) { + this.element.removeEventListener('scroll', this._endPan, true); + if (isLeftMouseReleased(event)) { + this._endPan(); + return; + } + var xDiff = event.clientX - this.clientXStart; + var yDiff = event.clientY - this.clientYStart; + this.element.scrollTop = this.scrollTopStart - yDiff; + this.element.scrollLeft = this.scrollLeftStart - xDiff; + if (!this.overlay.parentNode) { + document.body.appendChild(this.overlay); + } + }, + + /** + * @private + */ + _endPan: function GrabToPan__endPan() { + this.element.removeEventListener('scroll', this._endPan, true); + this.document.removeEventListener('mousemove', this._onmousemove, true); + this.document.removeEventListener('mouseup', this._endPan, true); + if (this.overlay.parentNode) { + this.overlay.parentNode.removeChild(this.overlay); + } + } + }; + + // Get the correct (vendor-prefixed) name of the matches method. + var matchesSelector; + ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) { + var name = prefix + 'atches'; + if (name in document.documentElement) { + matchesSelector = name; + } + name += 'Selector'; + if (name in document.documentElement) { + matchesSelector = name; + } + return matchesSelector; // If found, then truthy, and [].some() ends. + }); + + // Browser sniffing because it's impossible to feature-detect + // whether event.which for onmousemove is reliable + var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9; + var chrome = window.chrome; + var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app); + // ^ Chrome 15+ ^ Opera 15+ + var isSafari6plus = /Apple/.test(navigator.vendor) && + /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent); + + /** + * Whether the left mouse is not pressed. + * @param event {MouseEvent} + * @return {boolean} True if the left mouse button is not pressed. + * False if unsure or if the left mouse button is pressed. + */ + function isLeftMouseReleased(event) { + if ('buttons' in event && isNotIEorIsIE10plus) { + // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons + // Firefox 15+ + // Internet Explorer 10+ + return !(event.buttons | 1); + } + if (isChrome15OrOpera15plus || isSafari6plus) { + // Chrome 14+ + // Opera 15+ + // Safari 6.0+ + return event.which === 0; + } + } + + return GrabToPan; +})(); + +var HandTool = { + initialize: function handToolInitialize(options) { + var toggleHandTool = options.toggleHandTool; + this.handTool = new GrabToPan({ + element: options.container, + onActiveChanged: function(isActive) { + if (!toggleHandTool) { + return; + } + if (isActive) { + toggleHandTool.title = + mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool'); + } else { + toggleHandTool.title = + mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool'); + } + } + }); + if (toggleHandTool) { + toggleHandTool.addEventListener('click', this.toggle.bind(this), false); + + window.addEventListener('localized', function (evt) { + Preferences.get('enableHandToolOnLoad').then(function (prefValue) { + if (prefValue) { + this.handTool.activate(); + } + }.bind(this)); + }.bind(this)); + } + }, + + toggle: function handToolToggle() { + this.handTool.toggle(); + SecondaryToolbar.close(); + }, + + enterPresentationMode: function handToolEnterPresentationMode() { + if (this.handTool.active) { + this.wasActive = true; + this.handTool.deactivate(); + } + }, + + exitPresentationMode: function handToolExitPresentationMode() { + if (this.wasActive) { + this.wasActive = null; + this.handTool.activate(); + } + } +}; + + +var DocumentProperties = { + overlayContainer: null, + fileName: '', + fileSize: '', + visible: false, + + // Document property fields (in the viewer). + fileNameField: null, + fileSizeField: null, + titleField: null, + authorField: null, + subjectField: null, + keywordsField: null, + creationDateField: null, + modificationDateField: null, + creatorField: null, + producerField: null, + versionField: null, + pageCountField: null, + + initialize: function documentPropertiesInitialize(options) { + this.overlayContainer = options.overlayContainer; + + // Set the document property fields. + this.fileNameField = options.fileNameField; + this.fileSizeField = options.fileSizeField; + this.titleField = options.titleField; + this.authorField = options.authorField; + this.subjectField = options.subjectField; + this.keywordsField = options.keywordsField; + this.creationDateField = options.creationDateField; + this.modificationDateField = options.modificationDateField; + this.creatorField = options.creatorField; + this.producerField = options.producerField; + this.versionField = options.versionField; + this.pageCountField = options.pageCountField; + + // Bind the event listener for the Close button. + if (options.closeButton) { + options.closeButton.addEventListener('click', this.hide.bind(this)); + } + + this.dataAvailablePromise = new Promise(function (resolve) { + this.resolveDataAvailable = resolve; + }.bind(this)); + + // Bind the event listener for the Esc key (to close the dialog). + window.addEventListener('keydown', + function (e) { + if (e.keyCode === 27) { // Esc key + this.hide(); + } + }.bind(this)); + }, + + getProperties: function documentPropertiesGetProperties() { + if (!this.visible) { + // If the dialog was closed before dataAvailablePromise was resolved, + // don't bother updating the properties. + return; + } + // Get the file name. + this.fileName = getPDFFileNameFromURL(PDFView.url); + + // Get the file size. + PDFView.pdfDocument.getDownloadInfo().then(function(data) { + this.setFileSize(data.length); + this.updateUI(this.fileSizeField, this.fileSize); + }.bind(this)); + + // Get the other document properties. + PDFView.pdfDocument.getMetadata().then(function(data) { + var fields = [ + { field: this.fileNameField, content: this.fileName }, + // The fileSize field is updated once getDownloadInfo is resolved. + { field: this.titleField, content: data.info.Title }, + { field: this.authorField, content: data.info.Author }, + { field: this.subjectField, content: data.info.Subject }, + { field: this.keywordsField, content: data.info.Keywords }, + { field: this.creationDateField, + content: this.parseDate(data.info.CreationDate) }, + { field: this.modificationDateField, + content: this.parseDate(data.info.ModDate) }, + { field: this.creatorField, content: data.info.Creator }, + { field: this.producerField, content: data.info.Producer }, + { field: this.versionField, content: data.info.PDFFormatVersion }, + { field: this.pageCountField, content: PDFView.pdfDocument.numPages } + ]; + + // Show the properties in the dialog. + for (var item in fields) { + var element = fields[item]; + this.updateUI(element.field, element.content); + } + }.bind(this)); + }, + + updateUI: function documentPropertiesUpdateUI(field, content) { + if (field && content !== undefined && content !== '') { + field.textContent = content; + } + }, + + setFileSize: function documentPropertiesSetFileSize(fileSize) { + var kb = fileSize / 1024; + if (kb < 1024) { + this.fileSize = mozL10n.get('document_properties_kb', { + size_kb: (+kb.toPrecision(3)).toLocaleString(), + size_b: fileSize.toLocaleString() + }, '{{size_kb}} KB ({{size_b}} bytes)'); + } else { + this.fileSize = mozL10n.get('document_properties_mb', { + size_mb: (+(kb / 1024).toPrecision(3)).toLocaleString(), + size_b: fileSize.toLocaleString() + }, '{{size_mb}} MB ({{size_b}} bytes)'); + } + }, + + show: function documentPropertiesShow() { + if (this.visible) { + return; + } + this.visible = true; + this.overlayContainer.classList.remove('hidden'); + this.overlayContainer.lastElementChild.classList.remove('hidden'); + + this.dataAvailablePromise.then(function () { + this.getProperties(); + }.bind(this)); + }, + + hide: function documentPropertiesClose() { + if (!this.visible) { + return; + } + this.visible = false; + this.overlayContainer.classList.add('hidden'); + this.overlayContainer.lastElementChild.classList.add('hidden'); + }, + + parseDate: function documentPropertiesParseDate(inputDate) { + // This is implemented according to the PDF specification (see + // http://www.gnupdf.org/Date for an overview), but note that + // Adobe Reader doesn't handle changing the date to universal time + // and doesn't use the user's time zone (they're effectively ignoring + // the HH' and mm' parts of the date string). + var dateToParse = inputDate; + if (dateToParse === undefined) { + return ''; + } + + // Remove the D: prefix if it is available. + if (dateToParse.substring(0,2) === 'D:') { + dateToParse = dateToParse.substring(2); + } + + // Get all elements from the PDF date string. + // JavaScript's Date object expects the month to be between + // 0 and 11 instead of 1 and 12, so we're correcting for this. + var year = parseInt(dateToParse.substring(0,4), 10); + var month = parseInt(dateToParse.substring(4,6), 10) - 1; + var day = parseInt(dateToParse.substring(6,8), 10); + var hours = parseInt(dateToParse.substring(8,10), 10); + var minutes = parseInt(dateToParse.substring(10,12), 10); + var seconds = parseInt(dateToParse.substring(12,14), 10); + var utRel = dateToParse.substring(14,15); + var offsetHours = parseInt(dateToParse.substring(15,17), 10); + var offsetMinutes = parseInt(dateToParse.substring(18,20), 10); + + // As per spec, utRel = 'Z' means equal to universal time. + // The other cases ('-' and '+') have to be handled here. + if (utRel == '-') { + hours += offsetHours; + minutes += offsetMinutes; + } else if (utRel == '+') { + hours -= offsetHours; + minutes += offsetMinutes; + } + + // Return the new date format from the user's locale. + var date = new Date(Date.UTC(year, month, day, hours, minutes, seconds)); + var dateString = date.toLocaleDateString(); + var timeString = date.toLocaleTimeString(); + return mozL10n.get('document_properties_date_string', + {date: dateString, time: timeString}, + '{{date}}, {{time}}'); + } +}; + + +var PDFView = { + pages: [], + thumbnails: [], + currentScale: UNKNOWN_SCALE, + currentScaleValue: null, + initialBookmark: document.location.hash.substring(1), + container: null, + thumbnailContainer: null, + initialized: false, + fellback: false, + pdfDocument: null, + sidebarOpen: false, + pageViewScroll: null, + thumbnailViewScroll: null, + pageRotation: 0, + mouseScrollTimeStamp: 0, + mouseScrollDelta: 0, + lastScroll: 0, + previousPageNumber: 1, + isViewerEmbedded: (window.parent !== window), + idleTimeout: null, + currentPosition: null, + + // called once when the document is loaded + initialize: function pdfViewInitialize() { + var self = this; + var container = this.container = document.getElementById('viewerContainer'); + this.pageViewScroll = {}; + this.watchScroll(container, this.pageViewScroll, updateViewarea); + + var thumbnailContainer = this.thumbnailContainer = + document.getElementById('thumbnailView'); + this.thumbnailViewScroll = {}; + this.watchScroll(thumbnailContainer, this.thumbnailViewScroll, + this.renderHighestPriority.bind(this)); + + Preferences.initialize(); + + PDFFindBar.initialize({ + bar: document.getElementById('findbar'), + toggleButton: document.getElementById('viewFind'), + findField: document.getElementById('findInput'), + highlightAllCheckbox: document.getElementById('findHighlightAll'), + caseSensitiveCheckbox: document.getElementById('findMatchCase'), + findMsg: document.getElementById('findMsg'), + findStatusIcon: document.getElementById('findStatusIcon'), + findPreviousButton: document.getElementById('findPrevious'), + findNextButton: document.getElementById('findNext') + }); + + PDFFindController.initialize({ + pdfPageSource: this, + integratedFind: this.supportsIntegratedFind + }); + + HandTool.initialize({ + container: container, + toggleHandTool: document.getElementById('toggleHandTool') + }); + + SecondaryToolbar.initialize({ + toolbar: document.getElementById('secondaryToolbar'), + presentationMode: PresentationMode, + toggleButton: document.getElementById('secondaryToolbarToggle'), + presentationModeButton: + document.getElementById('secondaryPresentationMode'), + openFile: document.getElementById('secondaryOpenFile'), + print: document.getElementById('secondaryPrint'), + download: document.getElementById('secondaryDownload'), + viewBookmark: document.getElementById('secondaryViewBookmark'), + firstPage: document.getElementById('firstPage'), + lastPage: document.getElementById('lastPage'), + pageRotateCw: document.getElementById('pageRotateCw'), + pageRotateCcw: document.getElementById('pageRotateCcw'), + documentProperties: DocumentProperties, + documentPropertiesButton: document.getElementById('documentProperties') + }); + + PasswordPrompt.initialize({ + overlayContainer: document.getElementById('overlayContainer'), + passwordField: document.getElementById('password'), + passwordText: document.getElementById('passwordText'), + passwordSubmit: document.getElementById('passwordSubmit'), + passwordCancel: document.getElementById('passwordCancel') + }); + + PresentationMode.initialize({ + container: container, + secondaryToolbar: SecondaryToolbar, + firstPage: document.getElementById('contextFirstPage'), + lastPage: document.getElementById('contextLastPage'), + pageRotateCw: document.getElementById('contextPageRotateCw'), + pageRotateCcw: document.getElementById('contextPageRotateCcw') + }); + + DocumentProperties.initialize({ + overlayContainer: document.getElementById('overlayContainer'), + closeButton: document.getElementById('documentPropertiesClose'), + fileNameField: document.getElementById('fileNameField'), + fileSizeField: document.getElementById('fileSizeField'), + titleField: document.getElementById('titleField'), + authorField: document.getElementById('authorField'), + subjectField: document.getElementById('subjectField'), + keywordsField: document.getElementById('keywordsField'), + creationDateField: document.getElementById('creationDateField'), + modificationDateField: document.getElementById('modificationDateField'), + creatorField: document.getElementById('creatorField'), + producerField: document.getElementById('producerField'), + versionField: document.getElementById('versionField'), + pageCountField: document.getElementById('pageCountField') + }); + + container.addEventListener('scroll', function() { + self.lastScroll = Date.now(); + }, false); + + var initializedPromise = Promise.all([ + Preferences.get('enableWebGL').then(function (value) { + PDFJS.disableWebGL = !value; + }) + // TODO move more preferences and other async stuff here + ]); + + return initializedPromise.then(function () { + PDFView.initialized = true; + }); + }, + + getPage: function pdfViewGetPage(n) { + return this.pdfDocument.getPage(n); + }, + + // Helper function to keep track whether a div was scrolled up or down and + // then call a callback. + watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) { + state.down = true; + state.lastY = viewAreaElement.scrollTop; + viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) { + var currentY = viewAreaElement.scrollTop; + var lastY = state.lastY; + if (currentY > lastY) { + state.down = true; + } else if (currentY < lastY) { + state.down = false; + } + // else do nothing and use previous value + state.lastY = currentY; + callback(); + }, true); + }, + + _setScaleUpdatePages: function pdfView_setScaleUpdatePages( + newScale, newValue, resetAutoSettings, noScroll) { + this.currentScaleValue = newValue; + if (newScale === this.currentScale) { + return; + } + for (var i = 0, ii = this.pages.length; i < ii; i++) { + this.pages[i].update(newScale); + } + this.currentScale = newScale; + + if (!noScroll) { + var page = this.page, dest; + if (this.currentPosition && !IGNORE_CURRENT_POSITION_ON_ZOOM) { + page = this.currentPosition.page; + dest = [null, { name: 'XYZ' }, this.currentPosition.left, + this.currentPosition.top, null]; + } + this.pages[page - 1].scrollIntoView(dest); + } + var event = document.createEvent('UIEvents'); + event.initUIEvent('scalechange', false, false, window, 0); + event.scale = newScale; + event.resetAutoSettings = resetAutoSettings; + window.dispatchEvent(event); + }, + + setScale: function pdfViewSetScale(value, resetAutoSettings, noScroll) { + if (value === 'custom') { + return; + } + var scale = parseFloat(value); + + if (scale > 0) { + this._setScaleUpdatePages(scale, value, true, noScroll); + } else { + var currentPage = this.pages[this.page - 1]; + if (!currentPage) { + return; + } + var hPadding = PresentationMode.active ? 0 : SCROLLBAR_PADDING; + var vPadding = PresentationMode.active ? 0 : VERTICAL_PADDING; + var pageWidthScale = (this.container.clientWidth - hPadding) / + currentPage.width * currentPage.scale; + var pageHeightScale = (this.container.clientHeight - vPadding) / + currentPage.height * currentPage.scale; + switch (value) { + case 'page-actual': + scale = 1; + break; + case 'page-width': + scale = pageWidthScale; + break; + case 'page-height': + scale = pageHeightScale; + break; + case 'page-fit': + scale = Math.min(pageWidthScale, pageHeightScale); + break; + case 'auto': + scale = Math.min(MAX_AUTO_SCALE, pageWidthScale); + break; + default: + console.error('pdfViewSetScale: \'' + value + + '\' is an unknown zoom value.'); + return; + } + this._setScaleUpdatePages(scale, value, resetAutoSettings, noScroll); + + selectScaleOption(value); + } + }, + + zoomIn: function pdfViewZoomIn(ticks) { + var newScale = this.currentScale; + do { + newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2); + newScale = Math.ceil(newScale * 10) / 10; + newScale = Math.min(MAX_SCALE, newScale); + } while (--ticks && newScale < MAX_SCALE); + this.setScale(newScale, true); + }, + + zoomOut: function pdfViewZoomOut(ticks) { + var newScale = this.currentScale; + do { + newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2); + newScale = Math.floor(newScale * 10) / 10; + newScale = Math.max(MIN_SCALE, newScale); + } while (--ticks && newScale > MIN_SCALE); + this.setScale(newScale, true); + }, + + set page(val) { + var pages = this.pages; + var event = document.createEvent('UIEvents'); + event.initUIEvent('pagechange', false, false, window, 0); + + if (!(0 < val && val <= pages.length)) { + this.previousPageNumber = val; + event.pageNumber = this.page; + window.dispatchEvent(event); + return; + } + + pages[val - 1].updateStats(); + this.previousPageNumber = currentPageNumber; + currentPageNumber = val; + event.pageNumber = val; + window.dispatchEvent(event); + + // checking if the this.page was called from the updateViewarea function: + // avoiding the creation of two "set page" method (internal and public) + if (updateViewarea.inProgress) { + return; + } + // Avoid scrolling the first page during loading + if (this.loading && val === 1) { + return; + } + pages[val - 1].scrollIntoView(); + }, + + get page() { + return currentPageNumber; + }, + + get supportsPrinting() { + var canvas = document.createElement('canvas'); + var value = 'mozPrintCallback' in canvas; + // shadow + Object.defineProperty(this, 'supportsPrinting', { value: value, + enumerable: true, + configurable: true, + writable: false }); + return value; + }, + + get supportsFullscreen() { + var doc = document.documentElement; + var support = doc.requestFullscreen || doc.mozRequestFullScreen || + doc.webkitRequestFullScreen || doc.msRequestFullscreen; + + if (document.fullscreenEnabled === false || + document.mozFullScreenEnabled === false || + document.webkitFullscreenEnabled === false || + document.msFullscreenEnabled === false) { + support = false; + } + + Object.defineProperty(this, 'supportsFullscreen', { value: support, + enumerable: true, + configurable: true, + writable: false }); + return support; + }, + + get supportsIntegratedFind() { + var support = false; + support = FirefoxCom.requestSync('supportsIntegratedFind'); + Object.defineProperty(this, 'supportsIntegratedFind', { value: support, + enumerable: true, + configurable: true, + writable: false }); + return support; + }, + + get supportsDocumentFonts() { + var support = true; + support = FirefoxCom.requestSync('supportsDocumentFonts'); + Object.defineProperty(this, 'supportsDocumentFonts', { value: support, + enumerable: true, + configurable: true, + writable: false }); + return support; + }, + + get supportsDocumentColors() { + var support = true; + support = FirefoxCom.requestSync('supportsDocumentColors'); + Object.defineProperty(this, 'supportsDocumentColors', { value: support, + enumerable: true, + configurable: true, + writable: false }); + return support; + }, + + get loadingBar() { + var bar = new ProgressBar('#loadingBar', {}); + Object.defineProperty(this, 'loadingBar', { value: bar, + enumerable: true, + configurable: true, + writable: false }); + return bar; + }, + + get isHorizontalScrollbarEnabled() { + return (PresentationMode.active ? false : + (this.container.scrollWidth > this.container.clientWidth)); + }, + + initPassiveLoading: function pdfViewInitPassiveLoading() { + var pdfDataRangeTransport = { + rangeListeners: [], + progressListeners: [], + + addRangeListener: function PdfDataRangeTransport_addRangeListener( + listener) { + this.rangeListeners.push(listener); + }, + + addProgressListener: function PdfDataRangeTransport_addProgressListener( + listener) { + this.progressListeners.push(listener); + }, + + onDataRange: function PdfDataRangeTransport_onDataRange(begin, chunk) { + var listeners = this.rangeListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](begin, chunk); + } + }, + + onDataProgress: function PdfDataRangeTransport_onDataProgress(loaded) { + var listeners = this.progressListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](loaded); + } + }, + + requestDataRange: function PdfDataRangeTransport_requestDataRange( + begin, end) { + FirefoxCom.request('requestDataRange', { begin: begin, end: end }); + } + }; + + window.addEventListener('message', function windowMessage(e) { + if (e.source !== null) { + // The message MUST originate from Chrome code. + console.warn('Rejected untrusted message from ' + e.origin); + return; + } + var args = e.data; + + if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) { + return; + } + switch (args.pdfjsLoadAction) { + case 'supportsRangedLoading': + PDFView.open(args.pdfUrl, 0, undefined, pdfDataRangeTransport, { + length: args.length, + initialData: args.data + }); + break; + case 'range': + pdfDataRangeTransport.onDataRange(args.begin, args.chunk); + break; + case 'rangeProgress': + pdfDataRangeTransport.onDataProgress(args.loaded); + break; + case 'progress': + PDFView.progress(args.loaded / args.total); + break; + case 'complete': + if (!args.data) { + PDFView.error(mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'), e); + break; + } + PDFView.open(args.data, 0); + break; + } + }); + FirefoxCom.requestSync('initPassiveLoading', null); + }, + + setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { + this.url = url; + try { + this.setTitle(decodeURIComponent(getFileName(url)) || url); + } catch (e) { + // decodeURIComponent may throw URIError, + // fall back to using the unprocessed url in that case + this.setTitle(url); + } + }, + + setTitle: function pdfViewSetTitle(title) { + document.title = title; + }, + + close: function pdfViewClose() { + var errorWrapper = document.getElementById('errorWrapper'); + errorWrapper.setAttribute('hidden', 'true'); + + if (!this.pdfDocument) { + return; + } + + this.pdfDocument.destroy(); + this.pdfDocument = null; + + var thumbsView = document.getElementById('thumbnailView'); + while (thumbsView.hasChildNodes()) { + thumbsView.removeChild(thumbsView.lastChild); + } + + if ('_loadingInterval' in thumbsView) { + clearInterval(thumbsView._loadingInterval); + } + + var container = document.getElementById('viewer'); + while (container.hasChildNodes()) { + container.removeChild(container.lastChild); + } + + if (typeof PDFBug !== 'undefined') { + PDFBug.cleanup(); + } + }, + + // TODO(mack): This function signature should really be pdfViewOpen(url, args) + open: function pdfViewOpen(url, scale, password, + pdfDataRangeTransport, args) { + if (this.pdfDocument) { + // Reload the preferences if a document was previously opened. + Preferences.reload(); + } + this.close(); + + var parameters = {password: password}; + if (typeof url === 'string') { // URL + this.setTitleUsingUrl(url); + parameters.url = url; + } else if (url && 'byteLength' in url) { // ArrayBuffer + parameters.data = url; + } + if (args) { + for (var prop in args) { + parameters[prop] = args[prop]; + } + } + + var self = this; + self.loading = true; + self.downloadComplete = false; + + var passwordNeeded = function passwordNeeded(updatePassword, reason) { + PasswordPrompt.updatePassword = updatePassword; + PasswordPrompt.reason = reason; + PasswordPrompt.show(); + }; + + function getDocumentProgress(progressData) { + self.progress(progressData.loaded / progressData.total); + } + + PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded, + getDocumentProgress).then( + function getDocumentCallback(pdfDocument) { + self.load(pdfDocument, scale); + self.loading = false; + }, + function getDocumentError(message, exception) { + var loadingErrorMessage = mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'); + + if (exception && exception.name === 'InvalidPDFException') { + // change error message also for other builds + loadingErrorMessage = mozL10n.get('invalid_file_error', null, + 'Invalid or corrupted PDF file.'); + } + + if (exception && exception.name === 'MissingPDFException') { + // special message for missing PDF's + loadingErrorMessage = mozL10n.get('missing_file_error', null, + 'Missing PDF file.'); + + } + + var moreInfo = { + message: message + }; + self.error(loadingErrorMessage, moreInfo); + self.loading = false; + } + ); + }, + + download: function pdfViewDownload() { + function downloadByUrl() { + downloadManager.downloadUrl(url, filename); + } + + var url = this.url.split('#')[0]; + var filename = getPDFFileNameFromURL(url); + var downloadManager = new DownloadManager(); + downloadManager.onerror = function (err) { + // This error won't really be helpful because it's likely the + // fallback won't work either (or is already open). + PDFView.error('PDF failed to download.'); + }; + + if (!this.pdfDocument) { // the PDF is not ready yet + downloadByUrl(); + return; + } + + if (!this.downloadComplete) { // the PDF is still downloading + downloadByUrl(); + return; + } + + this.pdfDocument.getData().then( + function getDataSuccess(data) { + var blob = PDFJS.createBlob(data, 'application/pdf'); + downloadManager.download(blob, url, filename); + }, + downloadByUrl // Error occurred try downloading with just the url. + ).then(null, downloadByUrl); + }, + + fallback: function pdfViewFallback(featureId) { + // Only trigger the fallback once so we don't spam the user with messages + // for one PDF. + if (this.fellback) + return; + this.fellback = true; + var url = this.url.split('#')[0]; + FirefoxCom.request('fallback', { featureId: featureId, url: url }, + function response(download) { + if (!download) { + return; + } + PDFView.download(); + }); + }, + + navigateTo: function pdfViewNavigateTo(dest) { + var destString = ''; + var self = this; + + var goToDestination = function(destRef) { + self.pendingRefStr = null; + // dest array looks like that: + var pageNumber = destRef instanceof Object ? + self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : + (destRef + 1); + if (pageNumber) { + if (pageNumber > self.pages.length) { + pageNumber = self.pages.length; + } + var currentPage = self.pages[pageNumber - 1]; + currentPage.scrollIntoView(dest); + + // Update the browsing history. + PDFHistory.push({ dest: dest, hash: destString, page: pageNumber }); + } else { + self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) { + var pageNum = pageIndex + 1; + self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum; + goToDestination(destRef); + }); + } + }; + + this.destinationsPromise.then(function() { + if (typeof dest === 'string') { + destString = dest; + dest = self.destinations[dest]; + } + if (!(dest instanceof Array)) { + return; // invalid destination + } + goToDestination(dest[0]); + }); + }, + + getDestinationHash: function pdfViewGetDestinationHash(dest) { + if (typeof dest === 'string') { + return PDFView.getAnchorUrl('#' + escape(dest)); + } + if (dest instanceof Array) { + var destRef = dest[0]; // see navigateTo method for dest format + var pageNumber = destRef instanceof Object ? + this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : + (destRef + 1); + if (pageNumber) { + var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); + var destKind = dest[1]; + if (typeof destKind === 'object' && 'name' in destKind && + destKind.name == 'XYZ') { + var scale = (dest[4] || this.currentScaleValue); + var scaleNumber = parseFloat(scale); + if (scaleNumber) { + scale = scaleNumber * 100; + } + pdfOpenParams += '&zoom=' + scale; + if (dest[2] || dest[3]) { + pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); + } + } + return pdfOpenParams; + } + } + return ''; + }, + + /** + * Prefix the full url on anchor links to make sure that links are resolved + * relative to the current URL instead of the one defined in . + * @param {String} anchor The anchor hash, including the #. + */ + getAnchorUrl: function getAnchorUrl(anchor) { + return this.url.split('#')[0] + anchor; + }, + + /** + * Show the error box. + * @param {String} message A message that is human readable. + * @param {Object} moreInfo (optional) Further information about the error + * that is more technical. Should have a 'message' + * and optionally a 'stack' property. + */ + error: function pdfViewError(message, moreInfo) { + var moreInfoText = mozL10n.get('error_version_info', + {version: PDFJS.version || '?', build: PDFJS.build || '?'}, + 'PDF.js v{{version}} (build: {{build}})') + '\n'; + if (moreInfo) { + moreInfoText += + mozL10n.get('error_message', {message: moreInfo.message}, + 'Message: {{message}}'); + if (moreInfo.stack) { + moreInfoText += '\n' + + mozL10n.get('error_stack', {stack: moreInfo.stack}, + 'Stack: {{stack}}'); + } else { + if (moreInfo.filename) { + moreInfoText += '\n' + + mozL10n.get('error_file', {file: moreInfo.filename}, + 'File: {{file}}'); + } + if (moreInfo.lineNumber) { + moreInfoText += '\n' + + mozL10n.get('error_line', {line: moreInfo.lineNumber}, + 'Line: {{line}}'); + } + } + } + + console.error(message + '\n' + moreInfoText); + this.fallback(); + }, + + progress: function pdfViewProgress(level) { + var percent = Math.round(level * 100); + // When we transition from full request to range requests, it's possible + // that we discard some of the loaded data. This can cause the loading + // bar to move backwards. So prevent this by only updating the bar if it + // increases. + if (percent > PDFView.loadingBar.percent) { + PDFView.loadingBar.percent = percent; + } + }, + + load: function pdfViewLoad(pdfDocument, scale) { + var self = this; + var isOnePageRenderedResolved = false; + var resolveOnePageRendered = null; + var onePageRendered = new Promise(function (resolve) { + resolveOnePageRendered = resolve; + }); + function bindOnAfterDraw(pageView, thumbnailView) { + // when page is painted, using the image as thumbnail base + pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { + if (!isOnePageRenderedResolved) { + isOnePageRenderedResolved = true; + resolveOnePageRendered(); + } + thumbnailView.setImage(pageView.canvas); + }; + } + + PDFFindController.reset(); + + this.pdfDocument = pdfDocument; + + DocumentProperties.resolveDataAvailable(); + + var downloadedPromise = pdfDocument.getDownloadInfo().then(function() { + self.downloadComplete = true; + PDFView.loadingBar.hide(); + var outerContainer = document.getElementById('outerContainer'); + outerContainer.classList.remove('loadingInProgress'); + }); + + var pagesCount = pdfDocument.numPages; + + var id = pdfDocument.fingerprint; + document.getElementById('numPages').textContent = + mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}'); + document.getElementById('pageNumber').max = pagesCount; + + PDFView.documentFingerprint = id; + var store = PDFView.store = new ViewHistory(id); + + this.pageRotation = 0; + + var pages = this.pages = []; + var pagesRefMap = this.pagesRefMap = {}; + var thumbnails = this.thumbnails = []; + + var resolvePagesPromise; + var pagesPromise = new Promise(function (resolve) { + resolvePagesPromise = resolve; + }); + this.pagesPromise = pagesPromise; + + var firstPagePromise = pdfDocument.getPage(1); + var container = document.getElementById('viewer'); + var thumbsView = document.getElementById('thumbnailView'); + + // Fetch a single page so we can get a viewport that will be the default + // viewport for all pages + firstPagePromise.then(function(pdfPage) { + var viewport = pdfPage.getViewport((scale || 1.0) * CSS_UNITS); + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + var viewportClone = viewport.clone(); + var pageView = new PageView(container, pageNum, scale, + self.navigateTo.bind(self), + viewportClone); + var thumbnailView = new ThumbnailView(thumbsView, pageNum, + viewportClone); + bindOnAfterDraw(pageView, thumbnailView); + pages.push(pageView); + thumbnails.push(thumbnailView); + } + + // Fetch all the pages since the viewport is needed before printing + // starts to create the correct size canvas. Wait until one page is + // rendered so we don't tie up too many resources early on. + onePageRendered.then(function () { + if (!PDFJS.disableAutoFetch) { + var getPagesLeft = pagesCount; + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) { + var pageView = pages[pageNum - 1]; + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R'; + pagesRefMap[refStr] = pageNum; + getPagesLeft--; + if (!getPagesLeft) { + resolvePagesPromise(); + } + }.bind(null, pageNum)); + } + } else { + // XXX: Printing is semi-broken with auto fetch disabled. + resolvePagesPromise(); + } + }); + + downloadedPromise.then(function () { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('documentload', true, true, {}); + window.dispatchEvent(event); + }); + + PDFView.loadingBar.setWidth(container); + + PDFFindController.resolveFirstPage(); + + // Initialize the browsing history. + PDFHistory.initialize(self.documentFingerprint); + }); + + // Fetch the necessary preference values. + var showPreviousViewOnLoad; + var showPreviousViewOnLoadPromise = + Preferences.get('showPreviousViewOnLoad').then(function (prefValue) { + showPreviousViewOnLoad = prefValue; + }); + var defaultZoomValue; + var defaultZoomValuePromise = + Preferences.get('defaultZoomValue').then(function (prefValue) { + defaultZoomValue = prefValue; + }); + + var storePromise = store.initializedPromise; + Promise.all([firstPagePromise, storePromise, showPreviousViewOnLoadPromise, + defaultZoomValuePromise]).then(function resolved() { + var storedHash = null; + if (showPreviousViewOnLoad && store.get('exists', false)) { + var pageNum = store.get('page', '1'); + var zoom = defaultZoomValue || store.get('zoom', PDFView.currentScale); + var left = store.get('scrollLeft', '0'); + var top = store.get('scrollTop', '0'); + + storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' + + left + ',' + top; + } else if (defaultZoomValue) { + storedHash = 'page=1&zoom=' + defaultZoomValue; + } + self.setInitialView(storedHash, scale); + + // Make all navigation keys work on document load, + // unless the viewer is embedded in a web page. + if (!self.isViewerEmbedded) { + self.container.focus(); + self.container.blur(); + } + }, function rejected(errorMsg) { + console.error(errorMsg); + + firstPagePromise.then(function () { + self.setInitialView(null, scale); + }); + }); + + pagesPromise.then(function() { + if (PDFView.supportsPrinting) { + pdfDocument.getJavaScript().then(function(javaScript) { + if (javaScript.length) { + console.warn('Warning: JavaScript is not supported'); + PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript); + } + // Hack to support auto printing. + var regex = /\bprint\s*\(/g; + for (var i = 0, ii = javaScript.length; i < ii; i++) { + var js = javaScript[i]; + if (js && regex.test(js)) { + setTimeout(function() { + window.print(); + }); + return; + } + } + }); + } + }); + + var destinationsPromise = + this.destinationsPromise = pdfDocument.getDestinations(); + destinationsPromise.then(function(destinations) { + self.destinations = destinations; + }); + + // outline depends on destinations and pagesRefMap + var promises = [pagesPromise, destinationsPromise, + PDFView.animationStartedPromise]; + Promise.all(promises).then(function() { + pdfDocument.getOutline().then(function(outline) { + self.outline = new DocumentOutlineView(outline); + document.getElementById('viewOutline').disabled = !outline; + + if (outline) { + Preferences.get('ifAvailableShowOutlineOnLoad').then( + function (prefValue) { + if (prefValue) { + if (!self.sidebarOpen) { + document.getElementById('sidebarToggle').click(); + } + self.switchSidebarView('outline'); + } + }); + } + }); + pdfDocument.getAttachments().then(function(attachments) { + self.attachments = new DocumentAttachmentsView(attachments); + document.getElementById('viewAttachments').disabled = !attachments; + }); + }); + + pdfDocument.getMetadata().then(function(data) { + var info = data.info, metadata = data.metadata; + self.documentInfo = info; + self.metadata = metadata; + + // Provides some basic debug information + console.log('PDF ' + pdfDocument.fingerprint + ' [' + + info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() + + ' / ' + (info.Creator || '-').trim() + ']' + + ' (PDF.js: ' + (PDFJS.version || '-') + + (!PDFJS.disableWebGL ? ' [WebGL]' : '') + ')'); + + var pdfTitle; + if (metadata && metadata.has('dc:title')) { + pdfTitle = metadata.get('dc:title'); + } + + if (!pdfTitle && info && info['Title']) { + pdfTitle = info['Title']; + } + + if (pdfTitle) { + self.setTitle(pdfTitle + ' - ' + document.title); + } + + if (info.IsAcroFormPresent) { + console.warn('Warning: AcroForm/XFA is not supported'); + PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.forms); + } + + var versionId = String(info.PDFFormatVersion).slice(-1) | 0; + var generatorId = 0; + var KNOWN_GENERATORS = ["acrobat distiller", "acrobat pdfwritter", + "adobe livecycle", "adobe pdf library", "adobe photoshop", "ghostscript", + "tcpdf", "cairo", "dvipdfm", "dvips", "pdftex", "pdfkit", "itext", + "prince", "quarkxpress", "mac os x", "microsoft", "openoffice", "oracle", + "luradocument", "pdf-xchange", "antenna house", "aspose.cells", "fpdf"]; + var generatorId = 0; + if (info.Producer) { + KNOWN_GENERATORS.some(function (generator, s, i) { + if (generator.indexOf(s) < 0) { + return false; + } + generatorId = i + 1; + return true; + }.bind(null, info.Producer.toLowerCase())); + } + var formType = !info.IsAcroFormPresent ? null : info.IsXFAPresent ? + 'xfa' : 'acroform'; + FirefoxCom.request('reportTelemetry', JSON.stringify({ + type: 'documentInfo', + version: versionId, + generator: generatorId, + formType: formType + })); + }); + }, + + setInitialView: function pdfViewSetInitialView(storedHash, scale) { + // Reset the current scale, as otherwise the page's scale might not get + // updated if the zoom level stayed the same. + this.currentScale = 0; + this.currentScaleValue = null; + // When opening a new file (when one is already loaded in the viewer): + // Reset 'currentPageNumber', since otherwise the page's scale will be wrong + // if 'currentPageNumber' is larger than the number of pages in the file. + document.getElementById('pageNumber').value = currentPageNumber = 1; + // Reset the current position when loading a new file, + // to prevent displaying the wrong position in the document. + this.currentPosition = null; + + if (PDFHistory.initialDestination) { + this.navigateTo(PDFHistory.initialDestination); + PDFHistory.initialDestination = null; + } else if (this.initialBookmark) { + this.setHash(this.initialBookmark); + PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark); + this.initialBookmark = null; + } else if (storedHash) { + this.setHash(storedHash); + } else if (scale) { + this.setScale(scale, true); + this.page = 1; + } + + if (PDFView.currentScale === UNKNOWN_SCALE) { + // Scale was not initialized: invalid bookmark or scale was not specified. + // Setting the default one. + this.setScale(DEFAULT_SCALE, true); + } + }, + + renderHighestPriority: function pdfViewRenderHighestPriority() { + if (PDFView.idleTimeout) { + clearTimeout(PDFView.idleTimeout); + PDFView.idleTimeout = null; + } + + // Pages have a higher priority than thumbnails, so check them first. + var visiblePages = this.getVisiblePages(); + var pageView = this.getHighestPriority(visiblePages, this.pages, + this.pageViewScroll.down); + if (pageView) { + this.renderView(pageView, 'page'); + return; + } + // No pages needed rendering so check thumbnails. + if (this.sidebarOpen) { + var visibleThumbs = this.getVisibleThumbs(); + var thumbView = this.getHighestPriority(visibleThumbs, + this.thumbnails, + this.thumbnailViewScroll.down); + if (thumbView) { + this.renderView(thumbView, 'thumbnail'); + return; + } + } + + PDFView.idleTimeout = setTimeout(function () { + PDFView.cleanup(); + }, CLEANUP_TIMEOUT); + }, + + cleanup: function pdfViewCleanup() { + for (var i = 0, ii = this.pages.length; i < ii; i++) { + if (this.pages[i] && + this.pages[i].renderingState !== RenderingStates.FINISHED) { + this.pages[i].reset(); + } + } + this.pdfDocument.cleanup(); + }, + + getHighestPriority: function pdfViewGetHighestPriority(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; + + 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; + } + } + + // 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 false; + }, + + isViewFinished: function pdfViewIsViewFinished(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. + renderView: function pdfViewRender(view, type) { + var state = view.renderingState; + switch (state) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + PDFView.highestPriorityPage = type + view.id; + view.resume(); + break; + case RenderingStates.RUNNING: + PDFView.highestPriorityPage = type + view.id; + break; + case RenderingStates.INITIAL: + PDFView.highestPriorityPage = type + view.id; + view.draw(this.renderHighestPriority.bind(this)); + break; + } + return true; + }, + + setHash: function pdfViewSetHash(hash) { + if (!hash) { + return; + } + + if (hash.indexOf('=') >= 0) { + var params = PDFView.parseQueryString(hash); + // borrowing syntax from "Parameters for Opening PDF Files" + if ('nameddest' in params) { + PDFHistory.updateNextHashParam(params.nameddest); + PDFView.navigateTo(params.nameddest); + return; + } + var pageNumber, dest; + if ('page' in params) { + pageNumber = (params.page | 0) || 1; + } + if ('zoom' in params) { + var zoomArgs = params.zoom.split(','); // scale,left,top + // building destination array + + // If the zoom value, it has to get divided by 100. If it is a string, + // it should stay as it is. + var zoomArg = zoomArgs[0]; + var zoomArgNumber = parseFloat(zoomArg); + if (zoomArgNumber) { + zoomArg = zoomArgNumber / 100; + } + dest = [null, {name: 'XYZ'}, + zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null, + zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null, + zoomArg]; + } + if (dest) { + var currentPage = this.pages[(pageNumber || this.page) - 1]; + currentPage.scrollIntoView(dest); + } else if (pageNumber) { + this.page = pageNumber; // simple page + } + if ('pagemode' in params) { + var toggle = document.getElementById('sidebarToggle'); + if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' || + params.pagemode === 'attachments') { + if (!this.sidebarOpen) { + toggle.click(); + } + this.switchSidebarView(params.pagemode === 'bookmarks' ? + 'outline' : + params.pagemode); + } else if (params.pagemode === 'none' && this.sidebarOpen) { + toggle.click(); + } + } + } else if (/^\d+$/.test(hash)) { // page number + this.page = hash; + } else { // named destination + PDFHistory.updateNextHashParam(unescape(hash)); + PDFView.navigateTo(unescape(hash)); + } + }, + + switchSidebarView: function pdfViewSwitchSidebarView(view) { + var thumbsView = document.getElementById('thumbnailView'); + var outlineView = document.getElementById('outlineView'); + var attachmentsView = document.getElementById('attachmentsView'); + + var thumbsButton = document.getElementById('viewThumbnail'); + var outlineButton = document.getElementById('viewOutline'); + var attachmentsButton = document.getElementById('viewAttachments'); + + switch (view) { + case 'thumbs': + var wasAnotherViewVisible = thumbsView.classList.contains('hidden'); + + thumbsButton.classList.add('toggled'); + outlineButton.classList.remove('toggled'); + attachmentsButton.classList.remove('toggled'); + thumbsView.classList.remove('hidden'); + outlineView.classList.add('hidden'); + attachmentsView.classList.add('hidden'); + + PDFView.renderHighestPriority(); + + if (wasAnotherViewVisible) { + // Ensure that the thumbnail of the current page is visible + // when switching from another view. + scrollIntoView(document.getElementById('thumbnailContainer' + + this.page)); + } + break; + + case 'outline': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.add('toggled'); + attachmentsButton.classList.remove('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.remove('hidden'); + attachmentsView.classList.add('hidden'); + + if (outlineButton.getAttribute('disabled')) { + return; + } + break; + + case 'attachments': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.remove('toggled'); + attachmentsButton.classList.add('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.add('hidden'); + attachmentsView.classList.remove('hidden'); + + if (attachmentsButton.getAttribute('disabled')) { + return; + } + break; + } + }, + + getVisiblePages: function pdfViewGetVisiblePages() { + if (!PresentationMode.active) { + return this.getVisibleElements(this.container, this.pages, true); + } else { + // The algorithm in getVisibleElements doesn't work in all browsers and + // configurations when presentation mode is active. + var visible = []; + var currentPage = this.pages[this.page - 1]; + visible.push({ id: currentPage.id, view: currentPage }); + return { first: currentPage, last: currentPage, views: visible }; + } + }, + + getVisibleThumbs: function pdfViewGetVisibleThumbs() { + return this.getVisibleElements(this.thumbnailContainer, this.thumbnails); + }, + + // Generic helper to find out what elements are visible within a scroll pane. + getVisibleElements: function pdfViewGetVisibleElements( + scrollEl, views, sortByVisibility) { + var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; + var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; + + var visible = [], view; + var currentHeight, viewHeight, hiddenHeight, percentHeight; + var currentWidth, viewWidth; + for (var i = 0, ii = views.length; i < ii; ++i) { + view = views[i]; + currentHeight = view.el.offsetTop + view.el.clientTop; + viewHeight = view.el.clientHeight; + if ((currentHeight + viewHeight) < top) { + continue; + } + if (currentHeight > bottom) { + break; + } + currentWidth = view.el.offsetLeft + view.el.clientLeft; + viewWidth = view.el.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; + + visible.push({ id: view.id, x: currentWidth, y: currentHeight, + view: view, percent: percentHeight }); + } + + 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}; + }, + + // Helper function to parse query string (e.g. ?param1=value&parm2=...). + parseQueryString: function pdfViewParseQueryString(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]; + var value = param.length > 1 ? param[1] : null; + params[decodeURIComponent(key)] = decodeURIComponent(value); + } + return params; + }, + + beforePrint: function pdfViewSetupBeforePrint() { + if (!this.supportsPrinting) { + var printMessage = mozL10n.get('printing_not_supported', null, + 'Warning: Printing is not fully supported by this browser.'); + this.error(printMessage); + return; + } + + var alertNotReady = false; + var i, ii; + if (!this.pages.length) { + alertNotReady = true; + } else { + for (i = 0, ii = this.pages.length; i < ii; ++i) { + if (!this.pages[i].pdfPage) { + alertNotReady = true; + break; + } + } + } + if (alertNotReady) { + var notReadyMessage = mozL10n.get('printing_not_ready', null, + 'Warning: The PDF is not fully loaded for printing.'); + window.alert(notReadyMessage); + return; + } + + var body = document.querySelector('body'); + body.setAttribute('data-mozPrintCallback', true); + for (i = 0, ii = this.pages.length; i < ii; ++i) { + this.pages[i].beforePrint(); + } + }, + + afterPrint: function pdfViewSetupAfterPrint() { + var div = document.getElementById('printContainer'); + while (div.hasChildNodes()) { + div.removeChild(div.lastChild); + } + }, + + rotatePages: function pdfViewRotatePages(delta) { + var currentPage = this.pages[this.page - 1]; + var i, l; + this.pageRotation = (this.pageRotation + 360 + delta) % 360; + + for (i = 0, l = this.pages.length; i < l; i++) { + var page = this.pages[i]; + page.update(page.scale, this.pageRotation); + } + + for (i = 0, l = this.thumbnails.length; i < l; i++) { + var thumb = this.thumbnails[i]; + thumb.update(this.pageRotation); + } + + this.setScale(this.currentScaleValue, true, true); + + this.renderHighestPriority(); + + if (currentPage) { + currentPage.scrollIntoView(); + } + }, + + /** + * This function flips the page in presentation mode if the user scrolls up + * or down with large enough motion and prevents page flipping too often. + * + * @this {PDFView} + * @param {number} mouseScrollDelta The delta value from the mouse event. + */ + mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) { + var MOUSE_SCROLL_COOLDOWN_TIME = 50; + + var currentTime = (new Date()).getTime(); + var storedTime = this.mouseScrollTimeStamp; + + // In case one page has already been flipped there is a cooldown time + // which has to expire before next page can be scrolled on to. + if (currentTime > storedTime && + currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) { + return; + } + + // In case the user decides to scroll to the opposite direction than before + // clear the accumulated delta. + if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) || + (this.mouseScrollDelta < 0 && mouseScrollDelta > 0)) { + this.clearMouseScrollState(); + } + + this.mouseScrollDelta += mouseScrollDelta; + + var PAGE_FLIP_THRESHOLD = 120; + if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) { + + var PageFlipDirection = { + UP: -1, + DOWN: 1 + }; + + // In presentation mode scroll one page at a time. + var pageFlipDirection = (this.mouseScrollDelta > 0) ? + PageFlipDirection.UP : + PageFlipDirection.DOWN; + this.clearMouseScrollState(); + var currentPage = this.page; + + // In case we are already on the first or the last page there is no need + // to do anything. + if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) || + (currentPage == this.pages.length && + pageFlipDirection == PageFlipDirection.DOWN)) { + return; + } + + this.page += pageFlipDirection; + this.mouseScrollTimeStamp = currentTime; + } + }, + + /** + * This function clears the member attributes used with mouse scrolling in + * presentation mode. + * + * @this {PDFView} + */ + clearMouseScrollState: function pdfViewClearMouseScrollState() { + this.mouseScrollTimeStamp = 0; + this.mouseScrollDelta = 0; + } +}; + + +var PageView = function pageView(container, id, scale, + navigateTo, defaultViewport) { + this.id = id; + + this.rotation = 0; + this.scale = scale || 1.0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + + this.textLayer = null; + + this.zoomLayer = null; + + this.annotationLayer = null; + + var anchor = document.createElement('a'); + anchor.name = '' + this.id; + + var div = this.el = 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'; + + container.appendChild(anchor); + container.appendChild(div); + + this.setPdfPage = function pageViewSetPdfPage(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(); + }; + + this.destroy = function pageViewDestroy() { + this.zoomLayer = null; + this.reset(); + if (this.pdfPage) { + this.pdfPage.destroy(); + } + }; + + this.reset = function pageViewReset(keepAnnotations) { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + + var childNodes = div.childNodes; + for (var i = div.childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if ((this.zoomLayer && this.zoomLayer === node) || + (keepAnnotations && this.annotationLayer === node)) { + continue; + } + div.removeChild(node); + } + div.removeAttribute('data-loaded'); + + if (keepAnnotations) { + if (this.annotationLayer) { + // Hide annotationLayer until all elements are resized + // so they are not displayed on the already-resized page + this.annotationLayer.setAttribute('hidden', 'true'); + } + } else { + this.annotationLayer = null; + } + + delete this.canvas; + + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }; + + this.update = function pageViewUpdate(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 + }); + + if (USE_ONLY_CSS_ZOOM && this.canvas) { + this.cssTransform(this.canvas); + return; + } else if (this.canvas && !this.zoomLayer) { + this.zoomLayer = this.canvas.parentNode; + this.zoomLayer.style.position = 'absolute'; + } + if (this.zoomLayer) { + this.cssTransform(this.zoomLayer.firstChild); + } + this.reset(true); + }; + + this.cssTransform = function pageCssTransform(canvas) { + // Scale canvas, canvas wrapper, and page container. + var width = this.viewport.width; + var height = this.viewport.height; + 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, so 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 textRelativeRotation = this.viewport.rotation - + this.textLayer.viewport.rotation; + var textAbsRotation = Math.abs(textRelativeRotation); + var scale = (width / canvas.width); + if (textAbsRotation === 90 || textAbsRotation === 270) { + scale = width / canvas.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 (USE_ONLY_CSS_ZOOM && this.annotationLayer) { + setupAnnotations(div, this.pdfPage, this.viewport); + } + }; + + Object.defineProperty(this, 'width', { + get: function PageView_getWidth() { + return this.viewport.width; + }, + enumerable: true + }); + + Object.defineProperty(this, 'height', { + get: function PageView_getHeight() { + return this.viewport.height; + }, + enumerable: true + }); + + var self = this; + + function setupAnnotations(pageDiv, pdfPage, viewport) { + + function bindLink(link, dest) { + link.href = PDFView.getDestinationHash(dest); + link.onclick = function pageViewSetupLinksOnclick() { + if (dest) { + PDFView.navigateTo(dest); + } + return false; + }; + if (dest) { + link.className = 'internalLink'; + } + } + + function bindNamedAction(link, action) { + link.href = PDFView.getAnchorUrl(''); + link.onclick = function pageViewSetupNamedActionOnClick() { + // See PDF reference, table 8.45 - Named action + switch (action) { + case 'GoToPage': + document.getElementById('pageNumber').focus(); + break; + + case 'GoBack': + PDFHistory.back(); + break; + + case 'GoForward': + PDFHistory.forward(); + break; + + case 'Find': + if (!PDFView.supportsIntegratedFind) { + PDFFindBar.toggle(); + } + break; + + case 'NextPage': + PDFView.page++; + break; + + case 'PrevPage': + PDFView.page--; + break; + + case 'LastPage': + PDFView.page = PDFView.pages.length; + break; + + case 'FirstPage': + PDFView.page = 1; + break; + + default: + break; // No action according to spec + } + return false; + }; + link.className = 'internalLink'; + } + + pdfPage.getAnnotations().then(function(annotationsData) { + viewport = viewport.clone({ dontFlip: true }); + var transform = viewport.transform; + var transformStr = 'matrix(' + transform.join(',') + ')'; + var data, element, i, ii; + + if (self.annotationLayer) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + element = self.annotationLayer.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, transformStr); + } + } + // See this.reset() + self.annotationLayer.removeAttribute('hidden'); + } else { + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + var annotation = PDFJS.Annotation.fromData(data); + if (!annotation || !annotation.hasHtml()) { + continue; + } + + element = annotation.getHtmlElement(pdfPage.commonObjs); + element.setAttribute('data-annotation-id', data.id); + mozL10n.translate(element); + + data = annotation.getData(); + var rect = data.rect; + var view = pdfPage.view; + rect = PDFJS.Util.normalizeRect([ + rect[0], + view[3] - rect[1] + view[1], + rect[2], + view[3] - rect[3] + view[1] + ]); + element.style.left = rect[0] + 'px'; + element.style.top = rect[1] + 'px'; + element.style.position = 'absolute'; + + CustomStyle.setProp('transform', element, transformStr); + var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; + CustomStyle.setProp('transformOrigin', element, transformOriginStr); + + if (data.subtype === 'Link' && !data.url) { + var link = element.getElementsByTagName('a')[0]; + if (link) { + if (data.action) { + bindNamedAction(link, data.action); + } else { + bindLink(link, ('dest' in data) ? data.dest : null); + } + } + } + + if (!self.annotationLayer) { + var annotationLayerDiv = document.createElement('div'); + annotationLayerDiv.className = 'annotationLayer'; + pageDiv.appendChild(annotationLayerDiv); + self.annotationLayer = annotationLayerDiv; + } + + self.annotationLayer.appendChild(element); + } + } + }); + } + + this.getPagePoint = function pageViewGetPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }; + + this.scrollIntoView = function pageViewScrollIntoView(dest) { + if (PresentationMode.active) { + if (PDFView.page !== this.id) { + // Avoid breaking PDFView.getVisiblePages in presentation mode. + PDFView.page = this.id; + return; + } + dest = null; + PDFView.setScale(PDFView.currentScaleValue, true, true); + } + if (!dest) { + scrollIntoView(div); + return; + } + + var x = 0, y = 0; + var width = 0, height = 0, widthScale, heightScale; + var changeOrientation = (this.rotation % 180 === 0 ? false : true); + var pageWidth = (changeOrientation ? this.height : this.width) / + this.scale / CSS_UNITS; + var pageHeight = (changeOrientation ? this.width : this.height) / + this.scale / CSS_UNITS; + var scale = 0; + switch (dest[1].name) { + case 'XYZ': + x = dest[2]; + y = dest[3]; + scale = dest[4]; + // If x and/or y coordinates are not supplied, default to + // _top_ left of the page (not the obvious bottom left, + // since aligning the bottom of the intended page with the + // top of the window is rarely helpful). + x = x !== null ? x : 0; + y = y !== null ? y : pageHeight; + break; + case 'Fit': + case 'FitB': + scale = 'page-fit'; + break; + case 'FitH': + case 'FitBH': + y = dest[2]; + scale = 'page-width'; + break; + case 'FitV': + case 'FitBV': + x = dest[2]; + width = pageWidth; + height = pageHeight; + scale = 'page-height'; + break; + case 'FitR': + x = dest[2]; + y = dest[3]; + width = dest[4] - x; + height = dest[5] - y; + widthScale = (PDFView.container.clientWidth - SCROLLBAR_PADDING) / + width / CSS_UNITS; + heightScale = (PDFView.container.clientHeight - SCROLLBAR_PADDING) / + height / CSS_UNITS; + scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); + break; + default: + return; + } + + if (scale && scale !== PDFView.currentScale) { + PDFView.setScale(scale, true, true); + } else if (PDFView.currentScale === UNKNOWN_SCALE) { + PDFView.setScale(DEFAULT_SCALE, true, true); + } + + if (scale === 'page-fit' && !dest[4]) { + scrollIntoView(div); + return; + } + + var boundingRect = [ + this.viewport.convertToViewportPoint(x, y), + this.viewport.convertToViewportPoint(x + width, y + height) + ]; + var left = Math.min(boundingRect[0][0], boundingRect[1][0]); + var top = Math.min(boundingRect[0][1], boundingRect[1][1]); + + scrollIntoView(div, { left: left, top: top }); + }; + + this.getTextContent = function pageviewGetTextContent() { + return PDFView.getPage(this.id).then(function(pdfPage) { + return pdfPage.getTextContent(); + }); + }; + + this.draw = function pageviewDraw(callback) { + var pdfPage = this.pdfPage; + + if (this.pagePdfPromise) { + return; + } + if (!pdfPage) { + var promise = PDFView.getPage(this.id); + promise.then(function(pdfPage) { + delete this.pagePdfPromise; + this.setPdfPage(pdfPage); + this.draw(callback); + }.bind(this)); + this.pagePdfPromise = promise; + return; + } + + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + + this.renderingState = RenderingStates.RUNNING; + + var viewport = this.viewport; + // 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'); + + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + canvasWrapper.appendChild(canvas); + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(canvasWrapper, this.annotationLayer); + } else { + div.appendChild(canvasWrapper); + } + this.canvas = canvas; + + var ctx = canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + + if (USE_ONLY_CSS_ZOOM) { + 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; + } + + canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; + canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; + canvas.style.width = Math.floor(viewport.width) + 'px'; + canvas.style.height = Math.floor(viewport.height) + 'px'; + // Add the viewport so it's known what it was originally drawn with. + canvas._viewport = viewport; + + var textLayerDiv = null; + if (!PDFJS.disableTextLayer) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvas.style.width; + textLayerDiv.style.height = canvas.style.height; + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer); + } else { + div.appendChild(textLayerDiv); + } + } + var textLayer = this.textLayer = + textLayerDiv ? new TextLayerBuilder({ + textLayerDiv: textLayerDiv, + pageIndex: this.id - 1, + lastScrollSource: PDFView, + viewport: this.viewport, + isViewerInPresentationMode: PresentationMode.active + }) : null; + // TODO(mack): use data attributes to store these + ctx._scaleX = outputScale.sx; + ctx._scaleY = outputScale.sy; + if (outputScale.scaled) { + ctx.scale(outputScale.sx, outputScale.sy); + } + + // Rendering area + + 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; + } + + if (error === 'cancelled') { + return; + } + + self.renderingState = RenderingStates.FINISHED; + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } + + if (self.zoomLayer) { + div.removeChild(self.zoomLayer); + self.zoomLayer = null; + } + + if (self.textLayer && self.textLayer.textDivs && + self.textLayer.textDivs.length > 0 && + !PDFView.supportsDocumentColors) { + console.error(mozL10n.get('document_colors_disabled', null, + 'PDF documents are not allowed to use their own colors: ' + + '\'Allow pages to choose their own colors\' ' + + 'is deactivated in the browser.')); + PDFView.fallback(); + } + if (error) { + PDFView.error(mozL10n.get('rendering_error', null, + 'An error occurred while rendering the page.'), error); + } + + self.stats = pdfPage.stats; + self.updateStats(); + if (self.onAfterDraw) { + self.onAfterDraw(); + } + + cache.push(self); + + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagerender', true, true, { + pageNumber: pdfPage.pageNumber + }); + div.dispatchEvent(event); + + FirefoxCom.request('reportTelemetry', JSON.stringify({ + type: 'pageInfo' + })); + // TODO add stream types report here + callback(); + } + + var renderContext = { + canvasContext: ctx, + viewport: this.viewport, + textLayer: textLayer, + // intent: 'default', // === 'display' + continueCallback: function pdfViewcContinueCallback(cont) { + if (PDFView.highestPriorityPage !== 'page' + self.id) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + } + }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + + this.renderTask.promise.then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + if (textLayer) { + self.getTextContent().then( + function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + } + ); + } + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); + } + ); + + setupAnnotations(div, pdfPage, this.viewport); + div.setAttribute('data-loaded', true); + }; + + this.beforePrint = function pageViewBeforePrint() { + 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'); + canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; + canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; + canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; + canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; + var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + + (1 / PRINT_OUTPUT_SCALE) + ')'; + CustomStyle.setProp('transform' , canvas, cssScale); + CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); + + var printContainer = document.getElementById('printContainer'); + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = viewport.width + 'pt'; + canvasWrapper.style.height = viewport.height + 'pt'; + canvasWrapper.appendChild(canvas); + printContainer.appendChild(canvasWrapper); + + 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(); + 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(); + } + }); + }; + }; + + this.updateStats = function pageViewUpdateStats() { + if (!this.stats) { + return; + } + + if (PDFJS.pdfBug && Stats.enabled) { + var stats = this.stats; + Stats.add(this.id, stats); + } + }; +}; + + +var ThumbnailView = function thumbnailView(container, id, defaultViewport) { + var anchor = document.createElement('a'); + anchor.href = PDFView.getAnchorUrl('#page=' + id); + anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); + anchor.onclick = function stopNavigation() { + PDFView.page = id; + return false; + }; + + this.pdfPage = undefined; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + + this.rotation = 0; + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + this.id = id; + + this.canvasWidth = 98; + this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; + this.scale = (this.canvasWidth / this.pageWidth); + + var div = this.el = document.createElement('div'); + div.id = 'thumbnailContainer' + id; + div.className = 'thumbnail'; + + if (id === 1) { + // Highlight the thumbnail of the first page when no page number is + // specified (or exists in cache) when the document is loaded. + div.classList.add('selected'); + } + + var ring = document.createElement('div'); + ring.className = 'thumbnailSelectionRing'; + ring.style.width = this.canvasWidth + 'px'; + ring.style.height = this.canvasHeight + 'px'; + + div.appendChild(ring); + anchor.appendChild(div); + container.appendChild(anchor); + + this.hasImage = false; + this.renderingState = RenderingStates.INITIAL; + + this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(1, totalRotation); + this.update(); + }; + + this.update = function thumbnailViewUpdate(rotation) { + if (rotation !== undefined) { + this.rotation = rotation; + } + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: 1, + rotation: totalRotation + }); + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + + this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; + this.scale = (this.canvasWidth / this.pageWidth); + + div.removeAttribute('data-loaded'); + ring.textContent = ''; + ring.style.width = this.canvasWidth + 'px'; + ring.style.height = this.canvasHeight + 'px'; + + this.hasImage = false; + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + }; + + this.getPageDrawContext = function thumbnailViewGetPageDrawContext() { + var canvas = document.createElement('canvas'); + canvas.id = 'thumbnail' + id; + + canvas.width = this.canvasWidth; + canvas.height = this.canvasHeight; + canvas.className = 'thumbnailImage'; + canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', + {page: id}, 'Thumbnail of Page {{page}}')); + + div.setAttribute('data-loaded', true); + + ring.appendChild(canvas); + + var ctx = canvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); + ctx.restore(); + return ctx; + }; + + this.drawingRequired = function thumbnailViewDrawingRequired() { + return !this.hasImage; + }; + + this.draw = function thumbnailViewDraw(callback) { + if (!this.pdfPage) { + var promise = PDFView.getPage(this.id); + promise.then(function(pdfPage) { + this.setPdfPage(pdfPage); + this.draw(callback); + }.bind(this)); + return; + } + + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + + this.renderingState = RenderingStates.RUNNING; + if (this.hasImage) { + callback(); + return; + } + + var self = this; + var ctx = this.getPageDrawContext(); + var drawViewport = this.viewport.clone({ scale: this.scale }); + var renderContext = { + canvasContext: ctx, + viewport: drawViewport, + continueCallback: function(cont) { + if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + } + }; + this.pdfPage.render(renderContext).promise.then( + function pdfPageRenderCallback() { + self.renderingState = RenderingStates.FINISHED; + callback(); + }, + function pdfPageRenderError(error) { + self.renderingState = RenderingStates.FINISHED; + callback(); + } + ); + this.hasImage = true; + }; + + this.setImage = function thumbnailViewSetImage(img) { + if (!this.pdfPage) { + var promise = PDFView.getPage(this.id); + promise.then(function(pdfPage) { + this.setPdfPage(pdfPage); + this.setImage(img); + }.bind(this)); + return; + } + if (this.hasImage || !img) { + return; + } + this.renderingState = RenderingStates.FINISHED; + var ctx = this.getPageDrawContext(); + ctx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, ctx.canvas.width, ctx.canvas.height); + + this.hasImage = true; + }; +}; + + +var FIND_SCROLL_OFFSET_TOP = -50; +var FIND_SCROLL_OFFSET_LEFT = -400; + +/** + * TextLayerBuilder provides text-selection + * functionality for the PDF. It does this + * by creating overlay divs over the PDF + * text. This divs contain text that matches + * the PDF text they are overlaying. This + * object also provides for a way to highlight + * text that is being searched for. + */ +var TextLayerBuilder = function textLayerBuilder(options) { + var textLayerFrag = document.createDocumentFragment(); + + this.textLayerDiv = options.textLayerDiv; + this.layoutDone = false; + this.divContentDone = false; + this.pageIdx = options.pageIndex; + this.matches = []; + this.lastScrollSource = options.lastScrollSource; + this.viewport = options.viewport; + this.isViewerInPresentationMode = options.isViewerInPresentationMode; + this.textDivs = []; + + if (typeof PDFFindController === 'undefined') { + window.PDFFindController = null; + } + + if (typeof this.lastScrollSource === 'undefined') { + this.lastScrollSource = null; + } + + this.renderLayer = function textLayerBuilderRenderLayer() { + var textDivs = this.textDivs; + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + // No point in rendering so many divs as it'd make the browser unusable + // even after the divs are rendered + var MAX_TEXT_DIVS_TO_RENDER = 100000; + if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER) { + return; + } + + for (var i = 0, ii = textDivs.length; i < ii; i++) { + var textDiv = textDivs[i]; + if ('isWhitespace' in textDiv.dataset) { + continue; + } + + ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily; + var width = ctx.measureText(textDiv.textContent).width; + + if (width > 0) { + textLayerFrag.appendChild(textDiv); + var textScale = textDiv.dataset.canvasWidth / width; + var rotation = textDiv.dataset.angle; + var transform = 'scale(' + textScale + ', 1)'; + transform = 'rotate(' + rotation + 'deg) ' + transform; + CustomStyle.setProp('transform' , textDiv, transform); + CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%'); + } + } + + this.textLayerDiv.appendChild(textLayerFrag); + this.renderingDone = true; + this.updateMatches(); + }; + + this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() { + // Schedule renderLayout() if user has been scrolling, otherwise + // run it right away + var RENDER_DELAY = 200; // in ms + var self = this; + var lastScroll = (this.lastScrollSource === null ? + 0 : this.lastScrollSource.lastScroll); + + if (Date.now() - lastScroll > RENDER_DELAY) { + // Render right away + this.renderLayer(); + } else { + // Schedule + if (this.renderTimer) { + clearTimeout(this.renderTimer); + } + this.renderTimer = setTimeout(function() { + self.setupRenderLayoutTimer(); + }, RENDER_DELAY); + } + }; + + this.appendText = function textLayerBuilderAppendText(geom, styles) { + var style = styles[geom.fontName]; + var textDiv = document.createElement('div'); + this.textDivs.push(textDiv); + if (!/\S/.test(geom.str)) { + textDiv.dataset.isWhitespace = true; + return; + } + var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); + var angle = Math.atan2(tx[1], tx[0]); + if (style.vertical) { + angle += Math.PI / 2; + } + var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); + var fontAscent = (style.ascent ? style.ascent * fontHeight : + (style.descent ? (1 + style.descent) * fontHeight : fontHeight)); + + textDiv.style.position = 'absolute'; + textDiv.style.left = (tx[4] + (fontAscent * Math.sin(angle))) + 'px'; + textDiv.style.top = (tx[5] - (fontAscent * Math.cos(angle))) + 'px'; + textDiv.style.fontSize = fontHeight + 'px'; + textDiv.style.fontFamily = style.fontFamily; + + textDiv.textContent = geom.str; + textDiv.dataset.fontName = geom.fontName; + textDiv.dataset.angle = angle * (180 / Math.PI); + if (style.vertical) { + textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; + } else { + textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; + } + + }; + + this.setTextContent = function textLayerBuilderSetTextContent(textContent) { + this.textContent = textContent; + + var textItems = textContent.items; + for (var i = 0; i < textItems.length; i++) { + this.appendText(textItems[i], textContent.styles); + } + this.divContentDone = true; + + this.setupRenderLayoutTimer(); + }; + + this.convertMatches = function textLayerBuilderConvertMatches(matches) { + var i = 0; + var iIndex = 0; + var bidiTexts = this.textContent.items; + var end = bidiTexts.length - 1; + var queryLen = (PDFFindController === null ? + 0 : PDFFindController.state.query.length); + + var ret = []; + + // Loop over all the matches. + for (var m = 0; m < matches.length; m++) { + var matchIdx = matches[m]; + // # Calculate the begin position. + + // Loop over the divIdxs. + while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; + } + + // TODO: Do proper handling here if something goes wrong. + if (i == bidiTexts.length) { + console.error('Could not find matching mapping'); + } + + var match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex + } + }; + + // # Calculate the end position. + matchIdx += queryLen; + + // Somewhat same array as above, but use a > instead of >= to get the end + // position right. + while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; + } + + match.end = { + divIdx: i, + offset: matchIdx - iIndex + }; + ret.push(match); + } + + return ret; + }; + + this.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 isSelectedPage = (PDFFindController === null ? + false : (this.pageIdx === PDFFindController.selected.pageIdx)); + + var selectedMatchIdx = (PDFFindController === null ? + -1 : PDFFindController.selected.matchIdx); + + var highlightAll = (PDFFindController === null ? + false : PDFFindController.state.highlightAll); + + var infty = { + divIdx: -1, + offset: undefined + }; + + function beginText(begin, className) { + var divIdx = begin.divIdx; + var div = textDivs[divIdx]; + div.textContent = ''; + appendTextToDiv(divIdx, 0, begin.offset, className); + } + + function appendText(from, to, className) { + appendTextToDiv(from.divIdx, from.offset, to.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); + } + + function highlightDiv(divIdx, className) { + textDivs[divIdx].className = className; + } + + var i0 = selectedMatchIdx, i1 = i0 + 1, i; + + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + // Not highlighting all and this isn't the selected page, so do nothing. + return; + } + + for (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 (isSelected && !this.isViewerInPresentationMode) { + scrollIntoView(textDivs[begin.divIdx], { top: FIND_SCROLL_OFFSET_TOP, + left: FIND_SCROLL_OFFSET_LEFT }); + } + + // 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) { + appendText(prevEnd, infty); + } + // clears the divs and set the content until the begin point. + beginText(begin); + } else { + appendText(prevEnd, begin); + } + + if (begin.divIdx === end.divIdx) { + appendText(begin, end, 'highlight' + highlightSuffix); + } else { + appendText(begin, infty, 'highlight begin' + highlightSuffix); + for (var n = begin.divIdx + 1; n < end.divIdx; n++) { + highlightDiv(n, 'highlight middle' + highlightSuffix); + } + beginText(end, 'highlight end' + highlightSuffix); + } + prevEnd = end; + } + + if (prevEnd) { + appendText(prevEnd, infty); + } + }; + + this.updateMatches = function textLayerUpdateMatches() { + // Only show matches, once all rendering is done. + if (!this.renderingDone) { + return; + } + + // Clear out all matches. + var matches = this.matches; + var textDivs = this.textDivs; + var bidiTexts = this.textContent.items; + var clearedUntilDivIdx = -1; + + // Clear out all current matches. + for (var i = 0; i < matches.length; i++) { + var match = matches[i]; + var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (var n = begin; n <= match.end.divIdx; n++) { + var div = textDivs[n]; + div.textContent = bidiTexts[n].str; + div.className = ''; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } + + if (PDFFindController === null || !PDFFindController.active) { + return; + } + + // Convert the matches on the page controller into the match format used + // for the textLayer. + this.matches = matches = (this.convertMatches(PDFFindController === null ? + [] : (PDFFindController.pageMatches[this.pageIdx] || []))); + + this.renderMatches(this.matches); + }; +}; + + + +var DocumentOutlineView = function documentOutlineView(outline) { + var outlineView = document.getElementById('outlineView'); + while (outlineView.firstChild) { + outlineView.removeChild(outlineView.firstChild); + } + + if (!outline) { + if (!outlineView.classList.contains('hidden')) { + PDFView.switchSidebarView('thumbs'); + } + return; + } + + function bindItemLink(domObj, item) { + domObj.href = PDFView.getDestinationHash(item.dest); + domObj.onclick = function documentOutlineViewOnclick(e) { + PDFView.navigateTo(item.dest); + return false; + }; + } + + + var queue = [{parent: outlineView, items: outline}]; + while (queue.length > 0) { + var levelData = queue.shift(); + var i, n = levelData.items.length; + for (i = 0; i < n; i++) { + var item = levelData.items[i]; + var div = document.createElement('div'); + div.className = 'outlineItem'; + var a = document.createElement('a'); + bindItemLink(a, item); + a.textContent = item.title; + div.appendChild(a); + + if (item.items.length > 0) { + var itemsDiv = document.createElement('div'); + itemsDiv.className = 'outlineItems'; + div.appendChild(itemsDiv); + queue.push({parent: itemsDiv, items: item.items}); + } + + levelData.parent.appendChild(div); + } + } +}; + +var DocumentAttachmentsView = function documentAttachmentsView(attachments) { + var attachmentsView = document.getElementById('attachmentsView'); + while (attachmentsView.firstChild) { + attachmentsView.removeChild(attachmentsView.firstChild); + } + + if (!attachments) { + if (!attachmentsView.classList.contains('hidden')) { + PDFView.switchSidebarView('thumbs'); + } + return; + } + + function bindItemLink(domObj, item) { + domObj.href = '#'; + domObj.onclick = function documentAttachmentsViewOnclick(e) { + var downloadManager = new DownloadManager(); + downloadManager.downloadData(item.content, getFileName(item.filename), + ''); + return false; + }; + } + + var names = Object.keys(attachments).sort(function(a,b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }); + for (var i = 0, ii = names.length; i < ii; i++) { + var item = attachments[names[i]]; + var div = document.createElement('div'); + div.className = 'attachmentsItem'; + var a = document.createElement('a'); + bindItemLink(a, item); + a.textContent = getFileName(item.filename); + div.appendChild(a); + attachmentsView.appendChild(div); + } +}; + + +function webViewerLoad(evt) { + PDFView.initialize().then(webViewerInitialized); +} + +function webViewerInitialized() { + var file = window.location.href.split('#')[0]; + + document.getElementById('openFile').setAttribute('hidden', 'true'); + document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true'); + + // Special debugging flags in the hash section of the URL. + var hash = document.location.hash.substring(1); + var hashParams = PDFView.parseQueryString(hash); + + if ('disableWorker' in hashParams) { + PDFJS.disableWorker = (hashParams['disableWorker'] === 'true'); + } + + if ('disableRange' in hashParams) { + PDFJS.disableRange = (hashParams['disableRange'] === 'true'); + } + + if ('disableAutoFetch' in hashParams) { + PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true'); + } + + if ('disableFontFace' in hashParams) { + PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true'); + } + + if ('disableHistory' in hashParams) { + PDFJS.disableHistory = (hashParams['disableHistory'] === 'true'); + } + + if ('webgl' in hashParams) { + PDFJS.disableWebGL = (hashParams['webgl'] !== 'true'); + } + + if ('useOnlyCssZoom' in hashParams) { + USE_ONLY_CSS_ZOOM = (hashParams['useOnlyCssZoom'] === 'true'); + } + + if ('verbosity' in hashParams) { + PDFJS.verbosity = hashParams['verbosity'] | 0; + } + + if ('ignoreCurrentPositionOnZoom' in hashParams) { + IGNORE_CURRENT_POSITION_ON_ZOOM = + (hashParams['ignoreCurrentPositionOnZoom'] === 'true'); + } + + + + if (!PDFView.supportsDocumentFonts) { + PDFJS.disableFontFace = true; + console.warn(mozL10n.get('web_fonts_disabled', null, + 'Web fonts are disabled: unable to use embedded PDF fonts.')); + } + + if ('textLayer' in hashParams) { + switch (hashParams['textLayer']) { + case 'off': + PDFJS.disableTextLayer = true; + break; + case 'visible': + case 'shadow': + case 'hover': + var viewer = document.getElementById('viewer'); + viewer.classList.add('textLayer-' + hashParams['textLayer']); + break; + } + } + + if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) { + PDFJS.pdfBug = true; + var pdfBug = hashParams['pdfBug']; + var enabled = pdfBug.split(','); + PDFBug.enable(enabled); + PDFBug.init(); + } + + if (!PDFView.supportsPrinting) { + document.getElementById('print').classList.add('hidden'); + document.getElementById('secondaryPrint').classList.add('hidden'); + } + + if (!PDFView.supportsFullscreen) { + document.getElementById('presentationMode').classList.add('hidden'); + document.getElementById('secondaryPresentationMode'). + classList.add('hidden'); + } + + if (PDFView.supportsIntegratedFind) { + document.getElementById('viewFind').classList.add('hidden'); + } + + // Listen for unsuporrted features to trigger the fallback UI. + PDFJS.UnsupportedManager.listen(PDFView.fallback.bind(PDFView)); + + // Suppress context menus for some controls + document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler; + + var mainContainer = document.getElementById('mainContainer'); + var outerContainer = document.getElementById('outerContainer'); + mainContainer.addEventListener('transitionend', function(e) { + if (e.target == mainContainer) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('resize', false, false, window, 0); + window.dispatchEvent(event); + outerContainer.classList.remove('sidebarMoving'); + } + }, true); + + document.getElementById('sidebarToggle').addEventListener('click', + function() { + this.classList.toggle('toggled'); + outerContainer.classList.add('sidebarMoving'); + outerContainer.classList.toggle('sidebarOpen'); + PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen'); + PDFView.renderHighestPriority(); + }); + + document.getElementById('viewThumbnail').addEventListener('click', + function() { + PDFView.switchSidebarView('thumbs'); + }); + + document.getElementById('viewOutline').addEventListener('click', + function() { + PDFView.switchSidebarView('outline'); + }); + + document.getElementById('viewAttachments').addEventListener('click', + function() { + PDFView.switchSidebarView('attachments'); + }); + + document.getElementById('previous').addEventListener('click', + function() { + PDFView.page--; + }); + + document.getElementById('next').addEventListener('click', + function() { + PDFView.page++; + }); + + document.getElementById('zoomIn').addEventListener('click', + function() { + PDFView.zoomIn(); + }); + + document.getElementById('zoomOut').addEventListener('click', + function() { + PDFView.zoomOut(); + }); + + document.getElementById('pageNumber').addEventListener('click', + function() { + this.select(); + }); + + document.getElementById('pageNumber').addEventListener('change', + function() { + // Handle the user inputting a floating point number. + PDFView.page = (this.value | 0); + + if (this.value !== (this.value | 0).toString()) { + this.value = PDFView.page; + } + }); + + document.getElementById('scaleSelect').addEventListener('change', + function() { + PDFView.setScale(this.value); + }); + + document.getElementById('presentationMode').addEventListener('click', + SecondaryToolbar.presentationModeClick.bind(SecondaryToolbar)); + + document.getElementById('openFile').addEventListener('click', + SecondaryToolbar.openFileClick.bind(SecondaryToolbar)); + + document.getElementById('print').addEventListener('click', + SecondaryToolbar.printClick.bind(SecondaryToolbar)); + + document.getElementById('download').addEventListener('click', + SecondaryToolbar.downloadClick.bind(SecondaryToolbar)); + + PDFView.setTitleUsingUrl(file); + PDFView.initPassiveLoading(); + return; + + if (file) { + PDFView.open(file, 0); + } +} + +document.addEventListener('DOMContentLoaded', webViewerLoad, true); + +function updateViewarea() { + + if (!PDFView.initialized) { + return; + } + var visible = PDFView.getVisiblePages(); + var visiblePages = visible.views; + if (visiblePages.length === 0) { + return; + } + + PDFView.renderHighestPriority(); + + var currentId = PDFView.page; + var firstPage = visible.first; + + for (var i = 0, ii = visiblePages.length, stillFullyVisible = false; + i < ii; ++i) { + var page = visiblePages[i]; + + if (page.percent < 100) { + break; + } + if (page.id === PDFView.page) { + stillFullyVisible = true; + break; + } + } + + if (!stillFullyVisible) { + currentId = visiblePages[0].id; + } + + if (!PresentationMode.active) { + updateViewarea.inProgress = true; // used in "set page" + PDFView.page = currentId; + updateViewarea.inProgress = false; + } + + var currentScale = PDFView.currentScale; + var currentScaleValue = PDFView.currentScaleValue; + var normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? + Math.round(currentScale * 10000) / 100 : currentScaleValue; + + var pageNumber = firstPage.id; + var pdfOpenParams = '#page=' + pageNumber; + pdfOpenParams += '&zoom=' + normalizedScaleValue; + var currentPage = PDFView.pages[pageNumber - 1]; + var container = PDFView.container; + var topLeft = currentPage.getPagePoint((container.scrollLeft - firstPage.x), + (container.scrollTop - firstPage.y)); + var intLeft = Math.round(topLeft[0]); + var intTop = Math.round(topLeft[1]); + pdfOpenParams += ',' + intLeft + ',' + intTop; + + if (PresentationMode.active || PresentationMode.switchInProgress) { + PDFView.currentPosition = null; + } else { + PDFView.currentPosition = { page: pageNumber, left: intLeft, top: intTop }; + } + + var store = PDFView.store; + store.initializedPromise.then(function() { + store.set('exists', true); + store.set('page', pageNumber); + store.set('zoom', normalizedScaleValue); + store.set('scrollLeft', intLeft); + store.set('scrollTop', intTop); + }); + var href = PDFView.getAnchorUrl(pdfOpenParams); + document.getElementById('viewBookmark').href = href; + document.getElementById('secondaryViewBookmark').href = href; + + // Update the current bookmark in the browsing history. + PDFHistory.updateCurrentBookmark(pdfOpenParams, pageNumber); +} + +window.addEventListener('resize', function webViewerResize(evt) { + if (PDFView.initialized && + (document.getElementById('pageWidthOption').selected || + document.getElementById('pageFitOption').selected || + document.getElementById('pageAutoOption').selected)) { + PDFView.setScale(document.getElementById('scaleSelect').value); + } + updateViewarea(); + + // Set the 'max-height' CSS property of the secondary toolbar. + SecondaryToolbar.setMaxHeight(PDFView.container); +}); + +window.addEventListener('hashchange', function webViewerHashchange(evt) { + if (PDFHistory.isHashChangeUnlocked) { + PDFView.setHash(document.location.hash.substring(1)); + } +}); + + +function selectScaleOption(value) { + var options = document.getElementById('scaleSelect').options; + var predefinedValueFound = false; + for (var i = 0; i < options.length; i++) { + var option = options[i]; + if (option.value != value) { + option.selected = false; + continue; + } + option.selected = true; + predefinedValueFound = true; + } + return predefinedValueFound; +} + +window.addEventListener('localized', function localized(evt) { + document.getElementsByTagName('html')[0].dir = mozL10n.getDirection(); + + PDFView.animationStartedPromise.then(function() { + // Adjust the width of the zoom box to fit the content. + // Note: This is only done if the zoom box is actually visible, + // since otherwise element.clientWidth will return 0. + var container = document.getElementById('scaleSelectContainer'); + if (container.clientWidth > 0) { + var select = document.getElementById('scaleSelect'); + select.setAttribute('style', 'min-width: inherit;'); + var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING; + select.setAttribute('style', 'min-width: ' + + (width + SCALE_SELECT_PADDING) + 'px;'); + container.setAttribute('style', 'min-width: ' + width + 'px; ' + + 'max-width: ' + width + 'px;'); + } + + // Set the 'max-height' CSS property of the secondary toolbar. + SecondaryToolbar.setMaxHeight(PDFView.container); + }); +}, true); + +window.addEventListener('scalechange', function scalechange(evt) { + document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE); + document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE); + + var customScaleOption = document.getElementById('customScaleOption'); + customScaleOption.selected = false; + + if (!evt.resetAutoSettings && + (document.getElementById('pageWidthOption').selected || + document.getElementById('pageFitOption').selected || + document.getElementById('pageAutoOption').selected)) { + updateViewarea(); + return; + } + + var predefinedValueFound = selectScaleOption('' + evt.scale); + if (!predefinedValueFound) { + customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%'; + customScaleOption.selected = true; + } + updateViewarea(); +}, true); + +window.addEventListener('pagechange', function pagechange(evt) { + var page = evt.pageNumber; + if (PDFView.previousPageNumber !== page) { + document.getElementById('pageNumber').value = page; + var selected = document.querySelector('.thumbnail.selected'); + if (selected) { + selected.classList.remove('selected'); + } + var thumbnail = document.getElementById('thumbnailContainer' + page); + thumbnail.classList.add('selected'); + var visibleThumbs = PDFView.getVisibleThumbs(); + var numVisibleThumbs = visibleThumbs.views.length; + + // If the thumbnail isn't currently visible, scroll it into view. + if (numVisibleThumbs > 0) { + var first = visibleThumbs.first.id; + // Account for only one thumbnail being visible. + var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); + if (page <= first || page >= last) { + scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); + } + } + } + document.getElementById('previous').disabled = (page <= 1); + document.getElementById('next').disabled = (page >= PDFView.pages.length); +}, true); + +function handleMouseWheel(evt) { + var MOUSE_WHEEL_DELTA_FACTOR = 40; + var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail : + evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR; + var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn'; + + if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer + evt.preventDefault(); + PDFView[direction](Math.abs(ticks)); + } else if (PresentationMode.active) { + PDFView.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR); + } +} + +window.addEventListener('DOMMouseScroll', handleMouseWheel); +window.addEventListener('mousewheel', handleMouseWheel); + +window.addEventListener('click', function click(evt) { + if (!PresentationMode.active) { + if (SecondaryToolbar.opened && PDFView.container.contains(evt.target)) { + SecondaryToolbar.close(); + } + } else if (evt.button === 0) { + // Necessary since preventDefault() in 'mousedown' won't stop + // the event propagation in all circumstances in presentation mode. + evt.preventDefault(); + } +}, false); + +window.addEventListener('keydown', function keydown(evt) { + if (PasswordPrompt.visible) { + return; + } + + var handled = false; + var cmd = (evt.ctrlKey ? 1 : 0) | + (evt.altKey ? 2 : 0) | + (evt.shiftKey ? 4 : 0) | + (evt.metaKey ? 8 : 0); + + // First, handle the key bindings that are independent whether an input + // control is selected or not. + if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) { + // either CTRL or META key with optional SHIFT. + switch (evt.keyCode) { + case 70: // f + if (!PDFView.supportsIntegratedFind) { + PDFFindBar.open(); + handled = true; + } + break; + case 71: // g + if (!PDFView.supportsIntegratedFind) { + PDFFindBar.dispatchEvent('again', cmd === 5 || cmd === 12); + handled = true; + } + break; + case 61: // FF/Mac '=' + case 107: // FF '+' and '=' + case 187: // Chrome '+' + case 171: // FF with German keyboard + PDFView.zoomIn(); + handled = true; + break; + case 173: // FF/Mac '-' + case 109: // FF '-' + case 189: // Chrome '-' + PDFView.zoomOut(); + handled = true; + break; + case 48: // '0' + case 96: // '0' on Numpad of Swedish keyboard + // keeping it unhandled (to restore page zoom to 100%) + setTimeout(function () { + // ... and resetting the scale after browser adjusts its scale + PDFView.setScale(DEFAULT_SCALE, true); + }); + handled = false; + break; + } + } + + + // CTRL+ALT or Option+Command + if (cmd === 3 || cmd === 10) { + switch (evt.keyCode) { + case 80: // p + SecondaryToolbar.presentationModeClick(); + handled = true; + break; + case 71: // g + // focuses input#pageNumber field + document.getElementById('pageNumber').select(); + handled = true; + break; + } + } + + if (handled) { + evt.preventDefault(); + return; + } + + // Some shortcuts should not get handled if a control/input element + // is selected. + var curElement = document.activeElement || document.querySelector(':focus'); + var curElementTagName = curElement && curElement.tagName.toUpperCase(); + if (curElementTagName === 'INPUT' || + curElementTagName === 'TEXTAREA' || + curElementTagName === 'SELECT') { + // Make sure that the secondary toolbar is closed when Escape is pressed. + if (evt.keyCode !== 27) { // 'Esc' + return; + } + } + + if (cmd === 0) { // no control key pressed at all. + switch (evt.keyCode) { + case 38: // up arrow + case 33: // pg up + case 8: // backspace + if (!PresentationMode.active && + PDFView.currentScaleValue !== 'page-fit') { + break; + } + /* in presentation mode */ + /* falls through */ + case 37: // left arrow + // horizontal scrolling using arrow keys + if (PDFView.isHorizontalScrollbarEnabled) { + break; + } + /* falls through */ + case 75: // 'k' + case 80: // 'p' + PDFView.page--; + handled = true; + break; + case 27: // esc key + if (SecondaryToolbar.opened) { + SecondaryToolbar.close(); + handled = true; + } + if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) { + PDFFindBar.close(); + handled = true; + } + break; + case 40: // down arrow + case 34: // pg down + case 32: // spacebar + if (!PresentationMode.active && + PDFView.currentScaleValue !== 'page-fit') { + break; + } + /* falls through */ + case 39: // right arrow + // horizontal scrolling using arrow keys + if (PDFView.isHorizontalScrollbarEnabled) { + break; + } + /* falls through */ + case 74: // 'j' + case 78: // 'n' + PDFView.page++; + handled = true; + break; + + case 36: // home + if (PresentationMode.active) { + PDFView.page = 1; + handled = true; + } + break; + case 35: // end + if (PresentationMode.active) { + PDFView.page = PDFView.pdfDocument.numPages; + handled = true; + } + break; + + case 72: // 'h' + if (!PresentationMode.active) { + HandTool.toggle(); + } + break; + case 82: // 'r' + PDFView.rotatePages(90); + break; + } + } + + if (cmd === 4) { // shift-key + switch (evt.keyCode) { + case 32: // spacebar + if (!PresentationMode.active && + PDFView.currentScaleValue !== 'page-fit') { + break; + } + PDFView.page--; + handled = true; + break; + + case 82: // 'r' + PDFView.rotatePages(-90); + break; + } + } + + if (!handled && !PresentationMode.active) { + // 33=Page Up 34=Page Down 35=End 36=Home + // 37=Left 38=Up 39=Right 40=Down + if (evt.keyCode >= 33 && evt.keyCode <= 40 && + !PDFView.container.contains(curElement)) { + // The page container is not focused, but a page navigation key has been + // pressed. Change the focus to the viewer container to make sure that + // navigation by keyboard works as expected. + PDFView.container.focus(); + } + // 32=Spacebar + if (evt.keyCode === 32 && curElementTagName !== 'BUTTON') { + // Workaround for issue in Firefox, that prevents scroll keys from + // working when elements with 'tabindex' are focused. (#3498) + PDFView.container.blur(); + } + } + + if (cmd === 2) { // alt-key + switch (evt.keyCode) { + case 37: // left arrow + if (PresentationMode.active) { + PDFHistory.back(); + handled = true; + } + break; + case 39: // right arrow + if (PresentationMode.active) { + PDFHistory.forward(); + handled = true; + } + break; + } + } + + if (handled) { + evt.preventDefault(); + PDFView.clearMouseScrollState(); + } +}); + +window.addEventListener('beforeprint', function beforePrint(evt) { + PDFView.beforePrint(); +}); + +window.addEventListener('afterprint', function afterPrint(evt) { + PDFView.afterPrint(); +}); + +(function animationStartedClosure() { + // The offsetParent is not set until the pdf.js iframe or object is visible. + // Waiting for first animation. + var requestAnimationFrame = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function startAtOnce(callback) { callback(); }; + PDFView.animationStartedPromise = new Promise(function (resolve) { + requestAnimationFrame(function onAnimationFrame() { + resolve(); + }); + }); +})(); + +