browser/extensions/pdfjs/content/web/viewer.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
     3 /* Copyright 2012 Mozilla Foundation
     4  *
     5  * Licensed under the Apache License, Version 2.0 (the "License");
     6  * you may not use this file except in compliance with the License.
     7  * You may obtain a copy of the License at
     8  *
     9  *     http://www.apache.org/licenses/LICENSE-2.0
    10  *
    11  * Unless required by applicable law or agreed to in writing, software
    12  * distributed under the License is distributed on an "AS IS" BASIS,
    13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  * See the License for the specific language governing permissions and
    15  * limitations under the License.
    16  */
    17 /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, PDFFindBar, CustomStyle,
    18            PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager,
    19            getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory,
    20            Preferences, ViewHistory, PageView, ThumbnailView, URL,
    21            noContextMenuHandler, SecondaryToolbar, PasswordPrompt,
    22            PresentationMode, HandTool, Promise, DocumentProperties */
    24 'use strict';
    26 var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
    27 var DEFAULT_SCALE = 'auto';
    28 var DEFAULT_SCALE_DELTA = 1.1;
    29 var UNKNOWN_SCALE = 0;
    30 var CACHE_SIZE = 20;
    31 var CSS_UNITS = 96.0 / 72.0;
    32 var SCROLLBAR_PADDING = 40;
    33 var VERTICAL_PADDING = 5;
    34 var MAX_AUTO_SCALE = 1.25;
    35 var MIN_SCALE = 0.25;
    36 var MAX_SCALE = 4.0;
    37 var VIEW_HISTORY_MEMORY = 20;
    38 var SCALE_SELECT_CONTAINER_PADDING = 8;
    39 var SCALE_SELECT_PADDING = 22;
    40 var THUMBNAIL_SCROLL_MARGIN = -19;
    41 var USE_ONLY_CSS_ZOOM = false;
    42 var CLEANUP_TIMEOUT = 30000;
    43 var IGNORE_CURRENT_POSITION_ON_ZOOM = false;
    44 var RenderingStates = {
    45   INITIAL: 0,
    46   RUNNING: 1,
    47   PAUSED: 2,
    48   FINISHED: 3
    49 };
    50 var FindStates = {
    51   FIND_FOUND: 0,
    52   FIND_NOTFOUND: 1,
    53   FIND_WRAPPED: 2,
    54   FIND_PENDING: 3
    55 };
    57 PDFJS.imageResourcesPath = './images/';
    58   PDFJS.workerSrc = '../build/pdf.worker.js';
    59   PDFJS.cMapUrl = '../web/cmaps/';
    60   PDFJS.cMapPacked = true;
    62 var mozL10n = document.mozL10n || document.webL10n;
    65 // optimised CSS custom property getter/setter
    66 var CustomStyle = (function CustomStyleClosure() {
    68   // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
    69   //              animate-css-transforms-firefox-webkit.html
    70   // in some versions of IE9 it is critical that ms appear in this list
    71   // before Moz
    72   var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
    73   var _cache = {};
    75   function CustomStyle() {}
    77   CustomStyle.getProp = function get(propName, element) {
    78     // check cache only when no element is given
    79     if (arguments.length == 1 && typeof _cache[propName] == 'string') {
    80       return _cache[propName];
    81     }
    83     element = element || document.documentElement;
    84     var style = element.style, prefixed, uPropName;
    86     // test standard property first
    87     if (typeof style[propName] == 'string') {
    88       return (_cache[propName] = propName);
    89     }
    91     // capitalize
    92     uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
    94     // test vendor specific properties
    95     for (var i = 0, l = prefixes.length; i < l; i++) {
    96       prefixed = prefixes[i] + uPropName;
    97       if (typeof style[prefixed] == 'string') {
    98         return (_cache[propName] = prefixed);
    99       }
   100     }
   102     //if all fails then set to undefined
   103     return (_cache[propName] = 'undefined');
   104   };
   106   CustomStyle.setProp = function set(propName, element, str) {
   107     var prop = this.getProp(propName);
   108     if (prop != 'undefined') {
   109       element.style[prop] = str;
   110     }
   111   };
   113   return CustomStyle;
   114 })();
   116 function getFileName(url) {
   117   var anchor = url.indexOf('#');
   118   var query = url.indexOf('?');
   119   var end = Math.min(
   120     anchor > 0 ? anchor : url.length,
   121     query > 0 ? query : url.length);
   122   return url.substring(url.lastIndexOf('/', end) + 1, end);
   123 }
   125 /**
   126  * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
   127  * @return {Object} The object with horizontal (sx) and vertical (sy)
   128                     scales. The scaled property is set to false if scaling is
   129                     not required, true otherwise.
   130  */
   131 function getOutputScale(ctx) {
   132   var devicePixelRatio = window.devicePixelRatio || 1;
   133   var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
   134                           ctx.mozBackingStorePixelRatio ||
   135                           ctx.msBackingStorePixelRatio ||
   136                           ctx.oBackingStorePixelRatio ||
   137                           ctx.backingStorePixelRatio || 1;
   138   var pixelRatio = devicePixelRatio / backingStoreRatio;
   139   return {
   140     sx: pixelRatio,
   141     sy: pixelRatio,
   142     scaled: pixelRatio != 1
   143   };
   144 }
   146 /**
   147  * Scrolls specified element into view of its parent.
   148  * element {Object} The element to be visible.
   149  * spot {Object} An object with optional top and left properties,
   150  *               specifying the offset from the top left edge.
   151  */
   152 function scrollIntoView(element, spot) {
   153   // Assuming offsetParent is available (it's not available when viewer is in
   154   // hidden iframe or object). We have to scroll: if the offsetParent is not set
   155   // producing the error. See also animationStartedClosure.
   156   var parent = element.offsetParent;
   157   var offsetY = element.offsetTop + element.clientTop;
   158   var offsetX = element.offsetLeft + element.clientLeft;
   159   if (!parent) {
   160     console.error('offsetParent is not set -- cannot scroll');
   161     return;
   162   }
   163   while (parent.clientHeight === parent.scrollHeight) {
   164     if (parent.dataset._scaleY) {
   165       offsetY /= parent.dataset._scaleY;
   166       offsetX /= parent.dataset._scaleX;
   167     }
   168     offsetY += parent.offsetTop;
   169     offsetX += parent.offsetLeft;
   170     parent = parent.offsetParent;
   171     if (!parent) {
   172       return; // no need to scroll
   173     }
   174   }
   175   if (spot) {
   176     if (spot.top !== undefined) {
   177       offsetY += spot.top;
   178     }
   179     if (spot.left !== undefined) {
   180       offsetX += spot.left;
   181       parent.scrollLeft = offsetX;
   182     }
   183   }
   184   parent.scrollTop = offsetY;
   185 }
   187 /**
   188  * Event handler to suppress context menu.
   189  */
   190 function noContextMenuHandler(e) {
   191   e.preventDefault();
   192 }
   194 /**
   195  * Returns the filename or guessed filename from the url (see issue 3455).
   196  * url {String} The original PDF location.
   197  * @return {String} Guessed PDF file name.
   198  */
   199 function getPDFFileNameFromURL(url) {
   200   var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
   201   //            SCHEME      HOST         1.PATH  2.QUERY   3.REF
   202   // Pattern to get last matching NAME.pdf
   203   var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
   204   var splitURI = reURI.exec(url);
   205   var suggestedFilename = reFilename.exec(splitURI[1]) ||
   206                            reFilename.exec(splitURI[2]) ||
   207                            reFilename.exec(splitURI[3]);
   208   if (suggestedFilename) {
   209     suggestedFilename = suggestedFilename[0];
   210     if (suggestedFilename.indexOf('%') != -1) {
   211       // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
   212       try {
   213         suggestedFilename =
   214           reFilename.exec(decodeURIComponent(suggestedFilename))[0];
   215       } catch(e) { // Possible (extremely rare) errors:
   216         // URIError "Malformed URI", e.g. for "%AA.pdf"
   217         // TypeError "null has no properties", e.g. for "%2F.pdf"
   218       }
   219     }
   220   }
   221   return suggestedFilename || 'document.pdf';
   222 }
   224 var ProgressBar = (function ProgressBarClosure() {
   226   function clamp(v, min, max) {
   227     return Math.min(Math.max(v, min), max);
   228   }
   230   function ProgressBar(id, opts) {
   232     // Fetch the sub-elements for later.
   233     this.div = document.querySelector(id + ' .progress');
   235     // Get the loading bar element, so it can be resized to fit the viewer.
   236     this.bar = this.div.parentNode;
   238     // Get options, with sensible defaults.
   239     this.height = opts.height || 100;
   240     this.width = opts.width || 100;
   241     this.units = opts.units || '%';
   243     // Initialize heights.
   244     this.div.style.height = this.height + this.units;
   245     this.percent = 0;
   246   }
   248   ProgressBar.prototype = {
   250     updateBar: function ProgressBar_updateBar() {
   251       if (this._indeterminate) {
   252         this.div.classList.add('indeterminate');
   253         this.div.style.width = this.width + this.units;
   254         return;
   255       }
   257       this.div.classList.remove('indeterminate');
   258       var progressSize = this.width * this._percent / 100;
   259       this.div.style.width = progressSize + this.units;
   260     },
   262     get percent() {
   263       return this._percent;
   264     },
   266     set percent(val) {
   267       this._indeterminate = isNaN(val);
   268       this._percent = clamp(val, 0, 100);
   269       this.updateBar();
   270     },
   272     setWidth: function ProgressBar_setWidth(viewer) {
   273       if (viewer) {
   274         var container = viewer.parentNode;
   275         var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
   276         if (scrollbarWidth > 0) {
   277           this.bar.setAttribute('style', 'width: calc(100% - ' +
   278                                          scrollbarWidth + 'px);');
   279         }
   280       }
   281     },
   283     hide: function ProgressBar_hide() {
   284       this.bar.classList.add('hidden');
   285       this.bar.removeAttribute('style');
   286     }
   287   };
   289   return ProgressBar;
   290 })();
   292 var Cache = function cacheCache(size) {
   293   var data = [];
   294   this.push = function cachePush(view) {
   295     var i = data.indexOf(view);
   296     if (i >= 0) {
   297       data.splice(i);
   298     }
   299     data.push(view);
   300     if (data.length > size) {
   301       data.shift().destroy();
   302     }
   303   };
   304 };
   309 var DEFAULT_PREFERENCES = {
   310   showPreviousViewOnLoad: true,
   311   defaultZoomValue: '',
   312   ifAvailableShowOutlineOnLoad: false,
   313   enableHandToolOnLoad: false,
   314   enableWebGL: false
   315 };
   318 /**
   319  * Preferences - Utility for storing persistent settings.
   320  *   Used for settings that should be applied to all opened documents,
   321  *   or every time the viewer is loaded.
   322  */
   323 var Preferences = {
   324   prefs: Object.create(DEFAULT_PREFERENCES),
   325   isInitializedPromiseResolved: false,
   326   initializedPromise: null,
   328   /**
   329    * Initialize and fetch the current preference values from storage.
   330    * @return {Promise} A promise that is resolved when the preferences
   331    *                   have been initialized.
   332    */
   333   initialize: function preferencesInitialize() {
   334     return this.initializedPromise =
   335         this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
   336       this.isInitializedPromiseResolved = true;
   337       if (prefObj) {
   338         this.prefs = prefObj;
   339       }
   340     }.bind(this));
   341   },
   343   /**
   344    * Stub function for writing preferences to storage.
   345    * NOTE: This should be overridden by a build-specific function defined below.
   346    * @param {Object} prefObj The preferences that should be written to storage.
   347    * @return {Promise} A promise that is resolved when the preference values
   348    *                   have been written.
   349    */
   350   _writeToStorage: function preferences_writeToStorage(prefObj) {
   351     return Promise.resolve();
   352   },
   354   /**
   355    * Stub function for reading preferences from storage.
   356    * NOTE: This should be overridden by a build-specific function defined below.
   357    * @param {Object} prefObj The preferences that should be read from storage.
   358    * @return {Promise} A promise that is resolved with an {Object} containing
   359    *                   the preferences that have been read.
   360    */
   361   _readFromStorage: function preferences_readFromStorage(prefObj) {
   362     return Promise.resolve();
   363   },
   365   /**
   366    * Reset the preferences to their default values and update storage.
   367    * @return {Promise} A promise that is resolved when the preference values
   368    *                   have been reset.
   369    */
   370   reset: function preferencesReset() {
   371     return this.initializedPromise.then(function() {
   372       this.prefs = Object.create(DEFAULT_PREFERENCES);
   373       return this._writeToStorage(DEFAULT_PREFERENCES);
   374     }.bind(this));
   375   },
   377   /**
   378    * Replace the current preference values with the ones from storage.
   379    * @return {Promise} A promise that is resolved when the preference values
   380    *                   have been updated.
   381    */
   382   reload: function preferencesReload() {
   383     return this.initializedPromise.then(function () {
   384       this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
   385         if (prefObj) {
   386           this.prefs = prefObj;
   387         }
   388       }.bind(this));
   389     }.bind(this));
   390   },
   392   /**
   393    * Set the value of a preference.
   394    * @param {string} name The name of the preference that should be changed.
   395    * @param {boolean|number|string} value The new value of the preference.
   396    * @return {Promise} A promise that is resolved when the value has been set,
   397    *                   provided that the preference exists and the types match.
   398    */
   399   set: function preferencesSet(name, value) {
   400     return this.initializedPromise.then(function () {
   401       if (DEFAULT_PREFERENCES[name] === undefined) {
   402         throw new Error('preferencesSet: \'' + name + '\' is undefined.');
   403       } else if (value === undefined) {
   404         throw new Error('preferencesSet: no value is specified.');
   405       }
   406       var valueType = typeof value;
   407       var defaultType = typeof DEFAULT_PREFERENCES[name];
   409       if (valueType !== defaultType) {
   410         if (valueType === 'number' && defaultType === 'string') {
   411           value = value.toString();
   412         } else {
   413           throw new Error('Preferences_set: \'' + value + '\' is a \"' +
   414                           valueType + '\", expected \"' + defaultType + '\".');
   415         }
   416       } else {
   417         if (valueType === 'number' && (value | 0) !== value) {
   418           throw new Error('Preferences_set: \'' + value +
   419                           '\' must be an \"integer\".');
   420         }
   421       }
   422       this.prefs[name] = value;
   423       return this._writeToStorage(this.prefs);
   424     }.bind(this));
   425   },
   427   /**
   428    * Get the value of a preference.
   429    * @param {string} name The name of the preference whose value is requested.
   430    * @return {Promise} A promise that is resolved with a {boolean|number|string}
   431    *                   containing the value of the preference.
   432    */
   433   get: function preferencesGet(name) {
   434     return this.initializedPromise.then(function () {
   435       var defaultValue = DEFAULT_PREFERENCES[name];
   437       if (defaultValue === undefined) {
   438         throw new Error('preferencesGet: \'' + name + '\' is undefined.');
   439       } else {
   440         var prefValue = this.prefs[name];
   442         if (prefValue !== undefined) {
   443           return prefValue;
   444         }
   445       }
   446       return defaultValue;
   447     }.bind(this));
   448   }
   449 };
   457 var FirefoxCom = (function FirefoxComClosure() {
   458   return {
   459     /**
   460      * Creates an event that the extension is listening for and will
   461      * synchronously respond to.
   462      * NOTE: It is reccomended to use request() instead since one day we may not
   463      * be able to synchronously reply.
   464      * @param {String} action The action to trigger.
   465      * @param {String} data Optional data to send.
   466      * @return {*} The response.
   467      */
   468     requestSync: function(action, data) {
   469       var request = document.createTextNode('');
   470       document.documentElement.appendChild(request);
   472       var sender = document.createEvent('CustomEvent');
   473       sender.initCustomEvent('pdf.js.message', true, false,
   474                              {action: action, data: data, sync: true});
   475       request.dispatchEvent(sender);
   476       var response = sender.detail.response;
   477       document.documentElement.removeChild(request);
   478       return response;
   479     },
   480     /**
   481      * Creates an event that the extension is listening for and will
   482      * asynchronously respond by calling the callback.
   483      * @param {String} action The action to trigger.
   484      * @param {String} data Optional data to send.
   485      * @param {Function} callback Optional response callback that will be called
   486      * with one data argument.
   487      */
   488     request: function(action, data, callback) {
   489       var request = document.createTextNode('');
   490       if (callback) {
   491         document.addEventListener('pdf.js.response', function listener(event) {
   492           var node = event.target;
   493           var response = event.detail.response;
   495           document.documentElement.removeChild(node);
   497           document.removeEventListener('pdf.js.response', listener, false);
   498           return callback(response);
   499         }, false);
   500       }
   501       document.documentElement.appendChild(request);
   503       var sender = document.createEvent('CustomEvent');
   504       sender.initCustomEvent('pdf.js.message', true, false,
   505                              {action: action, data: data, sync: false,
   506                               callback: callback});
   507       return request.dispatchEvent(sender);
   508     }
   509   };
   510 })();
   512 var DownloadManager = (function DownloadManagerClosure() {
   513   function DownloadManager() {}
   515   DownloadManager.prototype = {
   516     downloadUrl: function DownloadManager_downloadUrl(url, filename) {
   517       FirefoxCom.request('download', {
   518         originalUrl: url,
   519         filename: filename
   520       });
   521     },
   523     downloadData: function DownloadManager_downloadData(data, filename,
   524                                                         contentType) {
   525       var blobUrl = PDFJS.createObjectURL(data, contentType);
   527       FirefoxCom.request('download', {
   528         blobUrl: blobUrl,
   529         originalUrl: blobUrl,
   530         filename: filename,
   531         isAttachment: true
   532       });
   533     },
   535     download: function DownloadManager_download(blob, url, filename) {
   536       var blobUrl = window.URL.createObjectURL(blob);
   538       FirefoxCom.request('download', {
   539         blobUrl: blobUrl,
   540         originalUrl: url,
   541         filename: filename
   542       },
   543         function response(err) {
   544           if (err && this.onerror) {
   545             this.onerror(err);
   546           }
   547           window.URL.revokeObjectURL(blobUrl);
   548         }.bind(this)
   549       );
   550     }
   551   };
   553   return DownloadManager;
   554 })();
   556 Preferences._writeToStorage = function (prefObj) {
   557   return new Promise(function (resolve) {
   558     FirefoxCom.request('setPreferences', prefObj, resolve);
   559   });
   560 };
   562 Preferences._readFromStorage = function (prefObj) {
   563   return new Promise(function (resolve) {
   564     FirefoxCom.request('getPreferences', prefObj, function (prefStr) {
   565       var readPrefs = JSON.parse(prefStr);
   566       resolve(readPrefs);
   567     });
   568   });
   569 };
   573 var cache = new Cache(CACHE_SIZE);
   574 var currentPageNumber = 1;
   577 /**
   578  * View History - This is a utility for saving various view parameters for
   579  *                recently opened files.
   580  *
   581  * The way that the view parameters are stored depends on how PDF.js is built,
   582  * for 'node make <flag>' the following cases exist:
   583  *  - FIREFOX or MOZCENTRAL - uses sessionStorage.
   584  *  - B2G                   - uses asyncStorage.
   585  *  - GENERIC or CHROME     - uses localStorage, if it is available.
   586  */
   587 var ViewHistory = (function ViewHistoryClosure() {
   588   function ViewHistory(fingerprint) {
   589     this.fingerprint = fingerprint;
   590     var initializedPromiseResolve;
   591     this.isInitializedPromiseResolved = false;
   592     this.initializedPromise = new Promise(function (resolve) {
   593       initializedPromiseResolve = resolve;
   594     });
   596     var resolvePromise = (function ViewHistoryResolvePromise(db) {
   597       this.isInitializedPromiseResolved = true;
   598       this.initialize(db || '{}');
   599       initializedPromiseResolve();
   600     }).bind(this);
   603     var sessionHistory;
   604     try {
   605       // Workaround for security error when the preference
   606       // network.cookie.lifetimePolicy is set to 1, see Mozilla Bug 365772.
   607       sessionHistory = sessionStorage.getItem('pdfjsHistory');
   608     } catch (ex) {}
   609     resolvePromise(sessionHistory);
   611   }
   613   ViewHistory.prototype = {
   614     initialize: function ViewHistory_initialize(database) {
   615       database = JSON.parse(database);
   616       if (!('files' in database)) {
   617         database.files = [];
   618       }
   619       if (database.files.length >= VIEW_HISTORY_MEMORY) {
   620         database.files.shift();
   621       }
   622       var index;
   623       for (var i = 0, length = database.files.length; i < length; i++) {
   624         var branch = database.files[i];
   625         if (branch.fingerprint === this.fingerprint) {
   626           index = i;
   627           break;
   628         }
   629       }
   630       if (typeof index !== 'number') {
   631         index = database.files.push({fingerprint: this.fingerprint}) - 1;
   632       }
   633       this.file = database.files[index];
   634       this.database = database;
   635     },
   637     set: function ViewHistory_set(name, val) {
   638       if (!this.isInitializedPromiseResolved) {
   639         return;
   640       }
   641       var file = this.file;
   642       file[name] = val;
   643       var database = JSON.stringify(this.database);
   646       try {
   647         // See comment in try-catch block above.
   648         sessionStorage.setItem('pdfjsHistory', database);
   649       } catch (ex) {}
   651     },
   653     get: function ViewHistory_get(name, defaultValue) {
   654       if (!this.isInitializedPromiseResolved) {
   655         return defaultValue;
   656       }
   657       return this.file[name] || defaultValue;
   658     }
   659   };
   661   return ViewHistory;
   662 })();
   665 /**
   666  * Creates a "search bar" given set of DOM elements
   667  * that act as controls for searching, or for setting
   668  * search preferences in the UI. This object also sets
   669  * up the appropriate events for the controls. Actual
   670  * searching is done by PDFFindController
   671  */
   672 var PDFFindBar = {
   673   opened: false,
   674   bar: null,
   675   toggleButton: null,
   676   findField: null,
   677   highlightAll: null,
   678   caseSensitive: null,
   679   findMsg: null,
   680   findStatusIcon: null,
   681   findPreviousButton: null,
   682   findNextButton: null,
   684   initialize: function(options) {
   685     if(typeof PDFFindController === 'undefined' || PDFFindController === null) {
   686       throw 'PDFFindBar cannot be initialized ' +
   687             'without a PDFFindController instance.';
   688     }
   690     this.bar = options.bar;
   691     this.toggleButton = options.toggleButton;
   692     this.findField = options.findField;
   693     this.highlightAll = options.highlightAllCheckbox;
   694     this.caseSensitive = options.caseSensitiveCheckbox;
   695     this.findMsg = options.findMsg;
   696     this.findStatusIcon = options.findStatusIcon;
   697     this.findPreviousButton = options.findPreviousButton;
   698     this.findNextButton = options.findNextButton;
   700     var self = this;
   701     this.toggleButton.addEventListener('click', function() {
   702       self.toggle();
   703     });
   705     this.findField.addEventListener('input', function() {
   706       self.dispatchEvent('');
   707     });
   709     this.bar.addEventListener('keydown', function(evt) {
   710       switch (evt.keyCode) {
   711         case 13: // Enter
   712           if (evt.target === self.findField) {
   713             self.dispatchEvent('again', evt.shiftKey);
   714           }
   715           break;
   716         case 27: // Escape
   717           self.close();
   718           break;
   719       }
   720     });
   722     this.findPreviousButton.addEventListener('click',
   723       function() { self.dispatchEvent('again', true); }
   724     );
   726     this.findNextButton.addEventListener('click', function() {
   727       self.dispatchEvent('again', false);
   728     });
   730     this.highlightAll.addEventListener('click', function() {
   731       self.dispatchEvent('highlightallchange');
   732     });
   734     this.caseSensitive.addEventListener('click', function() {
   735       self.dispatchEvent('casesensitivitychange');
   736     });
   737   },
   739   dispatchEvent: function(aType, aFindPrevious) {
   740     var event = document.createEvent('CustomEvent');
   741     event.initCustomEvent('find' + aType, true, true, {
   742       query: this.findField.value,
   743       caseSensitive: this.caseSensitive.checked,
   744       highlightAll: this.highlightAll.checked,
   745       findPrevious: aFindPrevious
   746     });
   747     return window.dispatchEvent(event);
   748   },
   750   updateUIState: function(state, previous) {
   751     var notFound = false;
   752     var findMsg = '';
   753     var status = '';
   755     switch (state) {
   756       case FindStates.FIND_FOUND:
   757         break;
   759       case FindStates.FIND_PENDING:
   760         status = 'pending';
   761         break;
   763       case FindStates.FIND_NOTFOUND:
   764         findMsg = mozL10n.get('find_not_found', null, 'Phrase not found');
   765         notFound = true;
   766         break;
   768       case FindStates.FIND_WRAPPED:
   769         if (previous) {
   770           findMsg = mozL10n.get('find_reached_top', null,
   771                       'Reached top of document, continued from bottom');
   772         } else {
   773           findMsg = mozL10n.get('find_reached_bottom', null,
   774                                 'Reached end of document, continued from top');
   775         }
   776         break;
   777     }
   779     if (notFound) {
   780       this.findField.classList.add('notFound');
   781     } else {
   782       this.findField.classList.remove('notFound');
   783     }
   785     this.findField.setAttribute('data-status', status);
   786     this.findMsg.textContent = findMsg;
   787   },
   789   open: function() {
   790     if (!this.opened) {
   791       this.opened = true;
   792       this.toggleButton.classList.add('toggled');
   793       this.bar.classList.remove('hidden');
   794     }
   796     this.findField.select();
   797     this.findField.focus();
   798   },
   800   close: function() {
   801     if (!this.opened) {
   802       return;
   803     }
   804     this.opened = false;
   805     this.toggleButton.classList.remove('toggled');
   806     this.bar.classList.add('hidden');
   808     PDFFindController.active = false;
   809   },
   811   toggle: function() {
   812     if (this.opened) {
   813       this.close();
   814     } else {
   815       this.open();
   816     }
   817   }
   818 };
   822 /**
   823  * Provides a "search" or "find" functionality for the PDF.
   824  * This object actually performs the search for a given string.
   825  */
   827 var PDFFindController = {
   828   startedTextExtraction: false,
   830   extractTextPromises: [],
   832   pendingFindMatches: {},
   834   // If active, find results will be highlighted.
   835   active: false,
   837   // Stores the text for each page.
   838   pageContents: [],
   840   pageMatches: [],
   842   // Currently selected match.
   843   selected: {
   844     pageIdx: -1,
   845     matchIdx: -1
   846   },
   848   // Where find algorithm currently is in the document.
   849   offset: {
   850     pageIdx: null,
   851     matchIdx: null
   852   },
   854   resumePageIdx: null,
   856   state: null,
   858   dirtyMatch: false,
   860   findTimeout: null,
   862   pdfPageSource: null,
   864   integratedFind: false,
   866   initialize: function(options) {
   867     if(typeof PDFFindBar === 'undefined' || PDFFindBar === null) {
   868       throw 'PDFFindController cannot be initialized ' +
   869             'without a PDFFindBar instance';
   870     }
   872     this.pdfPageSource = options.pdfPageSource;
   873     this.integratedFind = options.integratedFind;
   875     var events = [
   876       'find',
   877       'findagain',
   878       'findhighlightallchange',
   879       'findcasesensitivitychange'
   880     ];
   882     this.firstPagePromise = new Promise(function (resolve) {
   883       this.resolveFirstPage = resolve;
   884     }.bind(this));
   885     this.handleEvent = this.handleEvent.bind(this);
   887     for (var i = 0; i < events.length; i++) {
   888       window.addEventListener(events[i], this.handleEvent);
   889     }
   890   },
   892   reset: function pdfFindControllerReset() {
   893     this.startedTextExtraction = false;
   894     this.extractTextPromises = [];
   895     this.active = false;
   896   },
   898   calcFindMatch: function(pageIndex) {
   899     var pageContent = this.pageContents[pageIndex];
   900     var query = this.state.query;
   901     var caseSensitive = this.state.caseSensitive;
   902     var queryLen = query.length;
   904     if (queryLen === 0) {
   905       // Do nothing the matches should be wiped out already.
   906       return;
   907     }
   909     if (!caseSensitive) {
   910       pageContent = pageContent.toLowerCase();
   911       query = query.toLowerCase();
   912     }
   914     var matches = [];
   916     var matchIdx = -queryLen;
   917     while (true) {
   918       matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
   919       if (matchIdx === -1) {
   920         break;
   921       }
   923       matches.push(matchIdx);
   924     }
   925     this.pageMatches[pageIndex] = matches;
   926     this.updatePage(pageIndex);
   927     if (this.resumePageIdx === pageIndex) {
   928       this.resumePageIdx = null;
   929       this.nextPageMatch();
   930     }
   931   },
   933   extractText: function() {
   934     if (this.startedTextExtraction) {
   935       return;
   936     }
   937     this.startedTextExtraction = true;
   939     this.pageContents = [];
   940     var extractTextPromisesResolves = [];
   941     for (var i = 0, ii = this.pdfPageSource.pdfDocument.numPages; i < ii; i++) {
   942       this.extractTextPromises.push(new Promise(function (resolve) {
   943         extractTextPromisesResolves.push(resolve);
   944       }));
   945     }
   947     var self = this;
   948     function extractPageText(pageIndex) {
   949       self.pdfPageSource.pages[pageIndex].getTextContent().then(
   950         function textContentResolved(textContent) {
   951           var textItems = textContent.items;
   952           var str = '';
   954           for (var i = 0; i < textItems.length; i++) {
   955             str += textItems[i].str;
   956           }
   958           // Store the pageContent as a string.
   959           self.pageContents.push(str);
   961           extractTextPromisesResolves[pageIndex](pageIndex);
   962           if ((pageIndex + 1) < self.pdfPageSource.pages.length) {
   963             extractPageText(pageIndex + 1);
   964           }
   965         }
   966       );
   967     }
   968     extractPageText(0);
   969   },
   971   handleEvent: function(e) {
   972     if (this.state === null || e.type !== 'findagain') {
   973       this.dirtyMatch = true;
   974     }
   975     this.state = e.detail;
   976     this.updateUIState(FindStates.FIND_PENDING);
   978     this.firstPagePromise.then(function() {
   979       this.extractText();
   981       clearTimeout(this.findTimeout);
   982       if (e.type === 'find') {
   983         // Only trigger the find action after 250ms of silence.
   984         this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
   985       } else {
   986         this.nextMatch();
   987       }
   988     }.bind(this));
   989   },
   991   updatePage: function(idx) {
   992     var page = this.pdfPageSource.pages[idx];
   994     if (this.selected.pageIdx === idx) {
   995       // If the page is selected, scroll the page into view, which triggers
   996       // rendering the page, which adds the textLayer. Once the textLayer is
   997       // build, it will scroll onto the selected match.
   998       page.scrollIntoView();
   999     }
  1001     if (page.textLayer) {
  1002       page.textLayer.updateMatches();
  1004   },
  1006   nextMatch: function() {
  1007     var previous = this.state.findPrevious;
  1008     var currentPageIndex = this.pdfPageSource.page - 1;
  1009     var numPages = this.pdfPageSource.pages.length;
  1011     this.active = true;
  1013     if (this.dirtyMatch) {
  1014       // Need to recalculate the matches, reset everything.
  1015       this.dirtyMatch = false;
  1016       this.selected.pageIdx = this.selected.matchIdx = -1;
  1017       this.offset.pageIdx = currentPageIndex;
  1018       this.offset.matchIdx = null;
  1019       this.hadMatch = false;
  1020       this.resumePageIdx = null;
  1021       this.pageMatches = [];
  1022       var self = this;
  1024       for (var i = 0; i < numPages; i++) {
  1025         // Wipe out any previous highlighted matches.
  1026         this.updatePage(i);
  1028         // As soon as the text is extracted start finding the matches.
  1029         if (!(i in this.pendingFindMatches)) {
  1030           this.pendingFindMatches[i] = true;
  1031           this.extractTextPromises[i].then(function(pageIdx) {
  1032             delete self.pendingFindMatches[pageIdx];
  1033             self.calcFindMatch(pageIdx);
  1034           });
  1039     // If there's no query there's no point in searching.
  1040     if (this.state.query === '') {
  1041       this.updateUIState(FindStates.FIND_FOUND);
  1042       return;
  1045     // If we're waiting on a page, we return since we can't do anything else.
  1046     if (this.resumePageIdx) {
  1047       return;
  1050     var offset = this.offset;
  1051     // If there's already a matchIdx that means we are iterating through a
  1052     // page's matches.
  1053     if (offset.matchIdx !== null) {
  1054       var numPageMatches = this.pageMatches[offset.pageIdx].length;
  1055       if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
  1056           (previous && offset.matchIdx > 0)) {
  1057         // The simple case, we just have advance the matchIdx to select the next
  1058         // match on the page.
  1059         this.hadMatch = true;
  1060         offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1;
  1061         this.updateMatch(true);
  1062         return;
  1064       // We went beyond the current page's matches, so we advance to the next
  1065       // page.
  1066       this.advanceOffsetPage(previous);
  1068     // Start searching through the page.
  1069     this.nextPageMatch();
  1070   },
  1072   matchesReady: function(matches) {
  1073     var offset = this.offset;
  1074     var numMatches = matches.length;
  1075     var previous = this.state.findPrevious;
  1076     if (numMatches) {
  1077       // There were matches for the page, so initialize the matchIdx.
  1078       this.hadMatch = true;
  1079       offset.matchIdx = previous ? numMatches - 1 : 0;
  1080       this.updateMatch(true);
  1081       // matches were found
  1082       return true;
  1083     } else {
  1084       // No matches attempt to search the next page.
  1085       this.advanceOffsetPage(previous);
  1086       if (offset.wrapped) {
  1087         offset.matchIdx = null;
  1088         if (!this.hadMatch) {
  1089           // No point in wrapping there were no matches.
  1090           this.updateMatch(false);
  1091           // while matches were not found, searching for a page 
  1092           // with matches should nevertheless halt.
  1093           return true;
  1096       // matches were not found (and searching is not done)
  1097       return false;
  1099   },
  1101   nextPageMatch: function() {
  1102     if (this.resumePageIdx !== null) {
  1103       console.error('There can only be one pending page.');
  1105     do {
  1106       var pageIdx = this.offset.pageIdx;
  1107       var matches = this.pageMatches[pageIdx];
  1108       if (!matches) {
  1109         // The matches don't exist yet for processing by "matchesReady",
  1110         // so set a resume point for when they do exist.
  1111         this.resumePageIdx = pageIdx;
  1112         break;
  1114     } while (!this.matchesReady(matches));
  1115   },
  1117   advanceOffsetPage: function(previous) {
  1118     var offset = this.offset;
  1119     var numPages = this.extractTextPromises.length;
  1120     offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
  1121     offset.matchIdx = null;
  1122     if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
  1123       offset.pageIdx = previous ? numPages - 1 : 0;
  1124       offset.wrapped = true;
  1125       return;
  1127   },
  1129   updateMatch: function(found) {
  1130     var state = FindStates.FIND_NOTFOUND;
  1131     var wrapped = this.offset.wrapped;
  1132     this.offset.wrapped = false;
  1133     if (found) {
  1134       var previousPage = this.selected.pageIdx;
  1135       this.selected.pageIdx = this.offset.pageIdx;
  1136       this.selected.matchIdx = this.offset.matchIdx;
  1137       state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND;
  1138       // Update the currently selected page to wipe out any selected matches.
  1139       if (previousPage !== -1 && previousPage !== this.selected.pageIdx) {
  1140         this.updatePage(previousPage);
  1143     this.updateUIState(state, this.state.findPrevious);
  1144     if (this.selected.pageIdx !== -1) {
  1145       this.updatePage(this.selected.pageIdx, true);
  1147   },
  1149   updateUIState: function(state, previous) {
  1150     if (this.integratedFind) {
  1151       FirefoxCom.request('updateFindControlState',
  1152                          {result: state, findPrevious: previous});
  1153       return;
  1155     PDFFindBar.updateUIState(state, previous);
  1157 };
  1161 var PDFHistory = {
  1162   initialized: false,
  1163   initialDestination: null,
  1165   initialize: function pdfHistoryInitialize(fingerprint) {
  1166     if (PDFJS.disableHistory || PDFView.isViewerEmbedded) {
  1167       // The browsing history is only enabled when the viewer is standalone,
  1168       // i.e. not when it is embedded in a web page.
  1169       return;
  1171     this.initialized = true;
  1172     this.reInitialized = false;
  1173     this.allowHashChange = true;
  1174     this.historyUnlocked = true;
  1176     this.previousHash = window.location.hash.substring(1);
  1177     this.currentBookmark = '';
  1178     this.currentPage = 0;
  1179     this.updatePreviousBookmark = false;
  1180     this.previousBookmark = '';
  1181     this.previousPage = 0;
  1182     this.nextHashParam = '';
  1184     this.fingerprint = fingerprint;
  1185     this.currentUid = this.uid = 0;
  1186     this.current = {};
  1188     var state = window.history.state;
  1189     if (this._isStateObjectDefined(state)) {
  1190       // This corresponds to navigating back to the document
  1191       // from another page in the browser history.
  1192       if (state.target.dest) {
  1193         this.initialDestination = state.target.dest;
  1194       } else {
  1195         PDFView.initialBookmark = state.target.hash;
  1197       this.currentUid = state.uid;
  1198       this.uid = state.uid + 1;
  1199       this.current = state.target;
  1200     } else {
  1201       // This corresponds to the loading of a new document.
  1202       if (state && state.fingerprint &&
  1203           this.fingerprint !== state.fingerprint) {
  1204         // Reinitialize the browsing history when a new document
  1205         // is opened in the web viewer.
  1206         this.reInitialized = true;
  1208       this._pushOrReplaceState({ fingerprint: this.fingerprint }, true);
  1211     var self = this;
  1212     window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
  1213       evt.preventDefault();
  1214       evt.stopPropagation();
  1216       if (!self.historyUnlocked) {
  1217         return;
  1219       if (evt.state) {
  1220         // Move back/forward in the history.
  1221         self._goTo(evt.state);
  1222       } else {
  1223         // Handle the user modifying the hash of a loaded document.
  1224         self.previousHash = window.location.hash.substring(1);
  1226         // If the history is empty when the hash changes,
  1227         // update the previous entry in the browser history.
  1228         if (self.uid === 0) {
  1229           var previousParams = (self.previousHash && self.currentBookmark &&
  1230                                 self.previousHash !== self.currentBookmark) ?
  1231             { hash: self.currentBookmark, page: self.currentPage } :
  1232             { page: 1 };
  1233           self.historyUnlocked = false;
  1234           self.allowHashChange = false;
  1235           window.history.back();
  1236           self._pushToHistory(previousParams, false, true);
  1237           window.history.forward();
  1238           self.historyUnlocked = true;
  1240         self._pushToHistory({ hash: self.previousHash }, false, true);
  1241         self._updatePreviousBookmark();
  1243     }, false);
  1245     function pdfHistoryBeforeUnload() {
  1246       var previousParams = self._getPreviousParams(null, true);
  1247       if (previousParams) {
  1248         var replacePrevious = (!self.current.dest &&
  1249                                self.current.hash !== self.previousHash);
  1250         self._pushToHistory(previousParams, false, replacePrevious);
  1251         self._updatePreviousBookmark();
  1253       // Remove the event listener when navigating away from the document,
  1254       // since 'beforeunload' prevents Firefox from caching the document.
  1255       window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false);
  1257     window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
  1259     window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
  1260       // If the entire viewer (including the PDF file) is cached in the browser,
  1261       // we need to reattach the 'beforeunload' event listener since
  1262       // the 'DOMContentLoaded' event is not fired on 'pageshow'.
  1263       window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
  1264     }, false);
  1265   },
  1267   _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
  1268     return (state && state.uid >= 0 &&
  1269             state.fingerprint && this.fingerprint === state.fingerprint &&
  1270             state.target && state.target.hash) ? true : false;
  1271   },
  1273   _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj,
  1274                                                               replace) {
  1275     if (replace) {
  1276       window.history.replaceState(stateObj, '');
  1277     } else {
  1278       window.history.pushState(stateObj, '');
  1280   },
  1282   get isHashChangeUnlocked() {
  1283     if (!this.initialized) {
  1284       return true;
  1286     // If the current hash changes when moving back/forward in the history,
  1287     // this will trigger a 'popstate' event *as well* as a 'hashchange' event.
  1288     // Since the hash generally won't correspond to the exact the position
  1289     // stored in the history's state object, triggering the 'hashchange' event
  1290     // can thus corrupt the browser history.
  1291     //
  1292     // When the hash changes during a 'popstate' event, we *only* prevent the
  1293     // first 'hashchange' event and immediately reset allowHashChange.
  1294     // If it is not reset, the user would not be able to change the hash.
  1296     var temp = this.allowHashChange;
  1297     this.allowHashChange = true;
  1298     return temp;
  1299   },
  1301   _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() {
  1302     if (this.updatePreviousBookmark &&
  1303         this.currentBookmark && this.currentPage) {
  1304       this.previousBookmark = this.currentBookmark;
  1305       this.previousPage = this.currentPage;
  1306       this.updatePreviousBookmark = false;
  1308   },
  1310   updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark,
  1311                                                                   pageNum) {
  1312     if (this.initialized) {
  1313       this.currentBookmark = bookmark.substring(1);
  1314       this.currentPage = pageNum | 0;
  1315       this._updatePreviousBookmark();
  1317   },
  1319   updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
  1320     if (this.initialized) {
  1321       this.nextHashParam = param;
  1323   },
  1325   push: function pdfHistoryPush(params, isInitialBookmark) {
  1326     if (!(this.initialized && this.historyUnlocked)) {
  1327       return;
  1329     if (params.dest && !params.hash) {
  1330       params.hash = (this.current.hash && this.current.dest &&
  1331                      this.current.dest === params.dest) ?
  1332         this.current.hash :
  1333         PDFView.getDestinationHash(params.dest).split('#')[1];
  1335     if (params.page) {
  1336       params.page |= 0;
  1338     if (isInitialBookmark) {
  1339       var target = window.history.state.target;
  1340       if (!target) {
  1341         // Invoked when the user specifies an initial bookmark,
  1342         // thus setting PDFView.initialBookmark, when the document is loaded.
  1343         this._pushToHistory(params, false);
  1344         this.previousHash = window.location.hash.substring(1);
  1346       this.updatePreviousBookmark = this.nextHashParam ? false : true;
  1347       if (target) {
  1348         // If the current document is reloaded,
  1349         // avoid creating duplicate entries in the history.
  1350         this._updatePreviousBookmark();
  1352       return;
  1354     if (this.nextHashParam) {
  1355       if (this.nextHashParam === params.hash) {
  1356         this.nextHashParam = null;
  1357         this.updatePreviousBookmark = true;
  1358         return;
  1359       } else {
  1360         this.nextHashParam = null;
  1364     if (params.hash) {
  1365       if (this.current.hash) {
  1366         if (this.current.hash !== params.hash) {
  1367           this._pushToHistory(params, true);
  1368         } else {
  1369           if (!this.current.page && params.page) {
  1370             this._pushToHistory(params, false, true);
  1372           this.updatePreviousBookmark = true;
  1374       } else {
  1375         this._pushToHistory(params, true);
  1377     } else if (this.current.page && params.page &&
  1378                this.current.page !== params.page) {
  1379       this._pushToHistory(params, true);
  1381   },
  1383   _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage,
  1384                                                             beforeUnload) {
  1385     if (!(this.currentBookmark && this.currentPage)) {
  1386       return null;
  1387     } else if (this.updatePreviousBookmark) {
  1388       this.updatePreviousBookmark = false;
  1390     if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
  1391       // Prevent the history from getting stuck in the current state,
  1392       // effectively preventing the user from going back/forward in the history.
  1393       //
  1394       // This happens if the current position in the document didn't change when
  1395       // the history was previously updated. The reasons for this are either:
  1396       // 1. The current zoom value is such that the document does not need to,
  1397       //    or cannot, be scrolled to display the destination.
  1398       // 2. The previous destination is broken, and doesn't actally point to a
  1399       //    position within the document.
  1400       //    (This is either due to a bad PDF generator, or the user making a
  1401       //     mistake when entering a destination in the hash parameters.)
  1402       return null;
  1404     if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
  1405       if (this.previousBookmark === this.currentBookmark) {
  1406         return null;
  1408     } else if (this.current.page || onlyCheckPage) {
  1409       if (this.previousPage === this.currentPage) {
  1410         return null;
  1412     } else {
  1413       return null;
  1415     var params = { hash: this.currentBookmark, page: this.currentPage };
  1416     if (PresentationMode.active) {
  1417       params.hash = null;
  1419     return params;
  1420   },
  1422   _stateObj: function pdfHistory_stateObj(params) {
  1423     return { fingerprint: this.fingerprint, uid: this.uid, target: params };
  1424   },
  1426   _pushToHistory: function pdfHistory_pushToHistory(params,
  1427                                                     addPrevious, overwrite) {
  1428     if (!this.initialized) {
  1429       return;
  1431     if (!params.hash && params.page) {
  1432       params.hash = ('page=' + params.page);
  1434     if (addPrevious && !overwrite) {
  1435       var previousParams = this._getPreviousParams();
  1436       if (previousParams) {
  1437         var replacePrevious = (!this.current.dest &&
  1438                                this.current.hash !== this.previousHash);
  1439         this._pushToHistory(previousParams, false, replacePrevious);
  1442     this._pushOrReplaceState(this._stateObj(params),
  1443                              (overwrite || this.uid === 0));
  1444     this.currentUid = this.uid++;
  1445     this.current = params;
  1446     this.updatePreviousBookmark = true;
  1447   },
  1449   _goTo: function pdfHistory_goTo(state) {
  1450     if (!(this.initialized && this.historyUnlocked &&
  1451           this._isStateObjectDefined(state))) {
  1452       return;
  1454     if (!this.reInitialized && state.uid < this.currentUid) {
  1455       var previousParams = this._getPreviousParams(true);
  1456       if (previousParams) {
  1457         this._pushToHistory(this.current, false);
  1458         this._pushToHistory(previousParams, false);
  1459         this.currentUid = state.uid;
  1460         window.history.back();
  1461         return;
  1464     this.historyUnlocked = false;
  1466     if (state.target.dest) {
  1467       PDFView.navigateTo(state.target.dest);
  1468     } else {
  1469       PDFView.setHash(state.target.hash);
  1471     this.currentUid = state.uid;
  1472     if (state.uid > this.uid) {
  1473       this.uid = state.uid;
  1475     this.current = state.target;
  1476     this.updatePreviousBookmark = true;
  1478     var currentHash = window.location.hash.substring(1);
  1479     if (this.previousHash !== currentHash) {
  1480       this.allowHashChange = false;
  1482     this.previousHash = currentHash;
  1484     this.historyUnlocked = true;
  1485   },
  1487   back: function pdfHistoryBack() {
  1488     this.go(-1);
  1489   },
  1491   forward: function pdfHistoryForward() {
  1492     this.go(1);
  1493   },
  1495   go: function pdfHistoryGo(direction) {
  1496     if (this.initialized && this.historyUnlocked) {
  1497       var state = window.history.state;
  1498       if (direction === -1 && state && state.uid > 0) {
  1499         window.history.back();
  1500       } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
  1501         window.history.forward();
  1505 };
  1508 var SecondaryToolbar = {
  1509   opened: false,
  1510   previousContainerHeight: null,
  1511   newContainerHeight: null,
  1513   initialize: function secondaryToolbarInitialize(options) {
  1514     this.toolbar = options.toolbar;
  1515     this.presentationMode = options.presentationMode;
  1516     this.documentProperties = options.documentProperties;
  1517     this.buttonContainer = this.toolbar.firstElementChild;
  1519     // Define the toolbar buttons.
  1520     this.toggleButton = options.toggleButton;
  1521     this.presentationModeButton = options.presentationModeButton;
  1522     this.openFile = options.openFile;
  1523     this.print = options.print;
  1524     this.download = options.download;
  1525     this.viewBookmark = options.viewBookmark;
  1526     this.firstPage = options.firstPage;
  1527     this.lastPage = options.lastPage;
  1528     this.pageRotateCw = options.pageRotateCw;
  1529     this.pageRotateCcw = options.pageRotateCcw;
  1530     this.documentPropertiesButton = options.documentPropertiesButton;
  1532     // Attach the event listeners.
  1533     var elements = [
  1534       // Button to toggle the visibility of the secondary toolbar:
  1535       { element: this.toggleButton, handler: this.toggle },
  1536       // All items within the secondary toolbar
  1537       // (except for toggleHandTool, hand_tool.js is responsible for it):
  1538       { element: this.presentationModeButton,
  1539         handler: this.presentationModeClick },
  1540       { element: this.openFile, handler: this.openFileClick },
  1541       { element: this.print, handler: this.printClick },
  1542       { element: this.download, handler: this.downloadClick },
  1543       { element: this.viewBookmark, handler: this.viewBookmarkClick },
  1544       { element: this.firstPage, handler: this.firstPageClick },
  1545       { element: this.lastPage, handler: this.lastPageClick },
  1546       { element: this.pageRotateCw, handler: this.pageRotateCwClick },
  1547       { element: this.pageRotateCcw, handler: this.pageRotateCcwClick },
  1548       { element: this.documentPropertiesButton,
  1549         handler: this.documentPropertiesClick }
  1550     ];
  1552     for (var item in elements) {
  1553       var element = elements[item].element;
  1554       if (element) {
  1555         element.addEventListener('click', elements[item].handler.bind(this));
  1558   },
  1560   // Event handling functions.
  1561   presentationModeClick: function secondaryToolbarPresentationModeClick(evt) {
  1562     this.presentationMode.request();
  1563     this.close();
  1564   },
  1566   openFileClick: function secondaryToolbarOpenFileClick(evt) {
  1567     document.getElementById('fileInput').click();
  1568     this.close();
  1569   },
  1571   printClick: function secondaryToolbarPrintClick(evt) {
  1572     window.print();
  1573     this.close();
  1574   },
  1576   downloadClick: function secondaryToolbarDownloadClick(evt) {
  1577     PDFView.download();
  1578     this.close();
  1579   },
  1581   viewBookmarkClick: function secondaryToolbarViewBookmarkClick(evt) {
  1582     this.close();
  1583   },
  1585   firstPageClick: function secondaryToolbarFirstPageClick(evt) {
  1586     PDFView.page = 1;
  1587     this.close();
  1588   },
  1590   lastPageClick: function secondaryToolbarLastPageClick(evt) {
  1591     PDFView.page = PDFView.pdfDocument.numPages;
  1592     this.close();
  1593   },
  1595   pageRotateCwClick: function secondaryToolbarPageRotateCwClick(evt) {
  1596     PDFView.rotatePages(90);
  1597   },
  1599   pageRotateCcwClick: function secondaryToolbarPageRotateCcwClick(evt) {
  1600     PDFView.rotatePages(-90);
  1601   },
  1603   documentPropertiesClick: function secondaryToolbarDocumentPropsClick(evt) {
  1604     this.documentProperties.show();
  1605     this.close();
  1606   },
  1608   // Misc. functions for interacting with the toolbar.
  1609   setMaxHeight: function secondaryToolbarSetMaxHeight(container) {
  1610     if (!container || !this.buttonContainer) {
  1611       return;
  1613     this.newContainerHeight = container.clientHeight;
  1614     if (this.previousContainerHeight === this.newContainerHeight) {
  1615       return;
  1617     this.buttonContainer.setAttribute('style',
  1618       'max-height: ' + (this.newContainerHeight - SCROLLBAR_PADDING) + 'px;');
  1619     this.previousContainerHeight = this.newContainerHeight;
  1620   },
  1622   open: function secondaryToolbarOpen() {
  1623     if (this.opened) {
  1624       return;
  1626     this.opened = true;
  1627     this.toggleButton.classList.add('toggled');
  1628     this.toolbar.classList.remove('hidden');
  1629   },
  1631   close: function secondaryToolbarClose(target) {
  1632     if (!this.opened) {
  1633       return;
  1634     } else if (target && !this.toolbar.contains(target)) {
  1635       return;
  1637     this.opened = false;
  1638     this.toolbar.classList.add('hidden');
  1639     this.toggleButton.classList.remove('toggled');
  1640   },
  1642   toggle: function secondaryToolbarToggle() {
  1643     if (this.opened) {
  1644       this.close();
  1645     } else {
  1646       this.open();
  1649 };
  1652 var PasswordPrompt = {
  1653   visible: false,
  1654   updatePassword: null,
  1655   reason: null,
  1656   overlayContainer: null,
  1657   passwordField: null,
  1658   passwordText: null,
  1659   passwordSubmit: null,
  1660   passwordCancel: null,
  1662   initialize: function secondaryToolbarInitialize(options) {
  1663     this.overlayContainer = options.overlayContainer;
  1664     this.passwordField = options.passwordField;
  1665     this.passwordText = options.passwordText;
  1666     this.passwordSubmit = options.passwordSubmit;
  1667     this.passwordCancel = options.passwordCancel;
  1669     // Attach the event listeners.
  1670     this.passwordSubmit.addEventListener('click',
  1671       this.verifyPassword.bind(this));
  1673     this.passwordCancel.addEventListener('click', this.hide.bind(this));
  1675     this.passwordField.addEventListener('keydown',
  1676       function (e) {
  1677         if (e.keyCode === 13) { // Enter key
  1678           this.verifyPassword();
  1680       }.bind(this));
  1682     window.addEventListener('keydown',
  1683       function (e) {
  1684         if (e.keyCode === 27) { // Esc key
  1685           this.hide();
  1687       }.bind(this));
  1688   },
  1690   show: function passwordPromptShow() {
  1691     if (this.visible) {
  1692       return;
  1694     this.visible = true;
  1695     this.overlayContainer.classList.remove('hidden');
  1696     this.overlayContainer.firstElementChild.classList.remove('hidden');
  1697     this.passwordField.focus();
  1699     var promptString = mozL10n.get('password_label', null,
  1700       'Enter the password to open this PDF file.');
  1702     if (this.reason === PDFJS.PasswordResponses.INCORRECT_PASSWORD) {
  1703       promptString = mozL10n.get('password_invalid', null,
  1704         'Invalid password. Please try again.');
  1707     this.passwordText.textContent = promptString;
  1708   },
  1710   hide: function passwordPromptClose() {
  1711     if (!this.visible) {
  1712       return;
  1714     this.visible = false;
  1715     this.passwordField.value = '';
  1716     this.overlayContainer.classList.add('hidden');
  1717     this.overlayContainer.firstElementChild.classList.add('hidden');
  1718   },
  1720   verifyPassword: function passwordPromptVerifyPassword() {
  1721     var password = this.passwordField.value;
  1722     if (password && password.length > 0) {
  1723       this.hide();
  1724       return this.updatePassword(password);
  1727 };
  1730 var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms
  1731 var SELECTOR = 'presentationControls';
  1732 var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms
  1734 var PresentationMode = {
  1735   active: false,
  1736   args: null,
  1737   contextMenuOpen: false,
  1739   initialize: function presentationModeInitialize(options) {
  1740     this.container = options.container;
  1741     this.secondaryToolbar = options.secondaryToolbar;
  1743     this.viewer = this.container.firstElementChild;
  1745     this.firstPage = options.firstPage;
  1746     this.lastPage = options.lastPage;
  1747     this.pageRotateCw = options.pageRotateCw;
  1748     this.pageRotateCcw = options.pageRotateCcw;
  1750     this.firstPage.addEventListener('click', function() {
  1751       this.contextMenuOpen = false;
  1752       this.secondaryToolbar.firstPageClick();
  1753     }.bind(this));
  1754     this.lastPage.addEventListener('click', function() {
  1755       this.contextMenuOpen = false;
  1756       this.secondaryToolbar.lastPageClick();
  1757     }.bind(this));
  1759     this.pageRotateCw.addEventListener('click', function() {
  1760       this.contextMenuOpen = false;
  1761       this.secondaryToolbar.pageRotateCwClick();
  1762     }.bind(this));
  1763     this.pageRotateCcw.addEventListener('click', function() {
  1764       this.contextMenuOpen = false;
  1765       this.secondaryToolbar.pageRotateCcwClick();
  1766     }.bind(this));
  1767   },
  1769   get isFullscreen() {
  1770     return (document.fullscreenElement ||
  1771             document.mozFullScreen ||
  1772             document.webkitIsFullScreen ||
  1773             document.msFullscreenElement);
  1774   },
  1776   /**
  1777    * Initialize a timeout that is used to reset PDFView.currentPosition when the
  1778    * browser transitions to fullscreen mode. Since resize events are triggered
  1779    * multiple times during the switch to fullscreen mode, this is necessary in
  1780    * order to prevent the page from being scrolled partially, or completely,
  1781    * out of view when Presentation Mode is enabled.
  1782    * Note: This is only an issue at certain zoom levels, e.g. 'page-width'.
  1783    */
  1784   _setSwitchInProgress: function presentationMode_setSwitchInProgress() {
  1785     if (this.switchInProgress) {
  1786       clearTimeout(this.switchInProgress);
  1788     this.switchInProgress = setTimeout(function switchInProgressTimeout() {
  1789       delete this.switchInProgress;
  1790     }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS);
  1792     PDFView.currentPosition = null;
  1793   },
  1795   _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() {
  1796     if (this.switchInProgress) {
  1797       clearTimeout(this.switchInProgress);
  1798       delete this.switchInProgress;
  1800   },
  1802   request: function presentationModeRequest() {
  1803     if (!PDFView.supportsFullscreen || this.isFullscreen ||
  1804         !this.viewer.hasChildNodes()) {
  1805       return false;
  1807     this._setSwitchInProgress();
  1809     if (this.container.requestFullscreen) {
  1810       this.container.requestFullscreen();
  1811     } else if (this.container.mozRequestFullScreen) {
  1812       this.container.mozRequestFullScreen();
  1813     } else if (this.container.webkitRequestFullScreen) {
  1814       this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  1815     } else if (this.container.msRequestFullscreen) {
  1816       this.container.msRequestFullscreen();
  1817     } else {
  1818       return false;
  1821     this.args = {
  1822       page: PDFView.page,
  1823       previousScale: PDFView.currentScaleValue
  1824     };
  1826     return true;
  1827   },
  1829   enter: function presentationModeEnter() {
  1830     this.active = true;
  1831     this._resetSwitchInProgress();
  1833     // Ensure that the correct page is scrolled into view when entering
  1834     // Presentation Mode, by waiting until fullscreen mode in enabled.
  1835     // Note: This is only necessary in non-Mozilla browsers.
  1836     setTimeout(function enterPresentationModeTimeout() {
  1837       PDFView.page = this.args.page;
  1838       PDFView.setScale('page-fit', true);
  1839     }.bind(this), 0);
  1841     window.addEventListener('mousemove', this.mouseMove, false);
  1842     window.addEventListener('mousedown', this.mouseDown, false);
  1843     window.addEventListener('contextmenu', this.contextMenu, false);
  1845     this.showControls();
  1846     HandTool.enterPresentationMode();
  1847     this.contextMenuOpen = false;
  1848     this.container.setAttribute('contextmenu', 'viewerContextMenu');
  1849   },
  1851   exit: function presentationModeExit() {
  1852     var page = PDFView.page;
  1854     // Ensure that the correct page is scrolled into view when exiting
  1855     // Presentation Mode, by waiting until fullscreen mode is disabled.
  1856     // Note: This is only necessary in non-Mozilla browsers.
  1857     setTimeout(function exitPresentationModeTimeout() {
  1858       this.active = false;
  1859       PDFView.setScale(this.args.previousScale);
  1860       PDFView.page = page;
  1861       this.args = null;
  1862     }.bind(this), 0);
  1864     window.removeEventListener('mousemove', this.mouseMove, false);
  1865     window.removeEventListener('mousedown', this.mouseDown, false);
  1866     window.removeEventListener('contextmenu', this.contextMenu, false);
  1868     this.hideControls();
  1869     PDFView.clearMouseScrollState();
  1870     HandTool.exitPresentationMode();
  1871     this.container.removeAttribute('contextmenu');
  1872     this.contextMenuOpen = false;
  1874     // Ensure that the thumbnail of the current page is visible
  1875     // when exiting presentation mode.
  1876     scrollIntoView(document.getElementById('thumbnailContainer' + page));
  1877   },
  1879   showControls: function presentationModeShowControls() {
  1880     if (this.controlsTimeout) {
  1881       clearTimeout(this.controlsTimeout);
  1882     } else {
  1883       this.container.classList.add(SELECTOR);
  1885     this.controlsTimeout = setTimeout(function hideControlsTimeout() {
  1886       this.container.classList.remove(SELECTOR);
  1887       delete this.controlsTimeout;
  1888     }.bind(this), DELAY_BEFORE_HIDING_CONTROLS);
  1889   },
  1891   hideControls: function presentationModeHideControls() {
  1892     if (!this.controlsTimeout) {
  1893       return;
  1895     this.container.classList.remove(SELECTOR);
  1896     clearTimeout(this.controlsTimeout);
  1897     delete this.controlsTimeout;
  1898   },
  1900   mouseMove: function presentationModeMouseMove(evt) {
  1901     PresentationMode.showControls();
  1902   },
  1904   mouseDown: function presentationModeMouseDown(evt) {
  1905     var self = PresentationMode;
  1906     if (self.contextMenuOpen) {
  1907       self.contextMenuOpen = false;
  1908       evt.preventDefault();
  1909       return;
  1912     if (evt.button === 0) {
  1913       // Enable clicking of links in presentation mode. Please note:
  1914       // Only links pointing to destinations in the current PDF document work.
  1915       var isInternalLink = (evt.target.href &&
  1916                             evt.target.classList.contains('internalLink'));
  1917       if (!isInternalLink) {
  1918         // Unless an internal link was clicked, advance one page.
  1919         evt.preventDefault();
  1920         PDFView.page += (evt.shiftKey ? -1 : 1);
  1923   },
  1925   contextMenu: function presentationModeContextMenu(evt) {
  1926     PresentationMode.contextMenuOpen = true;
  1928 };
  1930 (function presentationModeClosure() {
  1931   function presentationModeChange(e) {
  1932     if (PresentationMode.isFullscreen) {
  1933       PresentationMode.enter();
  1934     } else {
  1935       PresentationMode.exit();
  1939   window.addEventListener('fullscreenchange', presentationModeChange, false);
  1940   window.addEventListener('mozfullscreenchange', presentationModeChange, false);
  1941   window.addEventListener('webkitfullscreenchange', presentationModeChange,
  1942                           false);
  1943   window.addEventListener('MSFullscreenChange', presentationModeChange, false);
  1944 })();
  1947 /* Copyright 2013 Rob Wu <gwnRob@gmail.com>
  1948  * https://github.com/Rob--W/grab-to-pan.js
  1950  * Licensed under the Apache License, Version 2.0 (the "License");
  1951  * you may not use this file except in compliance with the License.
  1952  * You may obtain a copy of the License at
  1954  *     http://www.apache.org/licenses/LICENSE-2.0
  1956  * Unless required by applicable law or agreed to in writing, software
  1957  * distributed under the License is distributed on an "AS IS" BASIS,
  1958  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  1959  * See the License for the specific language governing permissions and
  1960  * limitations under the License.
  1961  */
  1963 'use strict';
  1965 var GrabToPan = (function GrabToPanClosure() {
  1966   /**
  1967    * Construct a GrabToPan instance for a given HTML element.
  1968    * @param options.element {Element}
  1969    * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)`
  1970    * @param options.onActiveChanged {function(boolean)} optional. Called
  1971    *  when grab-to-pan is (de)activated. The first argument is a boolean that
  1972    *  shows whether grab-to-pan is activated.
  1973    */
  1974   function GrabToPan(options) {
  1975     this.element = options.element;
  1976     this.document = options.element.ownerDocument;
  1977     if (typeof options.ignoreTarget === 'function') {
  1978       this.ignoreTarget = options.ignoreTarget;
  1980     this.onActiveChanged = options.onActiveChanged;
  1982     // Bind the contexts to ensure that `this` always points to
  1983     // the GrabToPan instance.
  1984     this.activate = this.activate.bind(this);
  1985     this.deactivate = this.deactivate.bind(this);
  1986     this.toggle = this.toggle.bind(this);
  1987     this._onmousedown = this._onmousedown.bind(this);
  1988     this._onmousemove = this._onmousemove.bind(this);
  1989     this._endPan = this._endPan.bind(this);
  1991     // This overlay will be inserted in the document when the mouse moves during
  1992     // a grab operation, to ensure that the cursor has the desired appearance.
  1993     var overlay = this.overlay = document.createElement('div');
  1994     overlay.className = 'grab-to-pan-grabbing';
  1996   GrabToPan.prototype = {
  1997     /**
  1998      * Class name of element which can be grabbed
  1999      */
  2000     CSS_CLASS_GRAB: 'grab-to-pan-grab',
  2002     /**
  2003      * Bind a mousedown event to the element to enable grab-detection.
  2004      */
  2005     activate: function GrabToPan_activate() {
  2006       if (!this.active) {
  2007         this.active = true;
  2008         this.element.addEventListener('mousedown', this._onmousedown, true);
  2009         this.element.classList.add(this.CSS_CLASS_GRAB);
  2010         if (this.onActiveChanged) {
  2011           this.onActiveChanged(true);
  2014     },
  2016     /**
  2017      * Removes all events. Any pending pan session is immediately stopped.
  2018      */
  2019     deactivate: function GrabToPan_deactivate() {
  2020       if (this.active) {
  2021         this.active = false;
  2022         this.element.removeEventListener('mousedown', this._onmousedown, true);
  2023         this._endPan();
  2024         this.element.classList.remove(this.CSS_CLASS_GRAB);
  2025         if (this.onActiveChanged) {
  2026           this.onActiveChanged(false);
  2029     },
  2031     toggle: function GrabToPan_toggle() {
  2032       if (this.active) {
  2033         this.deactivate();
  2034       } else {
  2035         this.activate();
  2037     },
  2039     /**
  2040      * Whether to not pan if the target element is clicked.
  2041      * Override this method to change the default behaviour.
  2043      * @param node {Element} The target of the event
  2044      * @return {boolean} Whether to not react to the click event.
  2045      */
  2046     ignoreTarget: function GrabToPan_ignoreTarget(node) {
  2047       // Use matchesSelector to check whether the clicked element
  2048       // is (a child of) an input element / link
  2049       return node[matchesSelector](
  2050         'a[href], a[href] *, input, textarea, button, button *, select, option'
  2051       );
  2052     },
  2054     /**
  2055      * @private
  2056      */
  2057     _onmousedown: function GrabToPan__onmousedown(event) {
  2058       if (event.button !== 0 || this.ignoreTarget(event.target)) {
  2059         return;
  2061       if (event.originalTarget) {
  2062         try {
  2063           /* jshint expr:true */
  2064           event.originalTarget.tagName;
  2065         } catch (e) {
  2066           // Mozilla-specific: element is a scrollbar (XUL element)
  2067           return;
  2071       this.scrollLeftStart = this.element.scrollLeft;
  2072       this.scrollTopStart = this.element.scrollTop;
  2073       this.clientXStart = event.clientX;
  2074       this.clientYStart = event.clientY;
  2075       this.document.addEventListener('mousemove', this._onmousemove, true);
  2076       this.document.addEventListener('mouseup', this._endPan, true);
  2077       // When a scroll event occurs before a mousemove, assume that the user
  2078       // dragged a scrollbar (necessary for Opera Presto, Safari and IE)
  2079       // (not needed for Chrome/Firefox)
  2080       this.element.addEventListener('scroll', this._endPan, true);
  2081       event.preventDefault();
  2082       event.stopPropagation();
  2083       this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING);
  2084     },
  2086     /**
  2087      * @private
  2088      */
  2089     _onmousemove: function GrabToPan__onmousemove(event) {
  2090       this.element.removeEventListener('scroll', this._endPan, true);
  2091       if (isLeftMouseReleased(event)) {
  2092         this._endPan();
  2093         return;
  2095       var xDiff = event.clientX - this.clientXStart;
  2096       var yDiff = event.clientY - this.clientYStart;
  2097       this.element.scrollTop = this.scrollTopStart - yDiff;
  2098       this.element.scrollLeft = this.scrollLeftStart - xDiff;
  2099       if (!this.overlay.parentNode) {
  2100         document.body.appendChild(this.overlay);
  2102     },
  2104     /**
  2105      * @private
  2106      */
  2107     _endPan: function GrabToPan__endPan() {
  2108       this.element.removeEventListener('scroll', this._endPan, true);
  2109       this.document.removeEventListener('mousemove', this._onmousemove, true);
  2110       this.document.removeEventListener('mouseup', this._endPan, true);
  2111       if (this.overlay.parentNode) {
  2112         this.overlay.parentNode.removeChild(this.overlay);
  2115   };
  2117   // Get the correct (vendor-prefixed) name of the matches method.
  2118   var matchesSelector;
  2119   ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) {
  2120     var name = prefix + 'atches';
  2121     if (name in document.documentElement) {
  2122       matchesSelector = name;
  2124     name += 'Selector';
  2125     if (name in document.documentElement) {
  2126       matchesSelector = name;
  2128     return matchesSelector; // If found, then truthy, and [].some() ends.
  2129   });
  2131   // Browser sniffing because it's impossible to feature-detect
  2132   // whether event.which for onmousemove is reliable
  2133   var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9;
  2134   var chrome = window.chrome;
  2135   var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app);
  2136   //                                       ^ Chrome 15+       ^ Opera 15+
  2137   var isSafari6plus = /Apple/.test(navigator.vendor) &&
  2138                       /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent);
  2140   /**
  2141    * Whether the left mouse is not pressed.
  2142    * @param event {MouseEvent}
  2143    * @return {boolean} True if the left mouse button is not pressed.
  2144    *                   False if unsure or if the left mouse button is pressed.
  2145    */
  2146   function isLeftMouseReleased(event) {
  2147     if ('buttons' in event && isNotIEorIsIE10plus) {
  2148       // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons
  2149       // Firefox 15+
  2150       // Internet Explorer 10+
  2151       return !(event.buttons | 1);
  2153     if (isChrome15OrOpera15plus || isSafari6plus) {
  2154       // Chrome 14+
  2155       // Opera 15+
  2156       // Safari 6.0+
  2157       return event.which === 0;
  2161   return GrabToPan;
  2162 })();
  2164 var HandTool = {
  2165   initialize: function handToolInitialize(options) {
  2166     var toggleHandTool = options.toggleHandTool;
  2167     this.handTool = new GrabToPan({
  2168       element: options.container,
  2169       onActiveChanged: function(isActive) {
  2170         if (!toggleHandTool) {
  2171           return;
  2173         if (isActive) {
  2174           toggleHandTool.title =
  2175             mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool');
  2176           toggleHandTool.firstElementChild.textContent =
  2177             mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool');
  2178         } else {
  2179           toggleHandTool.title =
  2180             mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool');
  2181           toggleHandTool.firstElementChild.textContent =
  2182             mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool');
  2185     });
  2186     if (toggleHandTool) {
  2187       toggleHandTool.addEventListener('click', this.toggle.bind(this), false);
  2189       window.addEventListener('localized', function (evt) {
  2190         Preferences.get('enableHandToolOnLoad').then(function (prefValue) {
  2191           if (prefValue) {
  2192             this.handTool.activate();
  2194         }.bind(this));
  2195       }.bind(this));
  2197   },
  2199   toggle: function handToolToggle() {
  2200     this.handTool.toggle();
  2201     SecondaryToolbar.close();
  2202   },
  2204   enterPresentationMode: function handToolEnterPresentationMode() {
  2205     if (this.handTool.active) {
  2206       this.wasActive = true;
  2207       this.handTool.deactivate();
  2209   },
  2211   exitPresentationMode: function handToolExitPresentationMode() {
  2212     if (this.wasActive) {
  2213       this.wasActive = null;
  2214       this.handTool.activate();
  2217 };
  2220 var DocumentProperties = {
  2221   overlayContainer: null,
  2222   fileName: '',
  2223   fileSize: '',
  2224   visible: false,
  2226   // Document property fields (in the viewer).
  2227   fileNameField: null,
  2228   fileSizeField: null,
  2229   titleField: null,
  2230   authorField: null,
  2231   subjectField: null,
  2232   keywordsField: null,
  2233   creationDateField: null,
  2234   modificationDateField: null,
  2235   creatorField: null,
  2236   producerField: null,
  2237   versionField: null,
  2238   pageCountField: null,
  2240   initialize: function documentPropertiesInitialize(options) {
  2241     this.overlayContainer = options.overlayContainer;
  2243     // Set the document property fields.
  2244     this.fileNameField = options.fileNameField;
  2245     this.fileSizeField = options.fileSizeField;
  2246     this.titleField = options.titleField;
  2247     this.authorField = options.authorField;
  2248     this.subjectField = options.subjectField;
  2249     this.keywordsField = options.keywordsField;
  2250     this.creationDateField = options.creationDateField;
  2251     this.modificationDateField = options.modificationDateField;
  2252     this.creatorField = options.creatorField;
  2253     this.producerField = options.producerField;
  2254     this.versionField = options.versionField;
  2255     this.pageCountField = options.pageCountField;
  2257     // Bind the event listener for the Close button.
  2258     if (options.closeButton) {
  2259       options.closeButton.addEventListener('click', this.hide.bind(this));
  2262     this.dataAvailablePromise = new Promise(function (resolve) {
  2263       this.resolveDataAvailable = resolve;
  2264     }.bind(this));
  2266     // Bind the event listener for the Esc key (to close the dialog).
  2267     window.addEventListener('keydown',
  2268       function (e) {
  2269         if (e.keyCode === 27) { // Esc key
  2270           this.hide();
  2272       }.bind(this));
  2273   },
  2275   getProperties: function documentPropertiesGetProperties() {
  2276     if (!this.visible) {
  2277       // If the dialog was closed before dataAvailablePromise was resolved,
  2278       // don't bother updating the properties.
  2279       return;
  2281     // Get the file name.
  2282     this.fileName = getPDFFileNameFromURL(PDFView.url);
  2284     // Get the file size.
  2285     PDFView.pdfDocument.getDownloadInfo().then(function(data) {
  2286       this.setFileSize(data.length);
  2287       this.updateUI(this.fileSizeField, this.fileSize);
  2288     }.bind(this));
  2290     // Get the other document properties.
  2291     PDFView.pdfDocument.getMetadata().then(function(data) {
  2292       var fields = [
  2293         { field: this.fileNameField, content: this.fileName },
  2294         // The fileSize field is updated once getDownloadInfo is resolved.
  2295         { field: this.titleField, content: data.info.Title },
  2296         { field: this.authorField, content: data.info.Author },
  2297         { field: this.subjectField, content: data.info.Subject },
  2298         { field: this.keywordsField, content: data.info.Keywords },
  2299         { field: this.creationDateField,
  2300           content: this.parseDate(data.info.CreationDate) },
  2301         { field: this.modificationDateField,
  2302           content: this.parseDate(data.info.ModDate) },
  2303         { field: this.creatorField, content: data.info.Creator },
  2304         { field: this.producerField, content: data.info.Producer },
  2305         { field: this.versionField, content: data.info.PDFFormatVersion },
  2306         { field: this.pageCountField, content: PDFView.pdfDocument.numPages }
  2307       ];
  2309       // Show the properties in the dialog.
  2310       for (var item in fields) {
  2311         var element = fields[item];
  2312         this.updateUI(element.field, element.content);
  2314     }.bind(this));
  2315   },
  2317   updateUI: function documentPropertiesUpdateUI(field, content) {
  2318     if (field && content !== undefined && content !== '') {
  2319       field.textContent = content;
  2321   },
  2323   setFileSize: function documentPropertiesSetFileSize(fileSize) {
  2324     var kb = fileSize / 1024;
  2325     if (kb < 1024) {
  2326       this.fileSize = mozL10n.get('document_properties_kb', {
  2327         size_kb: (+kb.toPrecision(3)).toLocaleString(),
  2328         size_b: fileSize.toLocaleString()
  2329       }, '{{size_kb}} KB ({{size_b}} bytes)');
  2330     } else {
  2331       this.fileSize = mozL10n.get('document_properties_mb', {
  2332         size_mb: (+(kb / 1024).toPrecision(3)).toLocaleString(),
  2333         size_b: fileSize.toLocaleString()
  2334       }, '{{size_mb}} MB ({{size_b}} bytes)');
  2336   },
  2338   show: function documentPropertiesShow() {
  2339     if (this.visible) {
  2340       return;
  2342     this.visible = true;
  2343     this.overlayContainer.classList.remove('hidden');
  2344     this.overlayContainer.lastElementChild.classList.remove('hidden');
  2346     this.dataAvailablePromise.then(function () {
  2347       this.getProperties();
  2348     }.bind(this));
  2349   },
  2351   hide: function documentPropertiesClose() {
  2352     if (!this.visible) {
  2353       return;
  2355     this.visible = false;
  2356     this.overlayContainer.classList.add('hidden');
  2357     this.overlayContainer.lastElementChild.classList.add('hidden');
  2358   },
  2360   parseDate: function documentPropertiesParseDate(inputDate) {
  2361     // This is implemented according to the PDF specification (see
  2362     // http://www.gnupdf.org/Date for an overview), but note that 
  2363     // Adobe Reader doesn't handle changing the date to universal time
  2364     // and doesn't use the user's time zone (they're effectively ignoring
  2365     // the HH' and mm' parts of the date string).
  2366     var dateToParse = inputDate;
  2367     if (dateToParse === undefined) {
  2368       return '';
  2371     // Remove the D: prefix if it is available.
  2372     if (dateToParse.substring(0,2) === 'D:') {
  2373       dateToParse = dateToParse.substring(2);
  2376     // Get all elements from the PDF date string.
  2377     // JavaScript's Date object expects the month to be between
  2378     // 0 and 11 instead of 1 and 12, so we're correcting for this.
  2379     var year = parseInt(dateToParse.substring(0,4), 10);
  2380     var month = parseInt(dateToParse.substring(4,6), 10) - 1;
  2381     var day = parseInt(dateToParse.substring(6,8), 10);
  2382     var hours = parseInt(dateToParse.substring(8,10), 10);
  2383     var minutes = parseInt(dateToParse.substring(10,12), 10);
  2384     var seconds = parseInt(dateToParse.substring(12,14), 10);
  2385     var utRel = dateToParse.substring(14,15);
  2386     var offsetHours = parseInt(dateToParse.substring(15,17), 10);
  2387     var offsetMinutes = parseInt(dateToParse.substring(18,20), 10);
  2389     // As per spec, utRel = 'Z' means equal to universal time.
  2390     // The other cases ('-' and '+') have to be handled here.
  2391     if (utRel == '-') {
  2392       hours += offsetHours;
  2393       minutes += offsetMinutes;
  2394     } else if (utRel == '+') {
  2395       hours -= offsetHours;
  2396       minutes += offsetMinutes;
  2399     // Return the new date format from the user's locale.
  2400     var date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
  2401     var dateString = date.toLocaleDateString();
  2402     var timeString = date.toLocaleTimeString();
  2403     return mozL10n.get('document_properties_date_string',
  2404                        {date: dateString, time: timeString},
  2405                        '{{date}}, {{time}}');
  2407 };
  2410 var PDFView = {
  2411   pages: [],
  2412   thumbnails: [],
  2413   currentScale: UNKNOWN_SCALE,
  2414   currentScaleValue: null,
  2415   initialBookmark: document.location.hash.substring(1),
  2416   container: null,
  2417   thumbnailContainer: null,
  2418   initialized: false,
  2419   fellback: false,
  2420   pdfDocument: null,
  2421   sidebarOpen: false,
  2422   pageViewScroll: null,
  2423   thumbnailViewScroll: null,
  2424   pageRotation: 0,
  2425   mouseScrollTimeStamp: 0,
  2426   mouseScrollDelta: 0,
  2427   lastScroll: 0,
  2428   previousPageNumber: 1,
  2429   isViewerEmbedded: (window.parent !== window),
  2430   idleTimeout: null,
  2431   currentPosition: null,
  2433   // called once when the document is loaded
  2434   initialize: function pdfViewInitialize() {
  2435     var self = this;
  2436     var container = this.container = document.getElementById('viewerContainer');
  2437     this.pageViewScroll = {};
  2438     this.watchScroll(container, this.pageViewScroll, updateViewarea);
  2440     var thumbnailContainer = this.thumbnailContainer =
  2441                              document.getElementById('thumbnailView');
  2442     this.thumbnailViewScroll = {};
  2443     this.watchScroll(thumbnailContainer, this.thumbnailViewScroll,
  2444                      this.renderHighestPriority.bind(this));
  2446     Preferences.initialize();
  2448     PDFFindBar.initialize({
  2449       bar: document.getElementById('findbar'),
  2450       toggleButton: document.getElementById('viewFind'),
  2451       findField: document.getElementById('findInput'),
  2452       highlightAllCheckbox: document.getElementById('findHighlightAll'),
  2453       caseSensitiveCheckbox: document.getElementById('findMatchCase'),
  2454       findMsg: document.getElementById('findMsg'),
  2455       findStatusIcon: document.getElementById('findStatusIcon'),
  2456       findPreviousButton: document.getElementById('findPrevious'),
  2457       findNextButton: document.getElementById('findNext')
  2458     });
  2460     PDFFindController.initialize({
  2461       pdfPageSource: this,
  2462       integratedFind: this.supportsIntegratedFind
  2463     });
  2465     HandTool.initialize({
  2466       container: container,
  2467       toggleHandTool: document.getElementById('toggleHandTool')
  2468     });
  2470     SecondaryToolbar.initialize({
  2471       toolbar: document.getElementById('secondaryToolbar'),
  2472       presentationMode: PresentationMode,
  2473       toggleButton: document.getElementById('secondaryToolbarToggle'),
  2474       presentationModeButton:
  2475         document.getElementById('secondaryPresentationMode'),
  2476       openFile: document.getElementById('secondaryOpenFile'),
  2477       print: document.getElementById('secondaryPrint'),
  2478       download: document.getElementById('secondaryDownload'),
  2479       viewBookmark: document.getElementById('secondaryViewBookmark'),
  2480       firstPage: document.getElementById('firstPage'),
  2481       lastPage: document.getElementById('lastPage'),
  2482       pageRotateCw: document.getElementById('pageRotateCw'),
  2483       pageRotateCcw: document.getElementById('pageRotateCcw'),
  2484       documentProperties: DocumentProperties,
  2485       documentPropertiesButton: document.getElementById('documentProperties')
  2486     });
  2488     PasswordPrompt.initialize({
  2489       overlayContainer: document.getElementById('overlayContainer'),
  2490       passwordField: document.getElementById('password'),
  2491       passwordText: document.getElementById('passwordText'),
  2492       passwordSubmit: document.getElementById('passwordSubmit'),
  2493       passwordCancel: document.getElementById('passwordCancel')
  2494     });
  2496     PresentationMode.initialize({
  2497       container: container,
  2498       secondaryToolbar: SecondaryToolbar,
  2499       firstPage: document.getElementById('contextFirstPage'),
  2500       lastPage: document.getElementById('contextLastPage'),
  2501       pageRotateCw: document.getElementById('contextPageRotateCw'),
  2502       pageRotateCcw: document.getElementById('contextPageRotateCcw')
  2503     });
  2505     DocumentProperties.initialize({
  2506       overlayContainer: document.getElementById('overlayContainer'),
  2507       closeButton: document.getElementById('documentPropertiesClose'),
  2508       fileNameField: document.getElementById('fileNameField'),
  2509       fileSizeField: document.getElementById('fileSizeField'),
  2510       titleField: document.getElementById('titleField'),
  2511       authorField: document.getElementById('authorField'),
  2512       subjectField: document.getElementById('subjectField'),
  2513       keywordsField: document.getElementById('keywordsField'),
  2514       creationDateField: document.getElementById('creationDateField'),
  2515       modificationDateField: document.getElementById('modificationDateField'),
  2516       creatorField: document.getElementById('creatorField'),
  2517       producerField: document.getElementById('producerField'),
  2518       versionField: document.getElementById('versionField'),
  2519       pageCountField: document.getElementById('pageCountField')
  2520     });
  2522     container.addEventListener('scroll', function() {
  2523       self.lastScroll = Date.now();
  2524     }, false);
  2526     var initializedPromise = Promise.all([
  2527       Preferences.get('enableWebGL').then(function (value) {
  2528         PDFJS.disableWebGL = !value;
  2529       })
  2530       // TODO move more preferences and other async stuff here
  2531     ]);
  2533     return initializedPromise.then(function () {
  2534       PDFView.initialized = true;
  2535     });
  2536   },
  2538   getPage: function pdfViewGetPage(n) {
  2539     return this.pdfDocument.getPage(n);
  2540   },
  2542   // Helper function to keep track whether a div was scrolled up or down and
  2543   // then call a callback.
  2544   watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) {
  2545     state.down = true;
  2546     state.lastY = viewAreaElement.scrollTop;
  2547     viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) {
  2548       var currentY = viewAreaElement.scrollTop;
  2549       var lastY = state.lastY;
  2550       if (currentY > lastY) {
  2551         state.down = true;
  2552       } else if (currentY < lastY) {
  2553         state.down = false;
  2555       // else do nothing and use previous value
  2556       state.lastY = currentY;
  2557       callback();
  2558     }, true);
  2559   },
  2561   _setScaleUpdatePages: function pdfView_setScaleUpdatePages(
  2562       newScale, newValue, resetAutoSettings, noScroll) {
  2563     this.currentScaleValue = newValue;
  2564     if (newScale === this.currentScale) {
  2565       return;
  2567     for (var i = 0, ii = this.pages.length; i < ii; i++) {
  2568       this.pages[i].update(newScale);
  2570     this.currentScale = newScale;
  2572     if (!noScroll) {
  2573       var page = this.page, dest;
  2574       if (this.currentPosition && !IGNORE_CURRENT_POSITION_ON_ZOOM) {
  2575         page = this.currentPosition.page;
  2576         dest = [null, { name: 'XYZ' }, this.currentPosition.left,
  2577                 this.currentPosition.top, null];
  2579       this.pages[page - 1].scrollIntoView(dest);
  2581     var event = document.createEvent('UIEvents');
  2582     event.initUIEvent('scalechange', false, false, window, 0);
  2583     event.scale = newScale;
  2584     event.resetAutoSettings = resetAutoSettings;
  2585     window.dispatchEvent(event);
  2586   },
  2588   setScale: function pdfViewSetScale(value, resetAutoSettings, noScroll) {
  2589     if (value === 'custom') {
  2590       return;
  2592     var scale = parseFloat(value);
  2594     if (scale > 0) {
  2595       this._setScaleUpdatePages(scale, value, true, noScroll);
  2596     } else {
  2597       var currentPage = this.pages[this.page - 1];
  2598       if (!currentPage) {
  2599         return;
  2601       var hPadding = PresentationMode.active ? 0 : SCROLLBAR_PADDING;
  2602       var vPadding = PresentationMode.active ? 0 : VERTICAL_PADDING;
  2603       var pageWidthScale = (this.container.clientWidth - hPadding) /
  2604                             currentPage.width * currentPage.scale;
  2605       var pageHeightScale = (this.container.clientHeight - vPadding) /
  2606                              currentPage.height * currentPage.scale;
  2607       switch (value) {
  2608         case 'page-actual':
  2609           scale = 1;
  2610           break;
  2611         case 'page-width':
  2612           scale = pageWidthScale;
  2613           break;
  2614         case 'page-height':
  2615           scale = pageHeightScale;
  2616           break;
  2617         case 'page-fit':
  2618           scale = Math.min(pageWidthScale, pageHeightScale);
  2619           break;
  2620         case 'auto':
  2621           scale = Math.min(MAX_AUTO_SCALE, pageWidthScale);
  2622           break;
  2623         default:
  2624           console.error('pdfViewSetScale: \'' + value +
  2625                         '\' is an unknown zoom value.');
  2626           return;
  2628       this._setScaleUpdatePages(scale, value, resetAutoSettings, noScroll);
  2630       selectScaleOption(value);
  2632   },
  2634   zoomIn: function pdfViewZoomIn(ticks) {
  2635     var newScale = this.currentScale;
  2636     do {
  2637       newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
  2638       newScale = Math.ceil(newScale * 10) / 10;
  2639       newScale = Math.min(MAX_SCALE, newScale);
  2640     } while (--ticks && newScale < MAX_SCALE);
  2641     this.setScale(newScale, true);
  2642   },
  2644   zoomOut: function pdfViewZoomOut(ticks) {
  2645     var newScale = this.currentScale;
  2646     do {
  2647       newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
  2648       newScale = Math.floor(newScale * 10) / 10;
  2649       newScale = Math.max(MIN_SCALE, newScale);
  2650     } while (--ticks && newScale > MIN_SCALE);
  2651     this.setScale(newScale, true);
  2652   },
  2654   set page(val) {
  2655     var pages = this.pages;
  2656     var event = document.createEvent('UIEvents');
  2657     event.initUIEvent('pagechange', false, false, window, 0);
  2659     if (!(0 < val && val <= pages.length)) {
  2660       this.previousPageNumber = val;
  2661       event.pageNumber = this.page;
  2662       window.dispatchEvent(event);
  2663       return;
  2666     pages[val - 1].updateStats();
  2667     this.previousPageNumber = currentPageNumber;
  2668     currentPageNumber = val;
  2669     event.pageNumber = val;
  2670     window.dispatchEvent(event);
  2672     // checking if the this.page was called from the updateViewarea function:
  2673     // avoiding the creation of two "set page" method (internal and public)
  2674     if (updateViewarea.inProgress) {
  2675       return;
  2677     // Avoid scrolling the first page during loading
  2678     if (this.loading && val === 1) {
  2679       return;
  2681     pages[val - 1].scrollIntoView();
  2682   },
  2684   get page() {
  2685     return currentPageNumber;
  2686   },
  2688   get supportsPrinting() {
  2689     var canvas = document.createElement('canvas');
  2690     var value = 'mozPrintCallback' in canvas;
  2691     // shadow
  2692     Object.defineProperty(this, 'supportsPrinting', { value: value,
  2693                                                       enumerable: true,
  2694                                                       configurable: true,
  2695                                                       writable: false });
  2696     return value;
  2697   },
  2699   get supportsFullscreen() {
  2700     var doc = document.documentElement;
  2701     var support = doc.requestFullscreen || doc.mozRequestFullScreen ||
  2702                   doc.webkitRequestFullScreen || doc.msRequestFullscreen;
  2704     if (document.fullscreenEnabled === false ||
  2705         document.mozFullScreenEnabled === false ||
  2706         document.webkitFullscreenEnabled === false ||
  2707         document.msFullscreenEnabled === false) {
  2708       support = false;
  2711     Object.defineProperty(this, 'supportsFullscreen', { value: support,
  2712                                                         enumerable: true,
  2713                                                         configurable: true,
  2714                                                         writable: false });
  2715     return support;
  2716   },
  2718   get supportsIntegratedFind() {
  2719     var support = false;
  2720     support = FirefoxCom.requestSync('supportsIntegratedFind');
  2721     Object.defineProperty(this, 'supportsIntegratedFind', { value: support,
  2722                                                             enumerable: true,
  2723                                                             configurable: true,
  2724                                                             writable: false });
  2725     return support;
  2726   },
  2728   get supportsDocumentFonts() {
  2729     var support = true;
  2730     support = FirefoxCom.requestSync('supportsDocumentFonts');
  2731     Object.defineProperty(this, 'supportsDocumentFonts', { value: support,
  2732                                                            enumerable: true,
  2733                                                            configurable: true,
  2734                                                            writable: false });
  2735     return support;
  2736   },
  2738   get supportsDocumentColors() {
  2739     var support = true;
  2740     support = FirefoxCom.requestSync('supportsDocumentColors');
  2741     Object.defineProperty(this, 'supportsDocumentColors', { value: support,
  2742                                                             enumerable: true,
  2743                                                             configurable: true,
  2744                                                             writable: false });
  2745     return support;
  2746   },
  2748   get loadingBar() {
  2749     var bar = new ProgressBar('#loadingBar', {});
  2750     Object.defineProperty(this, 'loadingBar', { value: bar,
  2751                                                 enumerable: true,
  2752                                                 configurable: true,
  2753                                                 writable: false });
  2754     return bar;
  2755   },
  2757   get isHorizontalScrollbarEnabled() {
  2758     return (PresentationMode.active ? false :
  2759             (this.container.scrollWidth > this.container.clientWidth));
  2760   },
  2762   initPassiveLoading: function pdfViewInitPassiveLoading() {
  2763     var pdfDataRangeTransport = {
  2764       rangeListeners: [],
  2765       progressListeners: [],
  2767       addRangeListener: function PdfDataRangeTransport_addRangeListener(
  2768                                    listener) {
  2769         this.rangeListeners.push(listener);
  2770       },
  2772       addProgressListener: function PdfDataRangeTransport_addProgressListener(
  2773                                       listener) {
  2774         this.progressListeners.push(listener);
  2775       },
  2777       onDataRange: function PdfDataRangeTransport_onDataRange(begin, chunk) {
  2778         var listeners = this.rangeListeners;
  2779         for (var i = 0, n = listeners.length; i < n; ++i) {
  2780           listeners[i](begin, chunk);
  2782       },
  2784       onDataProgress: function PdfDataRangeTransport_onDataProgress(loaded) {
  2785         var listeners = this.progressListeners;
  2786         for (var i = 0, n = listeners.length; i < n; ++i) {
  2787           listeners[i](loaded);
  2789       },
  2791       requestDataRange: function PdfDataRangeTransport_requestDataRange(
  2792                                   begin, end) {
  2793         FirefoxCom.request('requestDataRange', { begin: begin, end: end });
  2795     };
  2797     window.addEventListener('message', function windowMessage(e) {
  2798       if (e.source !== null) {
  2799         // The message MUST originate from Chrome code.
  2800         console.warn('Rejected untrusted message from ' + e.origin);
  2801         return;
  2803       var args = e.data;
  2805       if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) {
  2806         return;
  2808       switch (args.pdfjsLoadAction) {
  2809         case 'supportsRangedLoading':
  2810           PDFView.open(args.pdfUrl, 0, undefined, pdfDataRangeTransport, {
  2811             length: args.length,
  2812             initialData: args.data
  2813           });
  2814           break;
  2815         case 'range':
  2816           pdfDataRangeTransport.onDataRange(args.begin, args.chunk);
  2817           break;
  2818         case 'rangeProgress':
  2819           pdfDataRangeTransport.onDataProgress(args.loaded);
  2820           break;
  2821         case 'progress':
  2822           PDFView.progress(args.loaded / args.total);
  2823           break;
  2824         case 'complete':
  2825           if (!args.data) {
  2826             PDFView.error(mozL10n.get('loading_error', null,
  2827                           'An error occurred while loading the PDF.'), e);
  2828             break;
  2830           PDFView.open(args.data, 0);
  2831           break;
  2833     });
  2834     FirefoxCom.requestSync('initPassiveLoading', null);
  2835   },
  2837   setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) {
  2838     this.url = url;
  2839     try {
  2840       this.setTitle(decodeURIComponent(getFileName(url)) || url);
  2841     } catch (e) {
  2842       // decodeURIComponent may throw URIError,
  2843       // fall back to using the unprocessed url in that case
  2844       this.setTitle(url);
  2846   },
  2848   setTitle: function pdfViewSetTitle(title) {
  2849     document.title = title;
  2850   },
  2852   close: function pdfViewClose() {
  2853     var errorWrapper = document.getElementById('errorWrapper');
  2854     errorWrapper.setAttribute('hidden', 'true');
  2856     if (!this.pdfDocument) {
  2857       return;
  2860     this.pdfDocument.destroy();
  2861     this.pdfDocument = null;
  2863     var thumbsView = document.getElementById('thumbnailView');
  2864     while (thumbsView.hasChildNodes()) {
  2865       thumbsView.removeChild(thumbsView.lastChild);
  2868     if ('_loadingInterval' in thumbsView) {
  2869       clearInterval(thumbsView._loadingInterval);
  2872     var container = document.getElementById('viewer');
  2873     while (container.hasChildNodes()) {
  2874       container.removeChild(container.lastChild);
  2877     if (typeof PDFBug !== 'undefined') {
  2878       PDFBug.cleanup();
  2880   },
  2882   // TODO(mack): This function signature should really be pdfViewOpen(url, args)
  2883   open: function pdfViewOpen(url, scale, password,
  2884                              pdfDataRangeTransport, args) {
  2885     if (this.pdfDocument) {
  2886       // Reload the preferences if a document was previously opened.
  2887       Preferences.reload();
  2889     this.close();
  2891     var parameters = {password: password};
  2892     if (typeof url === 'string') { // URL
  2893       this.setTitleUsingUrl(url);
  2894       parameters.url = url;
  2895     } else if (url && 'byteLength' in url) { // ArrayBuffer
  2896       parameters.data = url;
  2898     if (args) {
  2899       for (var prop in args) {
  2900         parameters[prop] = args[prop];
  2904     var self = this;
  2905     self.loading = true;
  2906     self.downloadComplete = false;
  2908     var passwordNeeded = function passwordNeeded(updatePassword, reason) {
  2909       PasswordPrompt.updatePassword = updatePassword;
  2910       PasswordPrompt.reason = reason;
  2911       PasswordPrompt.show();
  2912     };
  2914     function getDocumentProgress(progressData) {
  2915       self.progress(progressData.loaded / progressData.total);
  2918     PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded,
  2919                       getDocumentProgress).then(
  2920       function getDocumentCallback(pdfDocument) {
  2921         self.load(pdfDocument, scale);
  2922         self.loading = false;
  2923       },
  2924       function getDocumentError(message, exception) {
  2925         var loadingErrorMessage = mozL10n.get('loading_error', null,
  2926           'An error occurred while loading the PDF.');
  2928         if (exception && exception.name === 'InvalidPDFException') {
  2929           // change error message also for other builds
  2930           loadingErrorMessage = mozL10n.get('invalid_file_error', null,
  2931                                         'Invalid or corrupted PDF file.');
  2934         if (exception && exception.name === 'MissingPDFException') {
  2935           // special message for missing PDF's
  2936           loadingErrorMessage = mozL10n.get('missing_file_error', null,
  2937                                         'Missing PDF file.');
  2941         var moreInfo = {
  2942           message: message
  2943         };
  2944         self.error(loadingErrorMessage, moreInfo);
  2945         self.loading = false;
  2947     );
  2948   },
  2950   download: function pdfViewDownload() {
  2951     function downloadByUrl() {
  2952       downloadManager.downloadUrl(url, filename);
  2955     var url = this.url.split('#')[0];
  2956     var filename = getPDFFileNameFromURL(url);
  2957     var downloadManager = new DownloadManager();
  2958     downloadManager.onerror = function (err) {
  2959       // This error won't really be helpful because it's likely the
  2960       // fallback won't work either (or is already open).
  2961       PDFView.error('PDF failed to download.');
  2962     };
  2964     if (!this.pdfDocument) { // the PDF is not ready yet
  2965       downloadByUrl();
  2966       return;
  2969     if (!this.downloadComplete) { // the PDF is still downloading
  2970       downloadByUrl();
  2971       return;
  2974     this.pdfDocument.getData().then(
  2975       function getDataSuccess(data) {
  2976         var blob = PDFJS.createBlob(data, 'application/pdf');
  2977         downloadManager.download(blob, url, filename);
  2978       },
  2979       downloadByUrl // Error occurred try downloading with just the url.
  2980     ).then(null, downloadByUrl);
  2981   },
  2983   fallback: function pdfViewFallback(featureId) {
  2984     // Only trigger the fallback once so we don't spam the user with messages
  2985     // for one PDF.
  2986     if (this.fellback)
  2987       return;
  2988     this.fellback = true;
  2989     var url = this.url.split('#')[0];
  2990     FirefoxCom.request('fallback', { featureId: featureId, url: url },
  2991       function response(download) {
  2992         if (!download) {
  2993           return;
  2995         PDFView.download();
  2996       });
  2997   },
  2999   navigateTo: function pdfViewNavigateTo(dest) {
  3000     var destString = '';
  3001     var self = this;
  3003     var goToDestination = function(destRef) {
  3004       self.pendingRefStr = null;
  3005       // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
  3006       var pageNumber = destRef instanceof Object ?
  3007         self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
  3008         (destRef + 1);
  3009       if (pageNumber) {
  3010         if (pageNumber > self.pages.length) {
  3011           pageNumber = self.pages.length;
  3013         var currentPage = self.pages[pageNumber - 1];
  3014         currentPage.scrollIntoView(dest);
  3016         // Update the browsing history.
  3017         PDFHistory.push({ dest: dest, hash: destString, page: pageNumber });
  3018       } else {
  3019         self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) {
  3020           var pageNum = pageIndex + 1;
  3021           self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum;
  3022           goToDestination(destRef);
  3023         });
  3025     };
  3027     this.destinationsPromise.then(function() {
  3028       if (typeof dest === 'string') {
  3029         destString = dest;
  3030         dest = self.destinations[dest];
  3032       if (!(dest instanceof Array)) {
  3033         return; // invalid destination
  3035       goToDestination(dest[0]);
  3036     });
  3037   },
  3039   getDestinationHash: function pdfViewGetDestinationHash(dest) {
  3040     if (typeof dest === 'string') {
  3041       return PDFView.getAnchorUrl('#' + escape(dest));
  3043     if (dest instanceof Array) {
  3044       var destRef = dest[0]; // see navigateTo method for dest format
  3045       var pageNumber = destRef instanceof Object ?
  3046         this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
  3047         (destRef + 1);
  3048       if (pageNumber) {
  3049         var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber);
  3050         var destKind = dest[1];
  3051         if (typeof destKind === 'object' && 'name' in destKind &&
  3052             destKind.name == 'XYZ') {
  3053           var scale = (dest[4] || this.currentScaleValue);
  3054           var scaleNumber = parseFloat(scale);
  3055           if (scaleNumber) {
  3056             scale = scaleNumber * 100;
  3058           pdfOpenParams += '&zoom=' + scale;
  3059           if (dest[2] || dest[3]) {
  3060             pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
  3063         return pdfOpenParams;
  3066     return '';
  3067   },
  3069   /**
  3070    * Prefix the full url on anchor links to make sure that links are resolved
  3071    * relative to the current URL instead of the one defined in <base href>.
  3072    * @param {String} anchor The anchor hash, including the #.
  3073    */
  3074   getAnchorUrl: function getAnchorUrl(anchor) {
  3075     return this.url.split('#')[0] + anchor;
  3076   },
  3078   /**
  3079    * Show the error box.
  3080    * @param {String} message A message that is human readable.
  3081    * @param {Object} moreInfo (optional) Further information about the error
  3082    *                            that is more technical.  Should have a 'message'
  3083    *                            and optionally a 'stack' property.
  3084    */
  3085   error: function pdfViewError(message, moreInfo) {
  3086     var moreInfoText = mozL10n.get('error_version_info',
  3087       {version: PDFJS.version || '?', build: PDFJS.build || '?'},
  3088       'PDF.js v{{version}} (build: {{build}})') + '\n';
  3089     if (moreInfo) {
  3090       moreInfoText +=
  3091         mozL10n.get('error_message', {message: moreInfo.message},
  3092         'Message: {{message}}');
  3093       if (moreInfo.stack) {
  3094         moreInfoText += '\n' +
  3095           mozL10n.get('error_stack', {stack: moreInfo.stack},
  3096           'Stack: {{stack}}');
  3097       } else {
  3098         if (moreInfo.filename) {
  3099           moreInfoText += '\n' +
  3100             mozL10n.get('error_file', {file: moreInfo.filename},
  3101             'File: {{file}}');
  3103         if (moreInfo.lineNumber) {
  3104           moreInfoText += '\n' +
  3105             mozL10n.get('error_line', {line: moreInfo.lineNumber},
  3106             'Line: {{line}}');
  3111     console.error(message + '\n' + moreInfoText);
  3112     this.fallback();
  3113   },
  3115   progress: function pdfViewProgress(level) {
  3116     var percent = Math.round(level * 100);
  3117     // When we transition from full request to range requests, it's possible
  3118     // that we discard some of the loaded data. This can cause the loading
  3119     // bar to move backwards. So prevent this by only updating the bar if it
  3120     // increases.
  3121     if (percent > PDFView.loadingBar.percent) {
  3122       PDFView.loadingBar.percent = percent;
  3124   },
  3126   load: function pdfViewLoad(pdfDocument, scale) {
  3127     var self = this;
  3128     var isOnePageRenderedResolved = false;
  3129     var resolveOnePageRendered = null;
  3130     var onePageRendered = new Promise(function (resolve) {
  3131       resolveOnePageRendered = resolve;
  3132     });
  3133     function bindOnAfterDraw(pageView, thumbnailView) {
  3134       // when page is painted, using the image as thumbnail base
  3135       pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() {
  3136         if (!isOnePageRenderedResolved) {
  3137           isOnePageRenderedResolved = true;
  3138           resolveOnePageRendered();
  3140         thumbnailView.setImage(pageView.canvas);
  3141       };
  3144     PDFFindController.reset();
  3146     this.pdfDocument = pdfDocument;
  3148     DocumentProperties.resolveDataAvailable();
  3150     var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
  3151       self.downloadComplete = true;
  3152       PDFView.loadingBar.hide();
  3153       var outerContainer = document.getElementById('outerContainer');
  3154       outerContainer.classList.remove('loadingInProgress');
  3155     });
  3157     var pagesCount = pdfDocument.numPages;
  3159     var id = pdfDocument.fingerprint;
  3160     document.getElementById('numPages').textContent =
  3161       mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
  3162     document.getElementById('pageNumber').max = pagesCount;
  3164     PDFView.documentFingerprint = id;
  3165     var store = PDFView.store = new ViewHistory(id);
  3167     this.pageRotation = 0;
  3169     var pages = this.pages = [];
  3170     var pagesRefMap = this.pagesRefMap = {};
  3171     var thumbnails = this.thumbnails = [];
  3173     var resolvePagesPromise;
  3174     var pagesPromise = new Promise(function (resolve) {
  3175       resolvePagesPromise = resolve;
  3176     });
  3177     this.pagesPromise = pagesPromise;
  3179     var firstPagePromise = pdfDocument.getPage(1);
  3180     var container = document.getElementById('viewer');
  3181     var thumbsView = document.getElementById('thumbnailView');
  3183     // Fetch a single page so we can get a viewport that will be the default
  3184     // viewport for all pages
  3185     firstPagePromise.then(function(pdfPage) {
  3186       var viewport = pdfPage.getViewport((scale || 1.0) * CSS_UNITS);
  3187       for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
  3188         var viewportClone = viewport.clone();
  3189         var pageView = new PageView(container, pageNum, scale,
  3190                                     self.navigateTo.bind(self),
  3191                                     viewportClone);
  3192         var thumbnailView = new ThumbnailView(thumbsView, pageNum,
  3193                                               viewportClone);
  3194         bindOnAfterDraw(pageView, thumbnailView);
  3195         pages.push(pageView);
  3196         thumbnails.push(thumbnailView);
  3199       // Fetch all the pages since the viewport is needed before printing
  3200       // starts to create the correct size canvas. Wait until one page is
  3201       // rendered so we don't tie up too many resources early on.
  3202       onePageRendered.then(function () {
  3203         if (!PDFJS.disableAutoFetch) {
  3204           var getPagesLeft = pagesCount;
  3205           for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
  3206             pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) {
  3207               var pageView = pages[pageNum - 1];
  3208               if (!pageView.pdfPage) {
  3209                 pageView.setPdfPage(pdfPage);
  3211               var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R';
  3212               pagesRefMap[refStr] = pageNum;
  3213               getPagesLeft--;
  3214               if (!getPagesLeft) {
  3215                 resolvePagesPromise();
  3217             }.bind(null, pageNum));
  3219         } else {
  3220           // XXX: Printing is semi-broken with auto fetch disabled.
  3221           resolvePagesPromise();
  3223       });
  3225       downloadedPromise.then(function () {
  3226         var event = document.createEvent('CustomEvent');
  3227         event.initCustomEvent('documentload', true, true, {});
  3228         window.dispatchEvent(event);
  3229       });
  3231       PDFView.loadingBar.setWidth(container);
  3233       PDFFindController.resolveFirstPage();
  3235       // Initialize the browsing history.
  3236       PDFHistory.initialize(self.documentFingerprint);
  3237     });
  3239     // Fetch the necessary preference values.
  3240     var showPreviousViewOnLoad;
  3241     var showPreviousViewOnLoadPromise =
  3242       Preferences.get('showPreviousViewOnLoad').then(function (prefValue) {
  3243         showPreviousViewOnLoad = prefValue;
  3244       });
  3245     var defaultZoomValue;
  3246     var defaultZoomValuePromise =
  3247       Preferences.get('defaultZoomValue').then(function (prefValue) {
  3248         defaultZoomValue = prefValue;
  3249       });
  3251     var storePromise = store.initializedPromise;
  3252     Promise.all([firstPagePromise, storePromise, showPreviousViewOnLoadPromise,
  3253                  defaultZoomValuePromise]).then(function resolved() {
  3254       var storedHash = null;
  3255       if (showPreviousViewOnLoad && store.get('exists', false)) {
  3256         var pageNum = store.get('page', '1');
  3257         var zoom = defaultZoomValue || store.get('zoom', PDFView.currentScale);
  3258         var left = store.get('scrollLeft', '0');
  3259         var top = store.get('scrollTop', '0');
  3261         storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' +
  3262                      left + ',' + top;
  3263       } else if (defaultZoomValue) {
  3264         storedHash = 'page=1&zoom=' + defaultZoomValue;
  3266       self.setInitialView(storedHash, scale);
  3268       // Make all navigation keys work on document load,
  3269       // unless the viewer is embedded in a web page.
  3270       if (!self.isViewerEmbedded) {
  3271         self.container.focus();
  3272         self.container.blur();
  3274     }, function rejected(errorMsg) {
  3275       console.error(errorMsg);
  3277       firstPagePromise.then(function () {
  3278         self.setInitialView(null, scale);
  3279       });
  3280     });
  3282     pagesPromise.then(function() {
  3283       if (PDFView.supportsPrinting) {
  3284         pdfDocument.getJavaScript().then(function(javaScript) {
  3285           if (javaScript.length) {
  3286             console.warn('Warning: JavaScript is not supported');
  3287             PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript);
  3289           // Hack to support auto printing.
  3290           var regex = /\bprint\s*\(/g;
  3291           for (var i = 0, ii = javaScript.length; i < ii; i++) {
  3292             var js = javaScript[i];
  3293             if (js && regex.test(js)) {
  3294               setTimeout(function() {
  3295                 window.print();
  3296               });
  3297               return;
  3300         });
  3302     });
  3304     var destinationsPromise =
  3305       this.destinationsPromise = pdfDocument.getDestinations();
  3306     destinationsPromise.then(function(destinations) {
  3307       self.destinations = destinations;
  3308     });
  3310     // outline depends on destinations and pagesRefMap
  3311     var promises = [pagesPromise, destinationsPromise,
  3312                     PDFView.animationStartedPromise];
  3313     Promise.all(promises).then(function() {
  3314       pdfDocument.getOutline().then(function(outline) {
  3315         self.outline = new DocumentOutlineView(outline);
  3316         document.getElementById('viewOutline').disabled = !outline;
  3318         if (outline) {
  3319           Preferences.get('ifAvailableShowOutlineOnLoad').then(
  3320             function (prefValue) {
  3321               if (prefValue) {
  3322                 if (!self.sidebarOpen) {
  3323                   document.getElementById('sidebarToggle').click();
  3325                 self.switchSidebarView('outline');
  3327             });
  3329       });
  3330       pdfDocument.getAttachments().then(function(attachments) {
  3331         self.attachments = new DocumentAttachmentsView(attachments);
  3332         document.getElementById('viewAttachments').disabled = !attachments;
  3333       });
  3334     });
  3336     pdfDocument.getMetadata().then(function(data) {
  3337       var info = data.info, metadata = data.metadata;
  3338       self.documentInfo = info;
  3339       self.metadata = metadata;
  3341       // Provides some basic debug information
  3342       console.log('PDF ' + pdfDocument.fingerprint + ' [' +
  3343                   info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() +
  3344                   ' / ' + (info.Creator || '-').trim() + ']' +
  3345                   ' (PDF.js: ' + (PDFJS.version || '-') +
  3346                   (!PDFJS.disableWebGL ? ' [WebGL]' : '') + ')');
  3348       var pdfTitle;
  3349       if (metadata && metadata.has('dc:title')) {
  3350         pdfTitle = metadata.get('dc:title');
  3353       if (!pdfTitle && info && info['Title']) {
  3354         pdfTitle = info['Title'];
  3357       if (pdfTitle) {
  3358         self.setTitle(pdfTitle + ' - ' + document.title);
  3361       if (info.IsAcroFormPresent) {
  3362         console.warn('Warning: AcroForm/XFA is not supported');
  3363         PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.forms);
  3366       var versionId = String(info.PDFFormatVersion).slice(-1) | 0;
  3367       var generatorId = 0;
  3368       var KNOWN_GENERATORS = ["acrobat distiller", "acrobat pdfwritter",
  3369        "adobe livecycle", "adobe pdf library", "adobe photoshop", "ghostscript",
  3370        "tcpdf", "cairo", "dvipdfm", "dvips", "pdftex", "pdfkit", "itext",
  3371        "prince", "quarkxpress", "mac os x", "microsoft", "openoffice", "oracle",
  3372        "luradocument", "pdf-xchange", "antenna house", "aspose.cells", "fpdf"];
  3373       var generatorId = 0;
  3374       if (info.Producer) {
  3375         KNOWN_GENERATORS.some(function (generator, s, i) {
  3376           if (generator.indexOf(s) < 0) {
  3377             return false;
  3379           generatorId = i + 1;
  3380           return true;
  3381         }.bind(null, info.Producer.toLowerCase()));
  3383       var formType = !info.IsAcroFormPresent ? null : info.IsXFAPresent ?
  3384                      'xfa' : 'acroform';
  3385       FirefoxCom.request('reportTelemetry', JSON.stringify({
  3386         type: 'documentInfo',
  3387         version: versionId,
  3388         generator: generatorId,
  3389         formType: formType
  3390       }));
  3391     });
  3392   },
  3394   setInitialView: function pdfViewSetInitialView(storedHash, scale) {
  3395     // Reset the current scale, as otherwise the page's scale might not get
  3396     // updated if the zoom level stayed the same.
  3397     this.currentScale = 0;
  3398     this.currentScaleValue = null;
  3399     // When opening a new file (when one is already loaded in the viewer):
  3400     // Reset 'currentPageNumber', since otherwise the page's scale will be wrong
  3401     // if 'currentPageNumber' is larger than the number of pages in the file.
  3402     document.getElementById('pageNumber').value = currentPageNumber = 1;
  3403     // Reset the current position when loading a new file,
  3404     // to prevent displaying the wrong position in the document.
  3405     this.currentPosition = null;
  3407     if (PDFHistory.initialDestination) {
  3408       this.navigateTo(PDFHistory.initialDestination);
  3409       PDFHistory.initialDestination = null;
  3410     } else if (this.initialBookmark) {
  3411       this.setHash(this.initialBookmark);
  3412       PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark);
  3413       this.initialBookmark = null;
  3414     } else if (storedHash) {
  3415       this.setHash(storedHash);
  3416     } else if (scale) {
  3417       this.setScale(scale, true);
  3418       this.page = 1;
  3421     if (PDFView.currentScale === UNKNOWN_SCALE) {
  3422       // Scale was not initialized: invalid bookmark or scale was not specified.
  3423       // Setting the default one.
  3424       this.setScale(DEFAULT_SCALE, true);
  3426   },
  3428   renderHighestPriority: function pdfViewRenderHighestPriority() {
  3429     if (PDFView.idleTimeout) {
  3430       clearTimeout(PDFView.idleTimeout);
  3431       PDFView.idleTimeout = null;
  3434     // Pages have a higher priority than thumbnails, so check them first.
  3435     var visiblePages = this.getVisiblePages();
  3436     var pageView = this.getHighestPriority(visiblePages, this.pages,
  3437                                            this.pageViewScroll.down);
  3438     if (pageView) {
  3439       this.renderView(pageView, 'page');
  3440       return;
  3442     // No pages needed rendering so check thumbnails.
  3443     if (this.sidebarOpen) {
  3444       var visibleThumbs = this.getVisibleThumbs();
  3445       var thumbView = this.getHighestPriority(visibleThumbs,
  3446                                               this.thumbnails,
  3447                                               this.thumbnailViewScroll.down);
  3448       if (thumbView) {
  3449         this.renderView(thumbView, 'thumbnail');
  3450         return;
  3454     PDFView.idleTimeout = setTimeout(function () {
  3455       PDFView.cleanup();
  3456     }, CLEANUP_TIMEOUT);
  3457   },
  3459   cleanup: function pdfViewCleanup() {
  3460     for (var i = 0, ii = this.pages.length; i < ii; i++) {
  3461       if (this.pages[i] &&
  3462           this.pages[i].renderingState !== RenderingStates.FINISHED) {
  3463         this.pages[i].reset();
  3466     this.pdfDocument.cleanup();
  3467   },
  3469   getHighestPriority: function pdfViewGetHighestPriority(visible, views,
  3470                                                          scrolledDown) {
  3471     // The state has changed figure out which page has the highest priority to
  3472     // render next (if any).
  3473     // Priority:
  3474     // 1 visible pages
  3475     // 2 if last scrolled down page after the visible pages
  3476     // 2 if last scrolled up page before the visible pages
  3477     var visibleViews = visible.views;
  3479     var numVisible = visibleViews.length;
  3480     if (numVisible === 0) {
  3481       return false;
  3483     for (var i = 0; i < numVisible; ++i) {
  3484       var view = visibleViews[i].view;
  3485       if (!this.isViewFinished(view)) {
  3486         return view;
  3490     // All the visible views have rendered, try to render next/previous pages.
  3491     if (scrolledDown) {
  3492       var nextPageIndex = visible.last.id;
  3493       // ID's start at 1 so no need to add 1.
  3494       if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex])) {
  3495         return views[nextPageIndex];
  3497     } else {
  3498       var previousPageIndex = visible.first.id - 2;
  3499       if (views[previousPageIndex] &&
  3500           !this.isViewFinished(views[previousPageIndex])) {
  3501         return views[previousPageIndex];
  3504     // Everything that needs to be rendered has been.
  3505     return false;
  3506   },
  3508   isViewFinished: function pdfViewIsViewFinished(view) {
  3509     return view.renderingState === RenderingStates.FINISHED;
  3510   },
  3512   // Render a page or thumbnail view. This calls the appropriate function based
  3513   // on the views state. If the view is already rendered it will return false.
  3514   renderView: function pdfViewRender(view, type) {
  3515     var state = view.renderingState;
  3516     switch (state) {
  3517       case RenderingStates.FINISHED:
  3518         return false;
  3519       case RenderingStates.PAUSED:
  3520         PDFView.highestPriorityPage = type + view.id;
  3521         view.resume();
  3522         break;
  3523       case RenderingStates.RUNNING:
  3524         PDFView.highestPriorityPage = type + view.id;
  3525         break;
  3526       case RenderingStates.INITIAL:
  3527         PDFView.highestPriorityPage = type + view.id;
  3528         view.draw(this.renderHighestPriority.bind(this));
  3529         break;
  3531     return true;
  3532   },
  3534   setHash: function pdfViewSetHash(hash) {
  3535     if (!hash) {
  3536       return;
  3539     if (hash.indexOf('=') >= 0) {
  3540       var params = PDFView.parseQueryString(hash);
  3541       // borrowing syntax from "Parameters for Opening PDF Files"
  3542       if ('nameddest' in params) {
  3543         PDFHistory.updateNextHashParam(params.nameddest);
  3544         PDFView.navigateTo(params.nameddest);
  3545         return;
  3547       var pageNumber, dest;
  3548       if ('page' in params) {
  3549         pageNumber = (params.page | 0) || 1;
  3551       if ('zoom' in params) {
  3552         var zoomArgs = params.zoom.split(','); // scale,left,top
  3553         // building destination array
  3555         // If the zoom value, it has to get divided by 100. If it is a string,
  3556         // it should stay as it is.
  3557         var zoomArg = zoomArgs[0];
  3558         var zoomArgNumber = parseFloat(zoomArg);
  3559         if (zoomArgNumber) {
  3560           zoomArg = zoomArgNumber / 100;
  3562         dest = [null, {name: 'XYZ'},
  3563                 zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
  3564                 zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
  3565                 zoomArg];
  3567       if (dest) {
  3568         var currentPage = this.pages[(pageNumber || this.page) - 1];
  3569         currentPage.scrollIntoView(dest);
  3570       } else if (pageNumber) {
  3571         this.page = pageNumber; // simple page
  3573       if ('pagemode' in params) {
  3574         var toggle = document.getElementById('sidebarToggle');
  3575         if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' ||
  3576             params.pagemode === 'attachments') {
  3577           if (!this.sidebarOpen) {
  3578             toggle.click();
  3580           this.switchSidebarView(params.pagemode === 'bookmarks' ?
  3581                                    'outline' :
  3582                                    params.pagemode);
  3583         } else if (params.pagemode === 'none' && this.sidebarOpen) {
  3584           toggle.click();
  3587     } else if (/^\d+$/.test(hash)) { // page number
  3588       this.page = hash;
  3589     } else { // named destination
  3590       PDFHistory.updateNextHashParam(unescape(hash));
  3591       PDFView.navigateTo(unescape(hash));
  3593   },
  3595   switchSidebarView: function pdfViewSwitchSidebarView(view) {
  3596     var thumbsView = document.getElementById('thumbnailView');
  3597     var outlineView = document.getElementById('outlineView');
  3598     var attachmentsView = document.getElementById('attachmentsView');
  3600     var thumbsButton = document.getElementById('viewThumbnail');
  3601     var outlineButton = document.getElementById('viewOutline');
  3602     var attachmentsButton = document.getElementById('viewAttachments');
  3604     switch (view) {
  3605       case 'thumbs':
  3606         var wasAnotherViewVisible = thumbsView.classList.contains('hidden');
  3608         thumbsButton.classList.add('toggled');
  3609         outlineButton.classList.remove('toggled');
  3610         attachmentsButton.classList.remove('toggled');
  3611         thumbsView.classList.remove('hidden');
  3612         outlineView.classList.add('hidden');
  3613         attachmentsView.classList.add('hidden');
  3615         PDFView.renderHighestPriority();
  3617         if (wasAnotherViewVisible) {
  3618           // Ensure that the thumbnail of the current page is visible
  3619           // when switching from another view.
  3620           scrollIntoView(document.getElementById('thumbnailContainer' +
  3621                                                  this.page));
  3623         break;
  3625       case 'outline':
  3626         thumbsButton.classList.remove('toggled');
  3627         outlineButton.classList.add('toggled');
  3628         attachmentsButton.classList.remove('toggled');
  3629         thumbsView.classList.add('hidden');
  3630         outlineView.classList.remove('hidden');
  3631         attachmentsView.classList.add('hidden');
  3633         if (outlineButton.getAttribute('disabled')) {
  3634           return;
  3636         break;
  3638       case 'attachments':
  3639         thumbsButton.classList.remove('toggled');
  3640         outlineButton.classList.remove('toggled');
  3641         attachmentsButton.classList.add('toggled');
  3642         thumbsView.classList.add('hidden');
  3643         outlineView.classList.add('hidden');
  3644         attachmentsView.classList.remove('hidden');
  3646         if (attachmentsButton.getAttribute('disabled')) {
  3647           return;
  3649         break;
  3651   },
  3653   getVisiblePages: function pdfViewGetVisiblePages() {
  3654     if (!PresentationMode.active) {
  3655       return this.getVisibleElements(this.container, this.pages, true);
  3656     } else {
  3657       // The algorithm in getVisibleElements doesn't work in all browsers and
  3658       // configurations when presentation mode is active.
  3659       var visible = [];
  3660       var currentPage = this.pages[this.page - 1];
  3661       visible.push({ id: currentPage.id, view: currentPage });
  3662       return { first: currentPage, last: currentPage, views: visible };
  3664   },
  3666   getVisibleThumbs: function pdfViewGetVisibleThumbs() {
  3667     return this.getVisibleElements(this.thumbnailContainer, this.thumbnails);
  3668   },
  3670   // Generic helper to find out what elements are visible within a scroll pane.
  3671   getVisibleElements: function pdfViewGetVisibleElements(
  3672       scrollEl, views, sortByVisibility) {
  3673     var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
  3674     var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
  3676     var visible = [], view;
  3677     var currentHeight, viewHeight, hiddenHeight, percentHeight;
  3678     var currentWidth, viewWidth;
  3679     for (var i = 0, ii = views.length; i < ii; ++i) {
  3680       view = views[i];
  3681       currentHeight = view.el.offsetTop + view.el.clientTop;
  3682       viewHeight = view.el.clientHeight;
  3683       if ((currentHeight + viewHeight) < top) {
  3684         continue;
  3686       if (currentHeight > bottom) {
  3687         break;
  3689       currentWidth = view.el.offsetLeft + view.el.clientLeft;
  3690       viewWidth = view.el.clientWidth;
  3691       if ((currentWidth + viewWidth) < left || currentWidth > right) {
  3692         continue;
  3694       hiddenHeight = Math.max(0, top - currentHeight) +
  3695                      Math.max(0, currentHeight + viewHeight - bottom);
  3696       percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
  3698       visible.push({ id: view.id, x: currentWidth, y: currentHeight,
  3699                      view: view, percent: percentHeight });
  3702     var first = visible[0];
  3703     var last = visible[visible.length - 1];
  3705     if (sortByVisibility) {
  3706       visible.sort(function(a, b) {
  3707         var pc = a.percent - b.percent;
  3708         if (Math.abs(pc) > 0.001) {
  3709           return -pc;
  3711         return a.id - b.id; // ensure stability
  3712       });
  3714     return {first: first, last: last, views: visible};
  3715   },
  3717   // Helper function to parse query string (e.g. ?param1=value&parm2=...).
  3718   parseQueryString: function pdfViewParseQueryString(query) {
  3719     var parts = query.split('&');
  3720     var params = {};
  3721     for (var i = 0, ii = parts.length; i < ii; ++i) {
  3722       var param = parts[i].split('=');
  3723       var key = param[0];
  3724       var value = param.length > 1 ? param[1] : null;
  3725       params[decodeURIComponent(key)] = decodeURIComponent(value);
  3727     return params;
  3728   },
  3730   beforePrint: function pdfViewSetupBeforePrint() {
  3731     if (!this.supportsPrinting) {
  3732       var printMessage = mozL10n.get('printing_not_supported', null,
  3733           'Warning: Printing is not fully supported by this browser.');
  3734       this.error(printMessage);
  3735       return;
  3738     var alertNotReady = false;
  3739     var i, ii;
  3740     if (!this.pages.length) {
  3741       alertNotReady = true;
  3742     } else {
  3743       for (i = 0, ii = this.pages.length; i < ii; ++i) {
  3744         if (!this.pages[i].pdfPage) {
  3745           alertNotReady = true;
  3746           break;
  3750     if (alertNotReady) {
  3751       var notReadyMessage = mozL10n.get('printing_not_ready', null,
  3752           'Warning: The PDF is not fully loaded for printing.');
  3753       window.alert(notReadyMessage);
  3754       return;
  3757     var body = document.querySelector('body');
  3758     body.setAttribute('data-mozPrintCallback', true);
  3759     for (i = 0, ii = this.pages.length; i < ii; ++i) {
  3760       this.pages[i].beforePrint();
  3762   },
  3764   afterPrint: function pdfViewSetupAfterPrint() {
  3765     var div = document.getElementById('printContainer');
  3766     while (div.hasChildNodes()) {
  3767       div.removeChild(div.lastChild);
  3769   },
  3771   rotatePages: function pdfViewRotatePages(delta) {
  3772     var currentPage = this.pages[this.page - 1];
  3773     var i, l;
  3774     this.pageRotation = (this.pageRotation + 360 + delta) % 360;
  3776     for (i = 0, l = this.pages.length; i < l; i++) {
  3777       var page = this.pages[i];
  3778       page.update(page.scale, this.pageRotation);
  3781     for (i = 0, l = this.thumbnails.length; i < l; i++) {
  3782       var thumb = this.thumbnails[i];
  3783       thumb.update(this.pageRotation);
  3786     this.setScale(this.currentScaleValue, true, true);
  3788     this.renderHighestPriority();
  3790     if (currentPage) {
  3791       currentPage.scrollIntoView();
  3793   },
  3795   /**
  3796    * This function flips the page in presentation mode if the user scrolls up
  3797    * or down with large enough motion and prevents page flipping too often.
  3799    * @this {PDFView}
  3800    * @param {number} mouseScrollDelta The delta value from the mouse event.
  3801    */
  3802   mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) {
  3803     var MOUSE_SCROLL_COOLDOWN_TIME = 50;
  3805     var currentTime = (new Date()).getTime();
  3806     var storedTime = this.mouseScrollTimeStamp;
  3808     // In case one page has already been flipped there is a cooldown time
  3809     // which has to expire before next page can be scrolled on to.
  3810     if (currentTime > storedTime &&
  3811         currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) {
  3812       return;
  3815     // In case the user decides to scroll to the opposite direction than before
  3816     // clear the accumulated delta.
  3817     if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) ||
  3818         (this.mouseScrollDelta < 0 && mouseScrollDelta > 0)) {
  3819       this.clearMouseScrollState();
  3822     this.mouseScrollDelta += mouseScrollDelta;
  3824     var PAGE_FLIP_THRESHOLD = 120;
  3825     if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) {
  3827       var PageFlipDirection = {
  3828         UP: -1,
  3829         DOWN: 1
  3830       };
  3832       // In presentation mode scroll one page at a time.
  3833       var pageFlipDirection = (this.mouseScrollDelta > 0) ?
  3834                                 PageFlipDirection.UP :
  3835                                 PageFlipDirection.DOWN;
  3836       this.clearMouseScrollState();
  3837       var currentPage = this.page;
  3839       // In case we are already on the first or the last page there is no need
  3840       // to do anything.
  3841       if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) ||
  3842           (currentPage == this.pages.length &&
  3843            pageFlipDirection == PageFlipDirection.DOWN)) {
  3844         return;
  3847       this.page += pageFlipDirection;
  3848       this.mouseScrollTimeStamp = currentTime;
  3850   },
  3852   /**
  3853    * This function clears the member attributes used with mouse scrolling in
  3854    * presentation mode.
  3856    * @this {PDFView}
  3857    */
  3858   clearMouseScrollState: function pdfViewClearMouseScrollState() {
  3859     this.mouseScrollTimeStamp = 0;
  3860     this.mouseScrollDelta = 0;
  3862 };
  3865 var PageView = function pageView(container, id, scale,
  3866                                  navigateTo, defaultViewport) {
  3867   this.id = id;
  3869   this.rotation = 0;
  3870   this.scale = scale || 1.0;
  3871   this.viewport = defaultViewport;
  3872   this.pdfPageRotate = defaultViewport.rotation;
  3874   this.renderingState = RenderingStates.INITIAL;
  3875   this.resume = null;
  3877   this.textLayer = null;
  3879   this.zoomLayer = null;
  3881   this.annotationLayer = null;
  3883   var anchor = document.createElement('a');
  3884   anchor.name = '' + this.id;
  3886   var div = this.el = document.createElement('div');
  3887   div.id = 'pageContainer' + this.id;
  3888   div.className = 'page';
  3889   div.style.width = Math.floor(this.viewport.width) + 'px';
  3890   div.style.height = Math.floor(this.viewport.height) + 'px';
  3892   container.appendChild(anchor);
  3893   container.appendChild(div);
  3895   this.setPdfPage = function pageViewSetPdfPage(pdfPage) {
  3896     this.pdfPage = pdfPage;
  3897     this.pdfPageRotate = pdfPage.rotate;
  3898     var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
  3899     this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, totalRotation);
  3900     this.stats = pdfPage.stats;
  3901     this.reset();
  3902   };
  3904   this.destroy = function pageViewDestroy() {
  3905     this.zoomLayer = null;
  3906     this.reset();
  3907     if (this.pdfPage) {
  3908       this.pdfPage.destroy();
  3910   };
  3912   this.reset = function pageViewReset(keepAnnotations) {
  3913     if (this.renderTask) {
  3914       this.renderTask.cancel();
  3916     this.resume = null;
  3917     this.renderingState = RenderingStates.INITIAL;
  3919     div.style.width = Math.floor(this.viewport.width) + 'px';
  3920     div.style.height = Math.floor(this.viewport.height) + 'px';
  3922     var childNodes = div.childNodes;
  3923     for (var i = div.childNodes.length - 1; i >= 0; i--) {
  3924       var node = childNodes[i];
  3925       if ((this.zoomLayer && this.zoomLayer === node) ||
  3926           (keepAnnotations && this.annotationLayer === node)) {
  3927         continue;
  3929       div.removeChild(node);
  3931     div.removeAttribute('data-loaded');
  3933     if (keepAnnotations) {
  3934       if (this.annotationLayer) {
  3935         // Hide annotationLayer until all elements are resized
  3936         // so they are not displayed on the already-resized page
  3937         this.annotationLayer.setAttribute('hidden', 'true');
  3939     } else {
  3940       this.annotationLayer = null;
  3943     delete this.canvas;
  3945     this.loadingIconDiv = document.createElement('div');
  3946     this.loadingIconDiv.className = 'loadingIcon';
  3947     div.appendChild(this.loadingIconDiv);
  3948   };
  3950   this.update = function pageViewUpdate(scale, rotation) {
  3951     this.scale = scale || this.scale;
  3953     if (typeof rotation !== 'undefined') {
  3954       this.rotation = rotation;
  3957     var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
  3958     this.viewport = this.viewport.clone({
  3959       scale: this.scale * CSS_UNITS,
  3960       rotation: totalRotation
  3961     });
  3963     if (USE_ONLY_CSS_ZOOM && this.canvas) {
  3964       this.cssTransform(this.canvas);
  3965       return;
  3966     } else if (this.canvas && !this.zoomLayer) {
  3967       this.zoomLayer = this.canvas.parentNode;
  3968       this.zoomLayer.style.position = 'absolute';
  3970     if (this.zoomLayer) {
  3971       this.cssTransform(this.zoomLayer.firstChild);
  3973     this.reset(true);
  3974   };
  3976   this.cssTransform = function pageCssTransform(canvas) {
  3977     // Scale canvas, canvas wrapper, and page container.
  3978     var width = this.viewport.width;
  3979     var height = this.viewport.height;
  3980     canvas.style.width = canvas.parentNode.style.width = div.style.width =
  3981         Math.floor(width) + 'px';
  3982     canvas.style.height = canvas.parentNode.style.height = div.style.height =
  3983         Math.floor(height) + 'px';
  3984     // The canvas may have been originally rotated, so rotate relative to that.
  3985     var relativeRotation = this.viewport.rotation - canvas._viewport.rotation;
  3986     var absRotation = Math.abs(relativeRotation);
  3987     var scaleX = 1, scaleY = 1;
  3988     if (absRotation === 90 || absRotation === 270) {
  3989       // Scale x and y because of the rotation.
  3990       scaleX = height / width;
  3991       scaleY = width / height;
  3993     var cssTransform = 'rotate(' + relativeRotation + 'deg) ' +
  3994                        'scale(' + scaleX + ',' + scaleY + ')';
  3995     CustomStyle.setProp('transform', canvas, cssTransform);
  3997     if (this.textLayer) {
  3998       // Rotating the text layer is more complicated since the divs inside the
  3999       // the text layer are rotated.
  4000       // TODO: This could probably be simplified by drawing the text layer in
  4001       // one orientation then rotating overall.
  4002       var textRelativeRotation = this.viewport.rotation -
  4003                                  this.textLayer.viewport.rotation;
  4004       var textAbsRotation = Math.abs(textRelativeRotation);
  4005       var scale = (width / canvas.width);
  4006       if (textAbsRotation === 90 || textAbsRotation === 270) {
  4007         scale = width / canvas.height;
  4009       var textLayerDiv = this.textLayer.textLayerDiv;
  4010       var transX, transY;
  4011       switch (textAbsRotation) {
  4012         case 0:
  4013           transX = transY = 0;
  4014           break;
  4015         case 90:
  4016           transX = 0;
  4017           transY = '-' + textLayerDiv.style.height;
  4018           break;
  4019         case 180:
  4020           transX = '-' + textLayerDiv.style.width;
  4021           transY = '-' + textLayerDiv.style.height;
  4022           break;
  4023         case 270:
  4024           transX = '-' + textLayerDiv.style.width;
  4025           transY = 0;
  4026           break;
  4027         default:
  4028           console.error('Bad rotation value.');
  4029           break;
  4031       CustomStyle.setProp('transform', textLayerDiv,
  4032                           'rotate(' + textAbsRotation + 'deg) ' +
  4033                             'scale(' + scale + ', ' + scale + ') ' +
  4034                             'translate(' + transX + ', ' + transY + ')');
  4035       CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%');
  4038     if (USE_ONLY_CSS_ZOOM && this.annotationLayer) {
  4039       setupAnnotations(div, this.pdfPage, this.viewport);
  4041   };
  4043   Object.defineProperty(this, 'width', {
  4044     get: function PageView_getWidth() {
  4045       return this.viewport.width;
  4046     },
  4047     enumerable: true
  4048   });
  4050   Object.defineProperty(this, 'height', {
  4051     get: function PageView_getHeight() {
  4052       return this.viewport.height;
  4053     },
  4054     enumerable: true
  4055   });
  4057   var self = this;
  4059   function setupAnnotations(pageDiv, pdfPage, viewport) {
  4061     function bindLink(link, dest) {
  4062       link.href = PDFView.getDestinationHash(dest);
  4063       link.onclick = function pageViewSetupLinksOnclick() {
  4064         if (dest) {
  4065           PDFView.navigateTo(dest);
  4067         return false;
  4068       };
  4069       if (dest) {
  4070         link.className = 'internalLink';
  4074     function bindNamedAction(link, action) {
  4075       link.href = PDFView.getAnchorUrl('');
  4076       link.onclick = function pageViewSetupNamedActionOnClick() {
  4077         // See PDF reference, table 8.45 - Named action
  4078         switch (action) {
  4079           case 'GoToPage':
  4080             document.getElementById('pageNumber').focus();
  4081             break;
  4083           case 'GoBack':
  4084             PDFHistory.back();
  4085             break;
  4087           case 'GoForward':
  4088             PDFHistory.forward();
  4089             break;
  4091           case 'Find':
  4092             if (!PDFView.supportsIntegratedFind) {
  4093               PDFFindBar.toggle();
  4095             break;
  4097           case 'NextPage':
  4098             PDFView.page++;
  4099             break;
  4101           case 'PrevPage':
  4102             PDFView.page--;
  4103             break;
  4105           case 'LastPage':
  4106             PDFView.page = PDFView.pages.length;
  4107             break;
  4109           case 'FirstPage':
  4110             PDFView.page = 1;
  4111             break;
  4113           default:
  4114             break; // No action according to spec
  4116         return false;
  4117       };
  4118       link.className = 'internalLink';
  4121     pdfPage.getAnnotations().then(function(annotationsData) {
  4122       viewport = viewport.clone({ dontFlip: true });
  4123       var transform = viewport.transform;
  4124       var transformStr = 'matrix(' + transform.join(',') + ')';
  4125       var data, element, i, ii;
  4127       if (self.annotationLayer) {
  4128         // If an annotationLayer already exists, refresh its children's
  4129         // transformation matrices
  4130         for (i = 0, ii = annotationsData.length; i < ii; i++) {
  4131           data = annotationsData[i];
  4132           element = self.annotationLayer.querySelector(
  4133             '[data-annotation-id="' + data.id + '"]');
  4134           if (element) {
  4135             CustomStyle.setProp('transform', element, transformStr);
  4138         // See this.reset()
  4139         self.annotationLayer.removeAttribute('hidden');
  4140       } else {
  4141         for (i = 0, ii = annotationsData.length; i < ii; i++) {
  4142           data = annotationsData[i];
  4143           var annotation = PDFJS.Annotation.fromData(data);
  4144           if (!annotation || !annotation.hasHtml()) {
  4145             continue;
  4148           element = annotation.getHtmlElement(pdfPage.commonObjs);
  4149           element.setAttribute('data-annotation-id', data.id);
  4150           mozL10n.translate(element);
  4152           data = annotation.getData();
  4153           var rect = data.rect;
  4154           var view = pdfPage.view;
  4155           rect = PDFJS.Util.normalizeRect([
  4156             rect[0],
  4157             view[3] - rect[1] + view[1],
  4158             rect[2],
  4159             view[3] - rect[3] + view[1]
  4160           ]);
  4161           element.style.left = rect[0] + 'px';
  4162           element.style.top = rect[1] + 'px';
  4163           element.style.position = 'absolute';
  4165           CustomStyle.setProp('transform', element, transformStr);
  4166           var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px';
  4167           CustomStyle.setProp('transformOrigin', element, transformOriginStr);
  4169           if (data.subtype === 'Link' && !data.url) {
  4170             var link = element.getElementsByTagName('a')[0];
  4171             if (link) {
  4172               if (data.action) {
  4173                 bindNamedAction(link, data.action);
  4174               } else {
  4175                 bindLink(link, ('dest' in data) ? data.dest : null);
  4180           if (!self.annotationLayer) {
  4181             var annotationLayerDiv = document.createElement('div');
  4182             annotationLayerDiv.className = 'annotationLayer';
  4183             pageDiv.appendChild(annotationLayerDiv);
  4184             self.annotationLayer = annotationLayerDiv;
  4187           self.annotationLayer.appendChild(element);
  4190     });
  4193   this.getPagePoint = function pageViewGetPagePoint(x, y) {
  4194     return this.viewport.convertToPdfPoint(x, y);
  4195   };
  4197   this.scrollIntoView = function pageViewScrollIntoView(dest) {
  4198     if (PresentationMode.active) {
  4199       if (PDFView.page !== this.id) {
  4200         // Avoid breaking PDFView.getVisiblePages in presentation mode.
  4201         PDFView.page = this.id;
  4202         return;
  4204       dest = null;
  4205       PDFView.setScale(PDFView.currentScaleValue, true, true);
  4207     if (!dest) {
  4208       scrollIntoView(div);
  4209       return;
  4212     var x = 0, y = 0;
  4213     var width = 0, height = 0, widthScale, heightScale;
  4214     var changeOrientation = (this.rotation % 180 === 0 ? false : true);
  4215     var pageWidth = (changeOrientation ? this.height : this.width) /
  4216       this.scale / CSS_UNITS;
  4217     var pageHeight = (changeOrientation ? this.width : this.height) /
  4218       this.scale / CSS_UNITS;
  4219     var scale = 0;
  4220     switch (dest[1].name) {
  4221       case 'XYZ':
  4222         x = dest[2];
  4223         y = dest[3];
  4224         scale = dest[4];
  4225         // If x and/or y coordinates are not supplied, default to
  4226         // _top_ left of the page (not the obvious bottom left,
  4227         // since aligning the bottom of the intended page with the
  4228         // top of the window is rarely helpful).
  4229         x = x !== null ? x : 0;
  4230         y = y !== null ? y : pageHeight;
  4231         break;
  4232       case 'Fit':
  4233       case 'FitB':
  4234         scale = 'page-fit';
  4235         break;
  4236       case 'FitH':
  4237       case 'FitBH':
  4238         y = dest[2];
  4239         scale = 'page-width';
  4240         break;
  4241       case 'FitV':
  4242       case 'FitBV':
  4243         x = dest[2];
  4244         width = pageWidth;
  4245         height = pageHeight;
  4246         scale = 'page-height';
  4247         break;
  4248       case 'FitR':
  4249         x = dest[2];
  4250         y = dest[3];
  4251         width = dest[4] - x;
  4252         height = dest[5] - y;
  4253         widthScale = (PDFView.container.clientWidth - SCROLLBAR_PADDING) /
  4254           width / CSS_UNITS;
  4255         heightScale = (PDFView.container.clientHeight - SCROLLBAR_PADDING) /
  4256           height / CSS_UNITS;
  4257         scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
  4258         break;
  4259       default:
  4260         return;
  4263     if (scale && scale !== PDFView.currentScale) {
  4264       PDFView.setScale(scale, true, true);
  4265     } else if (PDFView.currentScale === UNKNOWN_SCALE) {
  4266       PDFView.setScale(DEFAULT_SCALE, true, true);
  4269     if (scale === 'page-fit' && !dest[4]) {
  4270       scrollIntoView(div);
  4271       return;
  4274     var boundingRect = [
  4275       this.viewport.convertToViewportPoint(x, y),
  4276       this.viewport.convertToViewportPoint(x + width, y + height)
  4277     ];
  4278     var left = Math.min(boundingRect[0][0], boundingRect[1][0]);
  4279     var top = Math.min(boundingRect[0][1], boundingRect[1][1]);
  4281     scrollIntoView(div, { left: left, top: top });
  4282   };
  4284   this.getTextContent = function pageviewGetTextContent() {
  4285     return PDFView.getPage(this.id).then(function(pdfPage) {
  4286       return pdfPage.getTextContent();
  4287     });
  4288   };
  4290   this.draw = function pageviewDraw(callback) {
  4291     var pdfPage = this.pdfPage;
  4293     if (this.pagePdfPromise) {
  4294       return;
  4296     if (!pdfPage) {
  4297       var promise = PDFView.getPage(this.id);
  4298       promise.then(function(pdfPage) {
  4299         delete this.pagePdfPromise;
  4300         this.setPdfPage(pdfPage);
  4301         this.draw(callback);
  4302       }.bind(this));
  4303       this.pagePdfPromise = promise;
  4304       return;
  4307     if (this.renderingState !== RenderingStates.INITIAL) {
  4308       console.error('Must be in new state before drawing');
  4311     this.renderingState = RenderingStates.RUNNING;
  4313     var viewport = this.viewport;
  4314     // Wrap the canvas so if it has a css transform for highdpi the overflow
  4315     // will be hidden in FF.
  4316     var canvasWrapper = document.createElement('div');
  4317     canvasWrapper.style.width = div.style.width;
  4318     canvasWrapper.style.height = div.style.height;
  4319     canvasWrapper.classList.add('canvasWrapper');
  4321     var canvas = document.createElement('canvas');
  4322     canvas.id = 'page' + this.id;
  4323     canvasWrapper.appendChild(canvas);
  4324     if (this.annotationLayer) {
  4325       // annotationLayer needs to stay on top
  4326       div.insertBefore(canvasWrapper, this.annotationLayer);
  4327     } else {
  4328       div.appendChild(canvasWrapper);
  4330     this.canvas = canvas;
  4332     var ctx = canvas.getContext('2d');
  4333     var outputScale = getOutputScale(ctx);
  4335     if (USE_ONLY_CSS_ZOOM) {
  4336       var actualSizeViewport = viewport.clone({ scale: CSS_UNITS });
  4337       // Use a scale that will make the canvas be the original intended size
  4338       // of the page.
  4339       outputScale.sx *= actualSizeViewport.width / viewport.width;
  4340       outputScale.sy *= actualSizeViewport.height / viewport.height;
  4341       outputScale.scaled = true;
  4344     canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0;
  4345     canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0;
  4346     canvas.style.width = Math.floor(viewport.width) + 'px';
  4347     canvas.style.height = Math.floor(viewport.height) + 'px';
  4348     // Add the viewport so it's known what it was originally drawn with.
  4349     canvas._viewport = viewport;
  4351     var textLayerDiv = null;
  4352     if (!PDFJS.disableTextLayer) {
  4353       textLayerDiv = document.createElement('div');
  4354       textLayerDiv.className = 'textLayer';
  4355       textLayerDiv.style.width = canvas.style.width;
  4356       textLayerDiv.style.height = canvas.style.height;
  4357       if (this.annotationLayer) {
  4358         // annotationLayer needs to stay on top
  4359         div.insertBefore(textLayerDiv, this.annotationLayer);
  4360       } else {
  4361         div.appendChild(textLayerDiv);
  4364     var textLayer = this.textLayer =
  4365       textLayerDiv ? new TextLayerBuilder({
  4366         textLayerDiv: textLayerDiv,
  4367         pageIndex: this.id - 1,
  4368         lastScrollSource: PDFView,
  4369         viewport: this.viewport,
  4370         isViewerInPresentationMode: PresentationMode.active
  4371       }) : null;
  4372     // TODO(mack): use data attributes to store these
  4373     ctx._scaleX = outputScale.sx;
  4374     ctx._scaleY = outputScale.sy;
  4375     if (outputScale.scaled) {
  4376       ctx.scale(outputScale.sx, outputScale.sy);
  4379     // Rendering area
  4381     var self = this;
  4382     function pageViewDrawCallback(error) {
  4383       // The renderTask may have been replaced by a new one, so only remove the
  4384       // reference to the renderTask if it matches the one that is triggering
  4385       // this callback.
  4386       if (renderTask === self.renderTask) {
  4387         self.renderTask = null;
  4390       if (error === 'cancelled') {
  4391         return;
  4394       self.renderingState = RenderingStates.FINISHED;
  4396       if (self.loadingIconDiv) {
  4397         div.removeChild(self.loadingIconDiv);
  4398         delete self.loadingIconDiv;
  4401       if (self.zoomLayer) {
  4402         div.removeChild(self.zoomLayer);
  4403         self.zoomLayer = null;
  4406       if (self.textLayer && self.textLayer.textDivs &&
  4407           self.textLayer.textDivs.length > 0 &&
  4408           !PDFView.supportsDocumentColors) {
  4409         console.error(mozL10n.get('document_colors_disabled', null,
  4410           'PDF documents are not allowed to use their own colors: ' +
  4411           '\'Allow pages to choose their own colors\' ' +
  4412           'is deactivated in the browser.'));
  4413         PDFView.fallback();
  4415       if (error) {
  4416         PDFView.error(mozL10n.get('rendering_error', null,
  4417           'An error occurred while rendering the page.'), error);
  4420       self.stats = pdfPage.stats;
  4421       self.updateStats();
  4422       if (self.onAfterDraw) {
  4423         self.onAfterDraw();
  4426       cache.push(self);
  4428       var event = document.createEvent('CustomEvent');
  4429       event.initCustomEvent('pagerender', true, true, {
  4430         pageNumber: pdfPage.pageNumber
  4431       });
  4432       div.dispatchEvent(event);
  4434       FirefoxCom.request('reportTelemetry', JSON.stringify({
  4435         type: 'pageInfo'
  4436       }));
  4437       // TODO add stream types report here
  4438       callback();
  4441     var renderContext = {
  4442       canvasContext: ctx,
  4443       viewport: this.viewport,
  4444       textLayer: textLayer,
  4445       // intent: 'default', // === 'display'
  4446       continueCallback: function pdfViewcContinueCallback(cont) {
  4447         if (PDFView.highestPriorityPage !== 'page' + self.id) {
  4448           self.renderingState = RenderingStates.PAUSED;
  4449           self.resume = function resumeCallback() {
  4450             self.renderingState = RenderingStates.RUNNING;
  4451             cont();
  4452           };
  4453           return;
  4455         cont();
  4457     };
  4458     var renderTask = this.renderTask = this.pdfPage.render(renderContext);
  4460     this.renderTask.promise.then(
  4461       function pdfPageRenderCallback() {
  4462         pageViewDrawCallback(null);
  4463         if (textLayer) {
  4464           self.getTextContent().then(
  4465             function textContentResolved(textContent) {
  4466               textLayer.setTextContent(textContent);
  4468           );
  4470       },
  4471       function pdfPageRenderError(error) {
  4472         pageViewDrawCallback(error);
  4474     );
  4476     setupAnnotations(div, pdfPage, this.viewport);
  4477     div.setAttribute('data-loaded', true);
  4478   };
  4480   this.beforePrint = function pageViewBeforePrint() {
  4481     var pdfPage = this.pdfPage;
  4483     var viewport = pdfPage.getViewport(1);
  4484     // Use the same hack we use for high dpi displays for printing to get better
  4485     // output until bug 811002 is fixed in FF.
  4486     var PRINT_OUTPUT_SCALE = 2;
  4487     var canvas = document.createElement('canvas');
  4488     canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE;
  4489     canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE;
  4490     canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt';
  4491     canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt';
  4492     var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' +
  4493                               (1 / PRINT_OUTPUT_SCALE) + ')';
  4494     CustomStyle.setProp('transform' , canvas, cssScale);
  4495     CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
  4497     var printContainer = document.getElementById('printContainer');
  4498     var canvasWrapper = document.createElement('div');
  4499     canvasWrapper.style.width = viewport.width + 'pt';
  4500     canvasWrapper.style.height = viewport.height + 'pt';
  4501     canvasWrapper.appendChild(canvas);
  4502     printContainer.appendChild(canvasWrapper);
  4504     canvas.mozPrintCallback = function(obj) {
  4505       var ctx = obj.context;
  4507       ctx.save();
  4508       ctx.fillStyle = 'rgb(255, 255, 255)';
  4509       ctx.fillRect(0, 0, canvas.width, canvas.height);
  4510       ctx.restore();
  4511       ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE);
  4513       var renderContext = {
  4514         canvasContext: ctx,
  4515         viewport: viewport,
  4516         intent: 'print'
  4517       };
  4519       pdfPage.render(renderContext).promise.then(function() {
  4520         // Tell the printEngine that rendering this canvas/page has finished.
  4521         obj.done();
  4522       }, function(error) {
  4523         console.error(error);
  4524         // Tell the printEngine that rendering this canvas/page has failed.
  4525         // This will make the print proces stop.
  4526         if ('abort' in obj) {
  4527           obj.abort();
  4528         } else {
  4529           obj.done();
  4531       });
  4532     };
  4533   };
  4535   this.updateStats = function pageViewUpdateStats() {
  4536     if (!this.stats) {
  4537       return;
  4540     if (PDFJS.pdfBug && Stats.enabled) {
  4541       var stats = this.stats;
  4542       Stats.add(this.id, stats);
  4544   };
  4545 };
  4548 var ThumbnailView = function thumbnailView(container, id, defaultViewport) {
  4549   var anchor = document.createElement('a');
  4550   anchor.href = PDFView.getAnchorUrl('#page=' + id);
  4551   anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
  4552   anchor.onclick = function stopNavigation() {
  4553     PDFView.page = id;
  4554     return false;
  4555   };
  4557   this.pdfPage = undefined;
  4558   this.viewport = defaultViewport;
  4559   this.pdfPageRotate = defaultViewport.rotation;
  4561   this.rotation = 0;
  4562   this.pageWidth = this.viewport.width;
  4563   this.pageHeight = this.viewport.height;
  4564   this.pageRatio = this.pageWidth / this.pageHeight;
  4565   this.id = id;
  4567   this.canvasWidth = 98;
  4568   this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
  4569   this.scale = (this.canvasWidth / this.pageWidth);
  4571   var div = this.el = document.createElement('div');
  4572   div.id = 'thumbnailContainer' + id;
  4573   div.className = 'thumbnail';
  4575   if (id === 1) {
  4576     // Highlight the thumbnail of the first page when no page number is
  4577     // specified (or exists in cache) when the document is loaded.
  4578     div.classList.add('selected');
  4581   var ring = document.createElement('div');
  4582   ring.className = 'thumbnailSelectionRing';
  4583   ring.style.width = this.canvasWidth + 'px';
  4584   ring.style.height = this.canvasHeight + 'px';
  4586   div.appendChild(ring);
  4587   anchor.appendChild(div);
  4588   container.appendChild(anchor);
  4590   this.hasImage = false;
  4591   this.renderingState = RenderingStates.INITIAL;
  4593   this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) {
  4594     this.pdfPage = pdfPage;
  4595     this.pdfPageRotate = pdfPage.rotate;
  4596     var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
  4597     this.viewport = pdfPage.getViewport(1, totalRotation);
  4598     this.update();
  4599   };
  4601   this.update = function thumbnailViewUpdate(rotation) {
  4602     if (rotation !== undefined) {
  4603       this.rotation = rotation;
  4605     var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
  4606     this.viewport = this.viewport.clone({
  4607       scale: 1,
  4608       rotation: totalRotation
  4609     });
  4610     this.pageWidth = this.viewport.width;
  4611     this.pageHeight = this.viewport.height;
  4612     this.pageRatio = this.pageWidth / this.pageHeight;
  4614     this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
  4615     this.scale = (this.canvasWidth / this.pageWidth);
  4617     div.removeAttribute('data-loaded');
  4618     ring.textContent = '';
  4619     ring.style.width = this.canvasWidth + 'px';
  4620     ring.style.height = this.canvasHeight + 'px';
  4622     this.hasImage = false;
  4623     this.renderingState = RenderingStates.INITIAL;
  4624     this.resume = null;
  4625   };
  4627   this.getPageDrawContext = function thumbnailViewGetPageDrawContext() {
  4628     var canvas = document.createElement('canvas');
  4629     canvas.id = 'thumbnail' + id;
  4631     canvas.width = this.canvasWidth;
  4632     canvas.height = this.canvasHeight;
  4633     canvas.className = 'thumbnailImage';
  4634     canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
  4635       {page: id}, 'Thumbnail of Page {{page}}'));
  4637     div.setAttribute('data-loaded', true);
  4639     ring.appendChild(canvas);
  4641     var ctx = canvas.getContext('2d');
  4642     ctx.save();
  4643     ctx.fillStyle = 'rgb(255, 255, 255)';
  4644     ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
  4645     ctx.restore();
  4646     return ctx;
  4647   };
  4649   this.drawingRequired = function thumbnailViewDrawingRequired() {
  4650     return !this.hasImage;
  4651   };
  4653   this.draw = function thumbnailViewDraw(callback) {
  4654     if (!this.pdfPage) {
  4655       var promise = PDFView.getPage(this.id);
  4656       promise.then(function(pdfPage) {
  4657         this.setPdfPage(pdfPage);
  4658         this.draw(callback);
  4659       }.bind(this));
  4660       return;
  4663     if (this.renderingState !== RenderingStates.INITIAL) {
  4664       console.error('Must be in new state before drawing');
  4667     this.renderingState = RenderingStates.RUNNING;
  4668     if (this.hasImage) {
  4669       callback();
  4670       return;
  4673     var self = this;
  4674     var ctx = this.getPageDrawContext();
  4675     var drawViewport = this.viewport.clone({ scale: this.scale });
  4676     var renderContext = {
  4677       canvasContext: ctx,
  4678       viewport: drawViewport,
  4679       continueCallback: function(cont) {
  4680         if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) {
  4681           self.renderingState = RenderingStates.PAUSED;
  4682           self.resume = function() {
  4683             self.renderingState = RenderingStates.RUNNING;
  4684             cont();
  4685           };
  4686           return;
  4688         cont();
  4690     };
  4691     this.pdfPage.render(renderContext).promise.then(
  4692       function pdfPageRenderCallback() {
  4693         self.renderingState = RenderingStates.FINISHED;
  4694         callback();
  4695       },
  4696       function pdfPageRenderError(error) {
  4697         self.renderingState = RenderingStates.FINISHED;
  4698         callback();
  4700     );
  4701     this.hasImage = true;
  4702   };
  4704   this.setImage = function thumbnailViewSetImage(img) {
  4705     if (!this.pdfPage) {
  4706       var promise = PDFView.getPage(this.id);
  4707       promise.then(function(pdfPage) {
  4708         this.setPdfPage(pdfPage);
  4709         this.setImage(img);
  4710       }.bind(this));
  4711       return;
  4713     if (this.hasImage || !img) {
  4714       return;
  4716     this.renderingState = RenderingStates.FINISHED;
  4717     var ctx = this.getPageDrawContext();
  4718     ctx.drawImage(img, 0, 0, img.width, img.height,
  4719                   0, 0, ctx.canvas.width, ctx.canvas.height);
  4721     this.hasImage = true;
  4722   };
  4723 };
  4726 var FIND_SCROLL_OFFSET_TOP = -50;
  4727 var FIND_SCROLL_OFFSET_LEFT = -400;
  4729 /**
  4730  * TextLayerBuilder provides text-selection
  4731  * functionality for the PDF. It does this
  4732  * by creating overlay divs over the PDF
  4733  * text. This divs contain text that matches
  4734  * the PDF text they are overlaying. This
  4735  * object also provides for a way to highlight
  4736  * text that is being searched for.
  4737  */
  4738 var TextLayerBuilder = function textLayerBuilder(options) {
  4739   var textLayerFrag = document.createDocumentFragment();
  4741   this.textLayerDiv = options.textLayerDiv;
  4742   this.layoutDone = false;
  4743   this.divContentDone = false;
  4744   this.pageIdx = options.pageIndex;
  4745   this.matches = [];
  4746   this.lastScrollSource = options.lastScrollSource;
  4747   this.viewport = options.viewport;
  4748   this.isViewerInPresentationMode = options.isViewerInPresentationMode;
  4749   this.textDivs = [];
  4751   if (typeof PDFFindController === 'undefined') {
  4752     window.PDFFindController = null;
  4755   if (typeof this.lastScrollSource === 'undefined') {
  4756     this.lastScrollSource = null;
  4759   this.renderLayer = function textLayerBuilderRenderLayer() {
  4760     var textDivs = this.textDivs;
  4761     var canvas = document.createElement('canvas');
  4762     var ctx = canvas.getContext('2d');
  4764     // No point in rendering so many divs as it'd make the browser unusable
  4765     // even after the divs are rendered
  4766     var MAX_TEXT_DIVS_TO_RENDER = 100000;
  4767     if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER) {
  4768       return;
  4771     for (var i = 0, ii = textDivs.length; i < ii; i++) {
  4772       var textDiv = textDivs[i];
  4773       if ('isWhitespace' in textDiv.dataset) {
  4774         continue;
  4777       ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily;
  4778       var width = ctx.measureText(textDiv.textContent).width;
  4780       if (width > 0) {
  4781         textLayerFrag.appendChild(textDiv);
  4782         var textScale = textDiv.dataset.canvasWidth / width;
  4783         var rotation = textDiv.dataset.angle;
  4784         var transform = 'scale(' + textScale + ', 1)';
  4785         transform = 'rotate(' + rotation + 'deg) ' + transform;
  4786         CustomStyle.setProp('transform' , textDiv, transform);
  4787         CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
  4791     this.textLayerDiv.appendChild(textLayerFrag);
  4792     this.renderingDone = true;
  4793     this.updateMatches();
  4794   };
  4796   this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() {
  4797     // Schedule renderLayout() if user has been scrolling, otherwise
  4798     // run it right away
  4799     var RENDER_DELAY = 200; // in ms
  4800     var self = this;
  4801     var lastScroll = (this.lastScrollSource === null ?
  4802                       0 : this.lastScrollSource.lastScroll);
  4804     if (Date.now() - lastScroll > RENDER_DELAY) {
  4805       // Render right away
  4806       this.renderLayer();
  4807     } else {
  4808       // Schedule
  4809       if (this.renderTimer) {
  4810         clearTimeout(this.renderTimer);
  4812       this.renderTimer = setTimeout(function() {
  4813         self.setupRenderLayoutTimer();
  4814       }, RENDER_DELAY);
  4816   };
  4818   this.appendText = function textLayerBuilderAppendText(geom, styles) {
  4819     var style = styles[geom.fontName];
  4820     var textDiv = document.createElement('div');
  4821     this.textDivs.push(textDiv);
  4822     if (!/\S/.test(geom.str)) {
  4823       textDiv.dataset.isWhitespace = true;
  4824       return;
  4826     var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform);
  4827     var angle = Math.atan2(tx[1], tx[0]);
  4828     if (style.vertical) {
  4829       angle += Math.PI / 2;
  4831     var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]));
  4832     var fontAscent = (style.ascent ? style.ascent * fontHeight :
  4833       (style.descent ? (1 + style.descent) * fontHeight : fontHeight));
  4835     textDiv.style.position = 'absolute';
  4836     textDiv.style.left = (tx[4] + (fontAscent * Math.sin(angle))) + 'px';
  4837     textDiv.style.top = (tx[5] - (fontAscent * Math.cos(angle))) + 'px';
  4838     textDiv.style.fontSize = fontHeight + 'px';
  4839     textDiv.style.fontFamily = style.fontFamily;
  4841     textDiv.textContent = geom.str;
  4842     textDiv.dataset.fontName = geom.fontName;
  4843     textDiv.dataset.angle = angle * (180 / Math.PI);
  4844     if (style.vertical) {
  4845       textDiv.dataset.canvasWidth = geom.height * this.viewport.scale;
  4846     } else {
  4847       textDiv.dataset.canvasWidth = geom.width * this.viewport.scale;
  4850   };
  4852   this.setTextContent = function textLayerBuilderSetTextContent(textContent) {
  4853     this.textContent = textContent;
  4855     var textItems = textContent.items;
  4856     for (var i = 0; i < textItems.length; i++) {
  4857       this.appendText(textItems[i], textContent.styles);
  4859     this.divContentDone = true;
  4861     this.setupRenderLayoutTimer();
  4862   };
  4864   this.convertMatches = function textLayerBuilderConvertMatches(matches) {
  4865     var i = 0;
  4866     var iIndex = 0;
  4867     var bidiTexts = this.textContent.items;
  4868     var end = bidiTexts.length - 1;
  4869     var queryLen = (PDFFindController === null ?
  4870                     0 : PDFFindController.state.query.length);
  4872     var ret = [];
  4874     // Loop over all the matches.
  4875     for (var m = 0; m < matches.length; m++) {
  4876       var matchIdx = matches[m];
  4877       // # Calculate the begin position.
  4879       // Loop over the divIdxs.
  4880       while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
  4881         iIndex += bidiTexts[i].str.length;
  4882         i++;
  4885       // TODO: Do proper handling here if something goes wrong.
  4886       if (i == bidiTexts.length) {
  4887         console.error('Could not find matching mapping');
  4890       var match = {
  4891         begin: {
  4892           divIdx: i,
  4893           offset: matchIdx - iIndex
  4895       };
  4897       // # Calculate the end position.
  4898       matchIdx += queryLen;
  4900       // Somewhat same array as above, but use a > instead of >= to get the end
  4901       // position right.
  4902       while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
  4903         iIndex += bidiTexts[i].str.length;
  4904         i++;
  4907       match.end = {
  4908         divIdx: i,
  4909         offset: matchIdx - iIndex
  4910       };
  4911       ret.push(match);
  4914     return ret;
  4915   };
  4917   this.renderMatches = function textLayerBuilder_renderMatches(matches) {
  4918     // Early exit if there is nothing to render.
  4919     if (matches.length === 0) {
  4920       return;
  4923     var bidiTexts = this.textContent.items;
  4924     var textDivs = this.textDivs;
  4925     var prevEnd = null;
  4926     var isSelectedPage = (PDFFindController === null ?
  4927       false : (this.pageIdx === PDFFindController.selected.pageIdx));
  4929     var selectedMatchIdx = (PDFFindController === null ?
  4930                             -1 : PDFFindController.selected.matchIdx);
  4932     var highlightAll = (PDFFindController === null ?
  4933                         false : PDFFindController.state.highlightAll);
  4935     var infty = {
  4936       divIdx: -1,
  4937       offset: undefined
  4938     };
  4940     function beginText(begin, className) {
  4941       var divIdx = begin.divIdx;
  4942       var div = textDivs[divIdx];
  4943       div.textContent = '';
  4944       appendTextToDiv(divIdx, 0, begin.offset, className);
  4947     function appendText(from, to, className) {
  4948       appendTextToDiv(from.divIdx, from.offset, to.offset, className);
  4951     function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
  4952       var div = textDivs[divIdx];
  4954       var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
  4955       var node = document.createTextNode(content);
  4956       if (className) {
  4957         var span = document.createElement('span');
  4958         span.className = className;
  4959         span.appendChild(node);
  4960         div.appendChild(span);
  4961         return;
  4963       div.appendChild(node);
  4966     function highlightDiv(divIdx, className) {
  4967       textDivs[divIdx].className = className;
  4970     var i0 = selectedMatchIdx, i1 = i0 + 1, i;
  4972     if (highlightAll) {
  4973       i0 = 0;
  4974       i1 = matches.length;
  4975     } else if (!isSelectedPage) {
  4976       // Not highlighting all and this isn't the selected page, so do nothing.
  4977       return;
  4980     for (i = i0; i < i1; i++) {
  4981       var match = matches[i];
  4982       var begin = match.begin;
  4983       var end = match.end;
  4985       var isSelected = isSelectedPage && i === selectedMatchIdx;
  4986       var highlightSuffix = (isSelected ? ' selected' : '');
  4987       if (isSelected && !this.isViewerInPresentationMode) {
  4988         scrollIntoView(textDivs[begin.divIdx], { top: FIND_SCROLL_OFFSET_TOP,
  4989                                                left: FIND_SCROLL_OFFSET_LEFT });
  4992       // Match inside new div.
  4993       if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
  4994         // If there was a previous div, then add the text at the end
  4995         if (prevEnd !== null) {
  4996           appendText(prevEnd, infty);
  4998         // clears the divs and set the content until the begin point.
  4999         beginText(begin);
  5000       } else {
  5001         appendText(prevEnd, begin);
  5004       if (begin.divIdx === end.divIdx) {
  5005         appendText(begin, end, 'highlight' + highlightSuffix);
  5006       } else {
  5007         appendText(begin, infty, 'highlight begin' + highlightSuffix);
  5008         for (var n = begin.divIdx + 1; n < end.divIdx; n++) {
  5009           highlightDiv(n, 'highlight middle' + highlightSuffix);
  5011         beginText(end, 'highlight end' + highlightSuffix);
  5013       prevEnd = end;
  5016     if (prevEnd) {
  5017       appendText(prevEnd, infty);
  5019   };
  5021   this.updateMatches = function textLayerUpdateMatches() {
  5022     // Only show matches, once all rendering is done.
  5023     if (!this.renderingDone) {
  5024       return;
  5027     // Clear out all matches.
  5028     var matches = this.matches;
  5029     var textDivs = this.textDivs;
  5030     var bidiTexts = this.textContent.items;
  5031     var clearedUntilDivIdx = -1;
  5033     // Clear out all current matches.
  5034     for (var i = 0; i < matches.length; i++) {
  5035       var match = matches[i];
  5036       var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
  5037       for (var n = begin; n <= match.end.divIdx; n++) {
  5038         var div = textDivs[n];
  5039         div.textContent = bidiTexts[n].str;
  5040         div.className = '';
  5042       clearedUntilDivIdx = match.end.divIdx + 1;
  5045     if (PDFFindController === null || !PDFFindController.active) {
  5046       return;
  5049     // Convert the matches on the page controller into the match format used
  5050     // for the textLayer.
  5051     this.matches = matches = (this.convertMatches(PDFFindController === null ?
  5052       [] : (PDFFindController.pageMatches[this.pageIdx] || [])));
  5054     this.renderMatches(this.matches);
  5055   };
  5056 };
  5060 var DocumentOutlineView = function documentOutlineView(outline) {
  5061   var outlineView = document.getElementById('outlineView');
  5062   while (outlineView.firstChild) {
  5063     outlineView.removeChild(outlineView.firstChild);
  5066   if (!outline) {
  5067     if (!outlineView.classList.contains('hidden')) {
  5068       PDFView.switchSidebarView('thumbs');
  5070     return;
  5073   function bindItemLink(domObj, item) {
  5074     domObj.href = PDFView.getDestinationHash(item.dest);
  5075     domObj.onclick = function documentOutlineViewOnclick(e) {
  5076       PDFView.navigateTo(item.dest);
  5077       return false;
  5078     };
  5082   var queue = [{parent: outlineView, items: outline}];
  5083   while (queue.length > 0) {
  5084     var levelData = queue.shift();
  5085     var i, n = levelData.items.length;
  5086     for (i = 0; i < n; i++) {
  5087       var item = levelData.items[i];
  5088       var div = document.createElement('div');
  5089       div.className = 'outlineItem';
  5090       var a = document.createElement('a');
  5091       bindItemLink(a, item);
  5092       a.textContent = item.title;
  5093       div.appendChild(a);
  5095       if (item.items.length > 0) {
  5096         var itemsDiv = document.createElement('div');
  5097         itemsDiv.className = 'outlineItems';
  5098         div.appendChild(itemsDiv);
  5099         queue.push({parent: itemsDiv, items: item.items});
  5102       levelData.parent.appendChild(div);
  5105 };
  5107 var DocumentAttachmentsView = function documentAttachmentsView(attachments) {
  5108   var attachmentsView = document.getElementById('attachmentsView');
  5109   while (attachmentsView.firstChild) {
  5110     attachmentsView.removeChild(attachmentsView.firstChild);
  5113   if (!attachments) {
  5114     if (!attachmentsView.classList.contains('hidden')) {
  5115       PDFView.switchSidebarView('thumbs');
  5117     return;
  5120   function bindItemLink(domObj, item) {
  5121     domObj.href = '#';
  5122     domObj.onclick = function documentAttachmentsViewOnclick(e) {
  5123       var downloadManager = new DownloadManager();
  5124       downloadManager.downloadData(item.content, getFileName(item.filename),
  5125                                    '');
  5126       return false;
  5127     };
  5130   var names = Object.keys(attachments).sort(function(a,b) {
  5131     return a.toLowerCase().localeCompare(b.toLowerCase());
  5132   });
  5133   for (var i = 0, ii = names.length; i < ii; i++) {
  5134     var item = attachments[names[i]];
  5135     var div = document.createElement('div');
  5136     div.className = 'attachmentsItem';
  5137     var a = document.createElement('a');
  5138     bindItemLink(a, item);
  5139     a.textContent = getFileName(item.filename);
  5140     div.appendChild(a);
  5141     attachmentsView.appendChild(div);
  5143 };
  5146 function webViewerLoad(evt) {
  5147   PDFView.initialize().then(webViewerInitialized);
  5150 function webViewerInitialized() {
  5151   var file = window.location.href.split('#')[0];
  5153   document.getElementById('openFile').setAttribute('hidden', 'true');
  5154   document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true');
  5156   // Special debugging flags in the hash section of the URL.
  5157   var hash = document.location.hash.substring(1);
  5158   var hashParams = PDFView.parseQueryString(hash);
  5160   if ('disableWorker' in hashParams) {
  5161     PDFJS.disableWorker = (hashParams['disableWorker'] === 'true');
  5164   if ('disableRange' in hashParams) {
  5165     PDFJS.disableRange = (hashParams['disableRange'] === 'true');
  5168   if ('disableAutoFetch' in hashParams) {
  5169     PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true');
  5172   if ('disableFontFace' in hashParams) {
  5173     PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true');
  5176   if ('disableHistory' in hashParams) {
  5177     PDFJS.disableHistory = (hashParams['disableHistory'] === 'true');
  5180   if ('webgl' in hashParams) {
  5181     PDFJS.disableWebGL = (hashParams['webgl'] !== 'true');
  5184   if ('useOnlyCssZoom' in hashParams) {
  5185     USE_ONLY_CSS_ZOOM = (hashParams['useOnlyCssZoom'] === 'true');
  5188   if ('verbosity' in hashParams) {
  5189     PDFJS.verbosity = hashParams['verbosity'] | 0;
  5192   if ('ignoreCurrentPositionOnZoom' in hashParams) {
  5193     IGNORE_CURRENT_POSITION_ON_ZOOM =
  5194       (hashParams['ignoreCurrentPositionOnZoom'] === 'true');
  5199   if (!PDFView.supportsDocumentFonts) {
  5200     PDFJS.disableFontFace = true;
  5201     console.warn(mozL10n.get('web_fonts_disabled', null,
  5202       'Web fonts are disabled: unable to use embedded PDF fonts.'));
  5205   if ('textLayer' in hashParams) {
  5206     switch (hashParams['textLayer']) {
  5207       case 'off':
  5208         PDFJS.disableTextLayer = true;
  5209         break;
  5210       case 'visible':
  5211       case 'shadow':
  5212       case 'hover':
  5213         var viewer = document.getElementById('viewer');
  5214         viewer.classList.add('textLayer-' + hashParams['textLayer']);
  5215         break;
  5219   if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) {
  5220     PDFJS.pdfBug = true;
  5221     var pdfBug = hashParams['pdfBug'];
  5222     var enabled = pdfBug.split(',');
  5223     PDFBug.enable(enabled);
  5224     PDFBug.init();
  5227   if (!PDFView.supportsPrinting) {
  5228     document.getElementById('print').classList.add('hidden');
  5229     document.getElementById('secondaryPrint').classList.add('hidden');
  5232   if (!PDFView.supportsFullscreen) {
  5233     document.getElementById('presentationMode').classList.add('hidden');
  5234     document.getElementById('secondaryPresentationMode').
  5235       classList.add('hidden');
  5238   if (PDFView.supportsIntegratedFind) {
  5239     document.getElementById('viewFind').classList.add('hidden');
  5242   // Listen for unsuporrted features to trigger the fallback UI.
  5243   PDFJS.UnsupportedManager.listen(PDFView.fallback.bind(PDFView));
  5245   // Suppress context menus for some controls
  5246   document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler;
  5248   var mainContainer = document.getElementById('mainContainer');
  5249   var outerContainer = document.getElementById('outerContainer');
  5250   mainContainer.addEventListener('transitionend', function(e) {
  5251     if (e.target == mainContainer) {
  5252       var event = document.createEvent('UIEvents');
  5253       event.initUIEvent('resize', false, false, window, 0);
  5254       window.dispatchEvent(event);
  5255       outerContainer.classList.remove('sidebarMoving');
  5257   }, true);
  5259   document.getElementById('sidebarToggle').addEventListener('click',
  5260     function() {
  5261       this.classList.toggle('toggled');
  5262       outerContainer.classList.add('sidebarMoving');
  5263       outerContainer.classList.toggle('sidebarOpen');
  5264       PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen');
  5265       PDFView.renderHighestPriority();
  5266     });
  5268   document.getElementById('viewThumbnail').addEventListener('click',
  5269     function() {
  5270       PDFView.switchSidebarView('thumbs');
  5271     });
  5273   document.getElementById('viewOutline').addEventListener('click',
  5274     function() {
  5275       PDFView.switchSidebarView('outline');
  5276     });
  5278   document.getElementById('viewAttachments').addEventListener('click',
  5279     function() {
  5280       PDFView.switchSidebarView('attachments');
  5281     });
  5283   document.getElementById('previous').addEventListener('click',
  5284     function() {
  5285       PDFView.page--;
  5286     });
  5288   document.getElementById('next').addEventListener('click',
  5289     function() {
  5290       PDFView.page++;
  5291     });
  5293   document.getElementById('zoomIn').addEventListener('click',
  5294     function() {
  5295       PDFView.zoomIn();
  5296     });
  5298   document.getElementById('zoomOut').addEventListener('click',
  5299     function() {
  5300       PDFView.zoomOut();
  5301     });
  5303   document.getElementById('pageNumber').addEventListener('click',
  5304     function() {
  5305       this.select();
  5306     });
  5308   document.getElementById('pageNumber').addEventListener('change',
  5309     function() {
  5310       // Handle the user inputting a floating point number.
  5311       PDFView.page = (this.value | 0);
  5313       if (this.value !== (this.value | 0).toString()) {
  5314         this.value = PDFView.page;
  5316     });
  5318   document.getElementById('scaleSelect').addEventListener('change',
  5319     function() {
  5320       PDFView.setScale(this.value);
  5321     });
  5323   document.getElementById('presentationMode').addEventListener('click',
  5324     SecondaryToolbar.presentationModeClick.bind(SecondaryToolbar));
  5326   document.getElementById('openFile').addEventListener('click',
  5327     SecondaryToolbar.openFileClick.bind(SecondaryToolbar));
  5329   document.getElementById('print').addEventListener('click',
  5330     SecondaryToolbar.printClick.bind(SecondaryToolbar));
  5332   document.getElementById('download').addEventListener('click',
  5333     SecondaryToolbar.downloadClick.bind(SecondaryToolbar));
  5335   PDFView.setTitleUsingUrl(file);
  5336   PDFView.initPassiveLoading();
  5337   return;
  5339   if (file) {
  5340     PDFView.open(file, 0);
  5344 document.addEventListener('DOMContentLoaded', webViewerLoad, true);
  5346 function updateViewarea() {
  5348   if (!PDFView.initialized) {
  5349     return;
  5351   var visible = PDFView.getVisiblePages();
  5352   var visiblePages = visible.views;
  5353   if (visiblePages.length === 0) {
  5354     return;
  5357   PDFView.renderHighestPriority();
  5359   var currentId = PDFView.page;
  5360   var firstPage = visible.first;
  5362   for (var i = 0, ii = visiblePages.length, stillFullyVisible = false;
  5363        i < ii; ++i) {
  5364     var page = visiblePages[i];
  5366     if (page.percent < 100) {
  5367       break;
  5369     if (page.id === PDFView.page) {
  5370       stillFullyVisible = true;
  5371       break;
  5375   if (!stillFullyVisible) {
  5376     currentId = visiblePages[0].id;
  5379   if (!PresentationMode.active) {
  5380     updateViewarea.inProgress = true; // used in "set page"
  5381     PDFView.page = currentId;
  5382     updateViewarea.inProgress = false;
  5385   var currentScale = PDFView.currentScale;
  5386   var currentScaleValue = PDFView.currentScaleValue;
  5387   var normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ?
  5388     Math.round(currentScale * 10000) / 100 : currentScaleValue;
  5390   var pageNumber = firstPage.id;
  5391   var pdfOpenParams = '#page=' + pageNumber;
  5392   pdfOpenParams += '&zoom=' + normalizedScaleValue;
  5393   var currentPage = PDFView.pages[pageNumber - 1];
  5394   var container = PDFView.container;
  5395   var topLeft = currentPage.getPagePoint((container.scrollLeft - firstPage.x),
  5396                                          (container.scrollTop - firstPage.y));
  5397   var intLeft = Math.round(topLeft[0]);
  5398   var intTop = Math.round(topLeft[1]);
  5399   pdfOpenParams += ',' + intLeft + ',' + intTop;
  5401   if (PresentationMode.active || PresentationMode.switchInProgress) {
  5402     PDFView.currentPosition = null;
  5403   } else {
  5404     PDFView.currentPosition = { page: pageNumber, left: intLeft, top: intTop };
  5407   var store = PDFView.store;
  5408   store.initializedPromise.then(function() {
  5409     store.set('exists', true);
  5410     store.set('page', pageNumber);
  5411     store.set('zoom', normalizedScaleValue);
  5412     store.set('scrollLeft', intLeft);
  5413     store.set('scrollTop', intTop);
  5414   });
  5415   var href = PDFView.getAnchorUrl(pdfOpenParams);
  5416   document.getElementById('viewBookmark').href = href;
  5417   document.getElementById('secondaryViewBookmark').href = href;
  5419   // Update the current bookmark in the browsing history.
  5420   PDFHistory.updateCurrentBookmark(pdfOpenParams, pageNumber);
  5423 window.addEventListener('resize', function webViewerResize(evt) {
  5424   if (PDFView.initialized &&
  5425       (document.getElementById('pageWidthOption').selected ||
  5426        document.getElementById('pageFitOption').selected ||
  5427        document.getElementById('pageAutoOption').selected)) {
  5428     PDFView.setScale(document.getElementById('scaleSelect').value);
  5430   updateViewarea();
  5432   // Set the 'max-height' CSS property of the secondary toolbar.
  5433   SecondaryToolbar.setMaxHeight(PDFView.container);
  5434 });
  5436 window.addEventListener('hashchange', function webViewerHashchange(evt) {
  5437   if (PDFHistory.isHashChangeUnlocked) {
  5438     PDFView.setHash(document.location.hash.substring(1));
  5440 });
  5443 function selectScaleOption(value) {
  5444   var options = document.getElementById('scaleSelect').options;
  5445   var predefinedValueFound = false;
  5446   for (var i = 0; i < options.length; i++) {
  5447     var option = options[i];
  5448     if (option.value != value) {
  5449       option.selected = false;
  5450       continue;
  5452     option.selected = true;
  5453     predefinedValueFound = true;
  5455   return predefinedValueFound;
  5458 window.addEventListener('localized', function localized(evt) {
  5459   document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
  5461   PDFView.animationStartedPromise.then(function() {
  5462     // Adjust the width of the zoom box to fit the content.
  5463     // Note: This is only done if the zoom box is actually visible,
  5464     // since otherwise element.clientWidth will return 0.
  5465     var container = document.getElementById('scaleSelectContainer');
  5466     if (container.clientWidth > 0) {
  5467       var select = document.getElementById('scaleSelect');
  5468       select.setAttribute('style', 'min-width: inherit;');
  5469       var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING;
  5470       select.setAttribute('style', 'min-width: ' +
  5471                                    (width + SCALE_SELECT_PADDING) + 'px;');
  5472       container.setAttribute('style', 'min-width: ' + width + 'px; ' +
  5473                                       'max-width: ' + width + 'px;');
  5476     // Set the 'max-height' CSS property of the secondary toolbar.
  5477     SecondaryToolbar.setMaxHeight(PDFView.container);
  5478   });
  5479 }, true);
  5481 window.addEventListener('scalechange', function scalechange(evt) {
  5482   document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE);
  5483   document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE);
  5485   var customScaleOption = document.getElementById('customScaleOption');
  5486   customScaleOption.selected = false;
  5488   if (!evt.resetAutoSettings &&
  5489       (document.getElementById('pageWidthOption').selected ||
  5490        document.getElementById('pageFitOption').selected ||
  5491        document.getElementById('pageAutoOption').selected)) {
  5492     updateViewarea();
  5493     return;
  5496   var predefinedValueFound = selectScaleOption('' + evt.scale);
  5497   if (!predefinedValueFound) {
  5498     customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
  5499     customScaleOption.selected = true;
  5501   updateViewarea();
  5502 }, true);
  5504 window.addEventListener('pagechange', function pagechange(evt) {
  5505   var page = evt.pageNumber;
  5506   if (PDFView.previousPageNumber !== page) {
  5507     document.getElementById('pageNumber').value = page;
  5508     var selected = document.querySelector('.thumbnail.selected');
  5509     if (selected) {
  5510       selected.classList.remove('selected');
  5512     var thumbnail = document.getElementById('thumbnailContainer' + page);
  5513     thumbnail.classList.add('selected');
  5514     var visibleThumbs = PDFView.getVisibleThumbs();
  5515     var numVisibleThumbs = visibleThumbs.views.length;
  5517     // If the thumbnail isn't currently visible, scroll it into view.
  5518     if (numVisibleThumbs > 0) {
  5519       var first = visibleThumbs.first.id;
  5520       // Account for only one thumbnail being visible.
  5521       var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first);
  5522       if (page <= first || page >= last) {
  5523         scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN });
  5527   document.getElementById('previous').disabled = (page <= 1);
  5528   document.getElementById('next').disabled = (page >= PDFView.pages.length);
  5529 }, true);
  5531 function handleMouseWheel(evt) {
  5532   var MOUSE_WHEEL_DELTA_FACTOR = 40;
  5533   var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail :
  5534               evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR;
  5535   var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn';
  5537   if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer
  5538     evt.preventDefault();
  5539     PDFView[direction](Math.abs(ticks));
  5540   } else if (PresentationMode.active) {
  5541     PDFView.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR);
  5545 window.addEventListener('DOMMouseScroll', handleMouseWheel);
  5546 window.addEventListener('mousewheel', handleMouseWheel);
  5548 window.addEventListener('click', function click(evt) {
  5549   if (!PresentationMode.active) {
  5550     if (SecondaryToolbar.opened && PDFView.container.contains(evt.target)) {
  5551       SecondaryToolbar.close();
  5553   } else if (evt.button === 0) {
  5554     // Necessary since preventDefault() in 'mousedown' won't stop
  5555     // the event propagation in all circumstances in presentation mode.
  5556     evt.preventDefault();
  5558 }, false);
  5560 window.addEventListener('keydown', function keydown(evt) {
  5561   if (PasswordPrompt.visible) {
  5562     return;
  5565   var handled = false;
  5566   var cmd = (evt.ctrlKey ? 1 : 0) |
  5567             (evt.altKey ? 2 : 0) |
  5568             (evt.shiftKey ? 4 : 0) |
  5569             (evt.metaKey ? 8 : 0);
  5571   // First, handle the key bindings that are independent whether an input
  5572   // control is selected or not.
  5573   if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
  5574     // either CTRL or META key with optional SHIFT.
  5575     switch (evt.keyCode) {
  5576       case 70: // f
  5577         if (!PDFView.supportsIntegratedFind) {
  5578           PDFFindBar.open();
  5579           handled = true;
  5581         break;
  5582       case 71: // g
  5583         if (!PDFView.supportsIntegratedFind) {
  5584           PDFFindBar.dispatchEvent('again', cmd === 5 || cmd === 12);
  5585           handled = true;
  5587         break;
  5588       case 61: // FF/Mac '='
  5589       case 107: // FF '+' and '='
  5590       case 187: // Chrome '+'
  5591       case 171: // FF with German keyboard
  5592         PDFView.zoomIn();
  5593         handled = true;
  5594         break;
  5595       case 173: // FF/Mac '-'
  5596       case 109: // FF '-'
  5597       case 189: // Chrome '-'
  5598         PDFView.zoomOut();
  5599         handled = true;
  5600         break;
  5601       case 48: // '0'
  5602       case 96: // '0' on Numpad of Swedish keyboard
  5603         // keeping it unhandled (to restore page zoom to 100%)
  5604         setTimeout(function () {
  5605           // ... and resetting the scale after browser adjusts its scale
  5606           PDFView.setScale(DEFAULT_SCALE, true);
  5607         });
  5608         handled = false;
  5609         break;
  5614   // CTRL+ALT or Option+Command
  5615   if (cmd === 3 || cmd === 10) {
  5616     switch (evt.keyCode) {
  5617       case 80: // p
  5618         SecondaryToolbar.presentationModeClick();
  5619         handled = true;
  5620         break;
  5621       case 71: // g
  5622         // focuses input#pageNumber field
  5623         document.getElementById('pageNumber').select();
  5624         handled = true;
  5625         break;
  5629   if (handled) {
  5630     evt.preventDefault();
  5631     return;
  5634   // Some shortcuts should not get handled if a control/input element
  5635   // is selected.
  5636   var curElement = document.activeElement || document.querySelector(':focus');
  5637   var curElementTagName = curElement && curElement.tagName.toUpperCase();
  5638   if (curElementTagName === 'INPUT' ||
  5639       curElementTagName === 'TEXTAREA' ||
  5640       curElementTagName === 'SELECT') {
  5641     // Make sure that the secondary toolbar is closed when Escape is pressed.
  5642     if (evt.keyCode !== 27) { // 'Esc'
  5643       return;
  5647   if (cmd === 0) { // no control key pressed at all.
  5648     switch (evt.keyCode) {
  5649       case 38: // up arrow
  5650       case 33: // pg up
  5651       case 8: // backspace
  5652         if (!PresentationMode.active &&
  5653             PDFView.currentScaleValue !== 'page-fit') {
  5654           break;
  5656         /* in presentation mode */
  5657         /* falls through */
  5658       case 37: // left arrow
  5659         // horizontal scrolling using arrow keys
  5660         if (PDFView.isHorizontalScrollbarEnabled) {
  5661           break;
  5663         /* falls through */
  5664       case 75: // 'k'
  5665       case 80: // 'p'
  5666         PDFView.page--;
  5667         handled = true;
  5668         break;
  5669       case 27: // esc key
  5670         if (SecondaryToolbar.opened) {
  5671           SecondaryToolbar.close();
  5672           handled = true;
  5674         if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) {
  5675           PDFFindBar.close();
  5676           handled = true;
  5678         break;
  5679       case 40: // down arrow
  5680       case 34: // pg down
  5681       case 32: // spacebar
  5682         if (!PresentationMode.active &&
  5683             PDFView.currentScaleValue !== 'page-fit') {
  5684           break;
  5686         /* falls through */
  5687       case 39: // right arrow
  5688         // horizontal scrolling using arrow keys
  5689         if (PDFView.isHorizontalScrollbarEnabled) {
  5690           break;
  5692         /* falls through */
  5693       case 74: // 'j'
  5694       case 78: // 'n'
  5695         PDFView.page++;
  5696         handled = true;
  5697         break;
  5699       case 36: // home
  5700         if (PresentationMode.active) {
  5701           PDFView.page = 1;
  5702           handled = true;
  5704         break;
  5705       case 35: // end
  5706         if (PresentationMode.active) {
  5707           PDFView.page = PDFView.pdfDocument.numPages;
  5708           handled = true;
  5710         break;
  5712       case 72: // 'h'
  5713         if (!PresentationMode.active) {
  5714           HandTool.toggle();
  5716         break;
  5717       case 82: // 'r'
  5718         PDFView.rotatePages(90);
  5719         break;
  5723   if (cmd === 4) { // shift-key
  5724     switch (evt.keyCode) {
  5725       case 32: // spacebar
  5726         if (!PresentationMode.active &&
  5727             PDFView.currentScaleValue !== 'page-fit') {
  5728           break;
  5730         PDFView.page--;
  5731         handled = true;
  5732         break;
  5734       case 82: // 'r'
  5735         PDFView.rotatePages(-90);
  5736         break;
  5740   if (!handled && !PresentationMode.active) {
  5741     // 33=Page Up  34=Page Down  35=End    36=Home
  5742     // 37=Left     38=Up         39=Right  40=Down
  5743     if (evt.keyCode >= 33 && evt.keyCode <= 40 &&
  5744         !PDFView.container.contains(curElement)) {
  5745       // The page container is not focused, but a page navigation key has been
  5746       // pressed. Change the focus to the viewer container to make sure that
  5747       // navigation by keyboard works as expected.
  5748       PDFView.container.focus();
  5750     // 32=Spacebar
  5751     if (evt.keyCode === 32 && curElementTagName !== 'BUTTON') {
  5752       // Workaround for issue in Firefox, that prevents scroll keys from
  5753       // working when elements with 'tabindex' are focused. (#3498)
  5754       PDFView.container.blur();
  5758   if (cmd === 2) { // alt-key
  5759     switch (evt.keyCode) {
  5760       case 37: // left arrow
  5761         if (PresentationMode.active) {
  5762           PDFHistory.back();
  5763           handled = true;
  5765         break;
  5766       case 39: // right arrow
  5767         if (PresentationMode.active) {
  5768           PDFHistory.forward();
  5769           handled = true;
  5771         break;
  5775   if (handled) {
  5776     evt.preventDefault();
  5777     PDFView.clearMouseScrollState();
  5779 });
  5781 window.addEventListener('beforeprint', function beforePrint(evt) {
  5782   PDFView.beforePrint();
  5783 });
  5785 window.addEventListener('afterprint', function afterPrint(evt) {
  5786   PDFView.afterPrint();
  5787 });
  5789 (function animationStartedClosure() {
  5790   // The offsetParent is not set until the pdf.js iframe or object is visible.
  5791   // Waiting for first animation.
  5792   var requestAnimationFrame = window.requestAnimationFrame ||
  5793                               window.mozRequestAnimationFrame ||
  5794                               window.webkitRequestAnimationFrame ||
  5795                               window.oRequestAnimationFrame ||
  5796                               window.msRequestAnimationFrame ||
  5797                               function startAtOnce(callback) { callback(); };
  5798   PDFView.animationStartedPromise = new Promise(function (resolve) {
  5799     requestAnimationFrame(function onAnimationFrame() {
  5800       resolve();
  5801     });
  5802   });
  5803 })();

mercurial