Wed, 31 Dec 2014 06:09:35 +0100
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();
1003 }
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 });
1035 }
1036 }
1037 }
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;
1043 }
1045 // If we're waiting on a page, we return since we can't do anything else.
1046 if (this.resumePageIdx) {
1047 return;
1048 }
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;
1063 }
1064 // We went beyond the current page's matches, so we advance to the next
1065 // page.
1066 this.advanceOffsetPage(previous);
1067 }
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;
1094 }
1095 }
1096 // matches were not found (and searching is not done)
1097 return false;
1098 }
1099 },
1101 nextPageMatch: function() {
1102 if (this.resumePageIdx !== null) {
1103 console.error('There can only be one pending page.');
1104 }
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;
1113 }
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;
1126 }
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);
1141 }
1142 }
1143 this.updateUIState(state, this.state.findPrevious);
1144 if (this.selected.pageIdx !== -1) {
1145 this.updatePage(this.selected.pageIdx, true);
1146 }
1147 },
1149 updateUIState: function(state, previous) {
1150 if (this.integratedFind) {
1151 FirefoxCom.request('updateFindControlState',
1152 {result: state, findPrevious: previous});
1153 return;
1154 }
1155 PDFFindBar.updateUIState(state, previous);
1156 }
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;
1170 }
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;
1196 }
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;
1207 }
1208 this._pushOrReplaceState({ fingerprint: this.fingerprint }, true);
1209 }
1211 var self = this;
1212 window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
1213 evt.preventDefault();
1214 evt.stopPropagation();
1216 if (!self.historyUnlocked) {
1217 return;
1218 }
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;
1239 }
1240 self._pushToHistory({ hash: self.previousHash }, false, true);
1241 self._updatePreviousBookmark();
1242 }
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();
1252 }
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);
1256 }
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, '');
1279 }
1280 },
1282 get isHashChangeUnlocked() {
1283 if (!this.initialized) {
1284 return true;
1285 }
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;
1307 }
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();
1316 }
1317 },
1319 updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
1320 if (this.initialized) {
1321 this.nextHashParam = param;
1322 }
1323 },
1325 push: function pdfHistoryPush(params, isInitialBookmark) {
1326 if (!(this.initialized && this.historyUnlocked)) {
1327 return;
1328 }
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];
1334 }
1335 if (params.page) {
1336 params.page |= 0;
1337 }
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);
1345 }
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();
1351 }
1352 return;
1353 }
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;
1361 }
1362 }
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);
1371 }
1372 this.updatePreviousBookmark = true;
1373 }
1374 } else {
1375 this._pushToHistory(params, true);
1376 }
1377 } else if (this.current.page && params.page &&
1378 this.current.page !== params.page) {
1379 this._pushToHistory(params, true);
1380 }
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;
1389 }
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;
1403 }
1404 if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
1405 if (this.previousBookmark === this.currentBookmark) {
1406 return null;
1407 }
1408 } else if (this.current.page || onlyCheckPage) {
1409 if (this.previousPage === this.currentPage) {
1410 return null;
1411 }
1412 } else {
1413 return null;
1414 }
1415 var params = { hash: this.currentBookmark, page: this.currentPage };
1416 if (PresentationMode.active) {
1417 params.hash = null;
1418 }
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;
1430 }
1431 if (!params.hash && params.page) {
1432 params.hash = ('page=' + params.page);
1433 }
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);
1440 }
1441 }
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;
1453 }
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;
1462 }
1463 }
1464 this.historyUnlocked = false;
1466 if (state.target.dest) {
1467 PDFView.navigateTo(state.target.dest);
1468 } else {
1469 PDFView.setHash(state.target.hash);
1470 }
1471 this.currentUid = state.uid;
1472 if (state.uid > this.uid) {
1473 this.uid = state.uid;
1474 }
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;
1481 }
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();
1502 }
1503 }
1504 }
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));
1556 }
1557 }
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;
1612 }
1613 this.newContainerHeight = container.clientHeight;
1614 if (this.previousContainerHeight === this.newContainerHeight) {
1615 return;
1616 }
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;
1625 }
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;
1636 }
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();
1647 }
1648 }
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();
1679 }
1680 }.bind(this));
1682 window.addEventListener('keydown',
1683 function (e) {
1684 if (e.keyCode === 27) { // Esc key
1685 this.hide();
1686 }
1687 }.bind(this));
1688 },
1690 show: function passwordPromptShow() {
1691 if (this.visible) {
1692 return;
1693 }
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.');
1705 }
1707 this.passwordText.textContent = promptString;
1708 },
1710 hide: function passwordPromptClose() {
1711 if (!this.visible) {
1712 return;
1713 }
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);
1725 }
1726 }
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);
1787 }
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;
1799 }
1800 },
1802 request: function presentationModeRequest() {
1803 if (!PDFView.supportsFullscreen || this.isFullscreen ||
1804 !this.viewer.hasChildNodes()) {
1805 return false;
1806 }
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;
1819 }
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);
1884 }
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;
1894 }
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;
1910 }
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);
1921 }
1922 }
1923 },
1925 contextMenu: function presentationModeContextMenu(evt) {
1926 PresentationMode.contextMenuOpen = true;
1927 }
1928 };
1930 (function presentationModeClosure() {
1931 function presentationModeChange(e) {
1932 if (PresentationMode.isFullscreen) {
1933 PresentationMode.enter();
1934 } else {
1935 PresentationMode.exit();
1936 }
1937 }
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
1949 *
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
1953 *
1954 * http://www.apache.org/licenses/LICENSE-2.0
1955 *
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;
1979 }
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';
1995 }
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);
2012 }
2013 }
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);
2027 }
2028 }
2029 },
2031 toggle: function GrabToPan_toggle() {
2032 if (this.active) {
2033 this.deactivate();
2034 } else {
2035 this.activate();
2036 }
2037 },
2039 /**
2040 * Whether to not pan if the target element is clicked.
2041 * Override this method to change the default behaviour.
2042 *
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;
2060 }
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;
2068 }
2069 }
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;
2094 }
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);
2101 }
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);
2113 }
2114 }
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;
2123 }
2124 name += 'Selector';
2125 if (name in document.documentElement) {
2126 matchesSelector = name;
2127 }
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);
2152 }
2153 if (isChrome15OrOpera15plus || isSafari6plus) {
2154 // Chrome 14+
2155 // Opera 15+
2156 // Safari 6.0+
2157 return event.which === 0;
2158 }
2159 }
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;
2172 }
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');
2183 }
2184 }
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();
2193 }
2194 }.bind(this));
2195 }.bind(this));
2196 }
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();
2208 }
2209 },
2211 exitPresentationMode: function handToolExitPresentationMode() {
2212 if (this.wasActive) {
2213 this.wasActive = null;
2214 this.handTool.activate();
2215 }
2216 }
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));
2260 }
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();
2271 }
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;
2280 }
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);
2313 }
2314 }.bind(this));
2315 },
2317 updateUI: function documentPropertiesUpdateUI(field, content) {
2318 if (field && content !== undefined && content !== '') {
2319 field.textContent = content;
2320 }
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)');
2335 }
2336 },
2338 show: function documentPropertiesShow() {
2339 if (this.visible) {
2340 return;
2341 }
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;
2354 }
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 '';
2369 }
2371 // Remove the D: prefix if it is available.
2372 if (dateToParse.substring(0,2) === 'D:') {
2373 dateToParse = dateToParse.substring(2);
2374 }
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;
2397 }
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}}');
2406 }
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;
2554 }
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;
2566 }
2567 for (var i = 0, ii = this.pages.length; i < ii; i++) {
2568 this.pages[i].update(newScale);
2569 }
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];
2578 }
2579 this.pages[page - 1].scrollIntoView(dest);
2580 }
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;
2591 }
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;
2600 }
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;
2627 }
2628 this._setScaleUpdatePages(scale, value, resetAutoSettings, noScroll);
2630 selectScaleOption(value);
2631 }
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;
2664 }
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;
2676 }
2677 // Avoid scrolling the first page during loading
2678 if (this.loading && val === 1) {
2679 return;
2680 }
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;
2709 }
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);
2781 }
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);
2788 }
2789 },
2791 requestDataRange: function PdfDataRangeTransport_requestDataRange(
2792 begin, end) {
2793 FirefoxCom.request('requestDataRange', { begin: begin, end: end });
2794 }
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;
2802 }
2803 var args = e.data;
2805 if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) {
2806 return;
2807 }
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;
2829 }
2830 PDFView.open(args.data, 0);
2831 break;
2832 }
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);
2845 }
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;
2858 }
2860 this.pdfDocument.destroy();
2861 this.pdfDocument = null;
2863 var thumbsView = document.getElementById('thumbnailView');
2864 while (thumbsView.hasChildNodes()) {
2865 thumbsView.removeChild(thumbsView.lastChild);
2866 }
2868 if ('_loadingInterval' in thumbsView) {
2869 clearInterval(thumbsView._loadingInterval);
2870 }
2872 var container = document.getElementById('viewer');
2873 while (container.hasChildNodes()) {
2874 container.removeChild(container.lastChild);
2875 }
2877 if (typeof PDFBug !== 'undefined') {
2878 PDFBug.cleanup();
2879 }
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();
2888 }
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;
2897 }
2898 if (args) {
2899 for (var prop in args) {
2900 parameters[prop] = args[prop];
2901 }
2902 }
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);
2916 }
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.');
2932 }
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.');
2939 }
2941 var moreInfo = {
2942 message: message
2943 };
2944 self.error(loadingErrorMessage, moreInfo);
2945 self.loading = false;
2946 }
2947 );
2948 },
2950 download: function pdfViewDownload() {
2951 function downloadByUrl() {
2952 downloadManager.downloadUrl(url, filename);
2953 }
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;
2967 }
2969 if (!this.downloadComplete) { // the PDF is still downloading
2970 downloadByUrl();
2971 return;
2972 }
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;
2994 }
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;
3012 }
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 });
3024 }
3025 };
3027 this.destinationsPromise.then(function() {
3028 if (typeof dest === 'string') {
3029 destString = dest;
3030 dest = self.destinations[dest];
3031 }
3032 if (!(dest instanceof Array)) {
3033 return; // invalid destination
3034 }
3035 goToDestination(dest[0]);
3036 });
3037 },
3039 getDestinationHash: function pdfViewGetDestinationHash(dest) {
3040 if (typeof dest === 'string') {
3041 return PDFView.getAnchorUrl('#' + escape(dest));
3042 }
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;
3057 }
3058 pdfOpenParams += '&zoom=' + scale;
3059 if (dest[2] || dest[3]) {
3060 pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
3061 }
3062 }
3063 return pdfOpenParams;
3064 }
3065 }
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}}');
3102 }
3103 if (moreInfo.lineNumber) {
3104 moreInfoText += '\n' +
3105 mozL10n.get('error_line', {line: moreInfo.lineNumber},
3106 'Line: {{line}}');
3107 }
3108 }
3109 }
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;
3123 }
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();
3139 }
3140 thumbnailView.setImage(pageView.canvas);
3141 };
3142 }
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);
3197 }
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);
3210 }
3211 var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R';
3212 pagesRefMap[refStr] = pageNum;
3213 getPagesLeft--;
3214 if (!getPagesLeft) {
3215 resolvePagesPromise();
3216 }
3217 }.bind(null, pageNum));
3218 }
3219 } else {
3220 // XXX: Printing is semi-broken with auto fetch disabled.
3221 resolvePagesPromise();
3222 }
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;
3265 }
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();
3273 }
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);
3288 }
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;
3298 }
3299 }
3300 });
3301 }
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();
3324 }
3325 self.switchSidebarView('outline');
3326 }
3327 });
3328 }
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');
3351 }
3353 if (!pdfTitle && info && info['Title']) {
3354 pdfTitle = info['Title'];
3355 }
3357 if (pdfTitle) {
3358 self.setTitle(pdfTitle + ' - ' + document.title);
3359 }
3361 if (info.IsAcroFormPresent) {
3362 console.warn('Warning: AcroForm/XFA is not supported');
3363 PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.forms);
3364 }
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;
3378 }
3379 generatorId = i + 1;
3380 return true;
3381 }.bind(null, info.Producer.toLowerCase()));
3382 }
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;
3419 }
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);
3425 }
3426 },
3428 renderHighestPriority: function pdfViewRenderHighestPriority() {
3429 if (PDFView.idleTimeout) {
3430 clearTimeout(PDFView.idleTimeout);
3431 PDFView.idleTimeout = null;
3432 }
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;
3441 }
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;
3451 }
3452 }
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();
3464 }
3465 }
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;
3482 }
3483 for (var i = 0; i < numVisible; ++i) {
3484 var view = visibleViews[i].view;
3485 if (!this.isViewFinished(view)) {
3486 return view;
3487 }
3488 }
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];
3496 }
3497 } else {
3498 var previousPageIndex = visible.first.id - 2;
3499 if (views[previousPageIndex] &&
3500 !this.isViewFinished(views[previousPageIndex])) {
3501 return views[previousPageIndex];
3502 }
3503 }
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;
3530 }
3531 return true;
3532 },
3534 setHash: function pdfViewSetHash(hash) {
3535 if (!hash) {
3536 return;
3537 }
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;
3546 }
3547 var pageNumber, dest;
3548 if ('page' in params) {
3549 pageNumber = (params.page | 0) || 1;
3550 }
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;
3561 }
3562 dest = [null, {name: 'XYZ'},
3563 zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
3564 zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
3565 zoomArg];
3566 }
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
3572 }
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();
3579 }
3580 this.switchSidebarView(params.pagemode === 'bookmarks' ?
3581 'outline' :
3582 params.pagemode);
3583 } else if (params.pagemode === 'none' && this.sidebarOpen) {
3584 toggle.click();
3585 }
3586 }
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));
3592 }
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));
3622 }
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;
3635 }
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;
3648 }
3649 break;
3650 }
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 };
3663 }
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;
3685 }
3686 if (currentHeight > bottom) {
3687 break;
3688 }
3689 currentWidth = view.el.offsetLeft + view.el.clientLeft;
3690 viewWidth = view.el.clientWidth;
3691 if ((currentWidth + viewWidth) < left || currentWidth > right) {
3692 continue;
3693 }
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 });
3700 }
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;
3710 }
3711 return a.id - b.id; // ensure stability
3712 });
3713 }
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);
3726 }
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;
3736 }
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;
3747 }
3748 }
3749 }
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;
3755 }
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();
3761 }
3762 },
3764 afterPrint: function pdfViewSetupAfterPrint() {
3765 var div = document.getElementById('printContainer');
3766 while (div.hasChildNodes()) {
3767 div.removeChild(div.lastChild);
3768 }
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);
3779 }
3781 for (i = 0, l = this.thumbnails.length; i < l; i++) {
3782 var thumb = this.thumbnails[i];
3783 thumb.update(this.pageRotation);
3784 }
3786 this.setScale(this.currentScaleValue, true, true);
3788 this.renderHighestPriority();
3790 if (currentPage) {
3791 currentPage.scrollIntoView();
3792 }
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.
3798 *
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;
3813 }
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();
3820 }
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;
3845 }
3847 this.page += pageFlipDirection;
3848 this.mouseScrollTimeStamp = currentTime;
3849 }
3850 },
3852 /**
3853 * This function clears the member attributes used with mouse scrolling in
3854 * presentation mode.
3855 *
3856 * @this {PDFView}
3857 */
3858 clearMouseScrollState: function pdfViewClearMouseScrollState() {
3859 this.mouseScrollTimeStamp = 0;
3860 this.mouseScrollDelta = 0;
3861 }
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();
3909 }
3910 };
3912 this.reset = function pageViewReset(keepAnnotations) {
3913 if (this.renderTask) {
3914 this.renderTask.cancel();
3915 }
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;
3928 }
3929 div.removeChild(node);
3930 }
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');
3938 }
3939 } else {
3940 this.annotationLayer = null;
3941 }
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;
3955 }
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';
3969 }
3970 if (this.zoomLayer) {
3971 this.cssTransform(this.zoomLayer.firstChild);
3972 }
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;
3992 }
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;
4008 }
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;
4030 }
4031 CustomStyle.setProp('transform', textLayerDiv,
4032 'rotate(' + textAbsRotation + 'deg) ' +
4033 'scale(' + scale + ', ' + scale + ') ' +
4034 'translate(' + transX + ', ' + transY + ')');
4035 CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%');
4036 }
4038 if (USE_ONLY_CSS_ZOOM && this.annotationLayer) {
4039 setupAnnotations(div, this.pdfPage, this.viewport);
4040 }
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);
4066 }
4067 return false;
4068 };
4069 if (dest) {
4070 link.className = 'internalLink';
4071 }
4072 }
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();
4094 }
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
4115 }
4116 return false;
4117 };
4118 link.className = 'internalLink';
4119 }
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);
4136 }
4137 }
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;
4146 }
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);
4176 }
4177 }
4178 }
4180 if (!self.annotationLayer) {
4181 var annotationLayerDiv = document.createElement('div');
4182 annotationLayerDiv.className = 'annotationLayer';
4183 pageDiv.appendChild(annotationLayerDiv);
4184 self.annotationLayer = annotationLayerDiv;
4185 }
4187 self.annotationLayer.appendChild(element);
4188 }
4189 }
4190 });
4191 }
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;
4203 }
4204 dest = null;
4205 PDFView.setScale(PDFView.currentScaleValue, true, true);
4206 }
4207 if (!dest) {
4208 scrollIntoView(div);
4209 return;
4210 }
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;
4261 }
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);
4267 }
4269 if (scale === 'page-fit' && !dest[4]) {
4270 scrollIntoView(div);
4271 return;
4272 }
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;
4295 }
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;
4305 }
4307 if (this.renderingState !== RenderingStates.INITIAL) {
4308 console.error('Must be in new state before drawing');
4309 }
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);
4329 }
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;
4342 }
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);
4362 }
4363 }
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);
4377 }
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;
4388 }
4390 if (error === 'cancelled') {
4391 return;
4392 }
4394 self.renderingState = RenderingStates.FINISHED;
4396 if (self.loadingIconDiv) {
4397 div.removeChild(self.loadingIconDiv);
4398 delete self.loadingIconDiv;
4399 }
4401 if (self.zoomLayer) {
4402 div.removeChild(self.zoomLayer);
4403 self.zoomLayer = null;
4404 }
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();
4414 }
4415 if (error) {
4416 PDFView.error(mozL10n.get('rendering_error', null,
4417 'An error occurred while rendering the page.'), error);
4418 }
4420 self.stats = pdfPage.stats;
4421 self.updateStats();
4422 if (self.onAfterDraw) {
4423 self.onAfterDraw();
4424 }
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();
4439 }
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;
4454 }
4455 cont();
4456 }
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);
4467 }
4468 );
4469 }
4470 },
4471 function pdfPageRenderError(error) {
4472 pageViewDrawCallback(error);
4473 }
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();
4530 }
4531 });
4532 };
4533 };
4535 this.updateStats = function pageViewUpdateStats() {
4536 if (!this.stats) {
4537 return;
4538 }
4540 if (PDFJS.pdfBug && Stats.enabled) {
4541 var stats = this.stats;
4542 Stats.add(this.id, stats);
4543 }
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');
4579 }
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;
4604 }
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;
4661 }
4663 if (this.renderingState !== RenderingStates.INITIAL) {
4664 console.error('Must be in new state before drawing');
4665 }
4667 this.renderingState = RenderingStates.RUNNING;
4668 if (this.hasImage) {
4669 callback();
4670 return;
4671 }
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;
4687 }
4688 cont();
4689 }
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();
4699 }
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;
4712 }
4713 if (this.hasImage || !img) {
4714 return;
4715 }
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;
4753 }
4755 if (typeof this.lastScrollSource === 'undefined') {
4756 this.lastScrollSource = null;
4757 }
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;
4769 }
4771 for (var i = 0, ii = textDivs.length; i < ii; i++) {
4772 var textDiv = textDivs[i];
4773 if ('isWhitespace' in textDiv.dataset) {
4774 continue;
4775 }
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%');
4788 }
4789 }
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);
4811 }
4812 this.renderTimer = setTimeout(function() {
4813 self.setupRenderLayoutTimer();
4814 }, RENDER_DELAY);
4815 }
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;
4825 }
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;
4830 }
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;
4848 }
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);
4858 }
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++;
4883 }
4885 // TODO: Do proper handling here if something goes wrong.
4886 if (i == bidiTexts.length) {
4887 console.error('Could not find matching mapping');
4888 }
4890 var match = {
4891 begin: {
4892 divIdx: i,
4893 offset: matchIdx - iIndex
4894 }
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++;
4905 }
4907 match.end = {
4908 divIdx: i,
4909 offset: matchIdx - iIndex
4910 };
4911 ret.push(match);
4912 }
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;
4921 }
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);
4945 }
4947 function appendText(from, to, className) {
4948 appendTextToDiv(from.divIdx, from.offset, to.offset, className);
4949 }
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;
4962 }
4963 div.appendChild(node);
4964 }
4966 function highlightDiv(divIdx, className) {
4967 textDivs[divIdx].className = className;
4968 }
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;
4978 }
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 });
4990 }
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);
4997 }
4998 // clears the divs and set the content until the begin point.
4999 beginText(begin);
5000 } else {
5001 appendText(prevEnd, begin);
5002 }
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);
5010 }
5011 beginText(end, 'highlight end' + highlightSuffix);
5012 }
5013 prevEnd = end;
5014 }
5016 if (prevEnd) {
5017 appendText(prevEnd, infty);
5018 }
5019 };
5021 this.updateMatches = function textLayerUpdateMatches() {
5022 // Only show matches, once all rendering is done.
5023 if (!this.renderingDone) {
5024 return;
5025 }
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 = '';
5041 }
5042 clearedUntilDivIdx = match.end.divIdx + 1;
5043 }
5045 if (PDFFindController === null || !PDFFindController.active) {
5046 return;
5047 }
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);
5064 }
5066 if (!outline) {
5067 if (!outlineView.classList.contains('hidden')) {
5068 PDFView.switchSidebarView('thumbs');
5069 }
5070 return;
5071 }
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 };
5079 }
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});
5100 }
5102 levelData.parent.appendChild(div);
5103 }
5104 }
5105 };
5107 var DocumentAttachmentsView = function documentAttachmentsView(attachments) {
5108 var attachmentsView = document.getElementById('attachmentsView');
5109 while (attachmentsView.firstChild) {
5110 attachmentsView.removeChild(attachmentsView.firstChild);
5111 }
5113 if (!attachments) {
5114 if (!attachmentsView.classList.contains('hidden')) {
5115 PDFView.switchSidebarView('thumbs');
5116 }
5117 return;
5118 }
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 };
5128 }
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);
5142 }
5143 };
5146 function webViewerLoad(evt) {
5147 PDFView.initialize().then(webViewerInitialized);
5148 }
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');
5162 }
5164 if ('disableRange' in hashParams) {
5165 PDFJS.disableRange = (hashParams['disableRange'] === 'true');
5166 }
5168 if ('disableAutoFetch' in hashParams) {
5169 PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true');
5170 }
5172 if ('disableFontFace' in hashParams) {
5173 PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true');
5174 }
5176 if ('disableHistory' in hashParams) {
5177 PDFJS.disableHistory = (hashParams['disableHistory'] === 'true');
5178 }
5180 if ('webgl' in hashParams) {
5181 PDFJS.disableWebGL = (hashParams['webgl'] !== 'true');
5182 }
5184 if ('useOnlyCssZoom' in hashParams) {
5185 USE_ONLY_CSS_ZOOM = (hashParams['useOnlyCssZoom'] === 'true');
5186 }
5188 if ('verbosity' in hashParams) {
5189 PDFJS.verbosity = hashParams['verbosity'] | 0;
5190 }
5192 if ('ignoreCurrentPositionOnZoom' in hashParams) {
5193 IGNORE_CURRENT_POSITION_ON_ZOOM =
5194 (hashParams['ignoreCurrentPositionOnZoom'] === 'true');
5195 }
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.'));
5203 }
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;
5216 }
5217 }
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();
5225 }
5227 if (!PDFView.supportsPrinting) {
5228 document.getElementById('print').classList.add('hidden');
5229 document.getElementById('secondaryPrint').classList.add('hidden');
5230 }
5232 if (!PDFView.supportsFullscreen) {
5233 document.getElementById('presentationMode').classList.add('hidden');
5234 document.getElementById('secondaryPresentationMode').
5235 classList.add('hidden');
5236 }
5238 if (PDFView.supportsIntegratedFind) {
5239 document.getElementById('viewFind').classList.add('hidden');
5240 }
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');
5256 }
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;
5315 }
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);
5341 }
5342 }
5344 document.addEventListener('DOMContentLoaded', webViewerLoad, true);
5346 function updateViewarea() {
5348 if (!PDFView.initialized) {
5349 return;
5350 }
5351 var visible = PDFView.getVisiblePages();
5352 var visiblePages = visible.views;
5353 if (visiblePages.length === 0) {
5354 return;
5355 }
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;
5368 }
5369 if (page.id === PDFView.page) {
5370 stillFullyVisible = true;
5371 break;
5372 }
5373 }
5375 if (!stillFullyVisible) {
5376 currentId = visiblePages[0].id;
5377 }
5379 if (!PresentationMode.active) {
5380 updateViewarea.inProgress = true; // used in "set page"
5381 PDFView.page = currentId;
5382 updateViewarea.inProgress = false;
5383 }
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 };
5405 }
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);
5421 }
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);
5429 }
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));
5439 }
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;
5451 }
5452 option.selected = true;
5453 predefinedValueFound = true;
5454 }
5455 return predefinedValueFound;
5456 }
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;');
5474 }
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;
5494 }
5496 var predefinedValueFound = selectScaleOption('' + evt.scale);
5497 if (!predefinedValueFound) {
5498 customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
5499 customScaleOption.selected = true;
5500 }
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');
5511 }
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 });
5524 }
5525 }
5526 }
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);
5542 }
5543 }
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();
5552 }
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();
5557 }
5558 }, false);
5560 window.addEventListener('keydown', function keydown(evt) {
5561 if (PasswordPrompt.visible) {
5562 return;
5563 }
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;
5580 }
5581 break;
5582 case 71: // g
5583 if (!PDFView.supportsIntegratedFind) {
5584 PDFFindBar.dispatchEvent('again', cmd === 5 || cmd === 12);
5585 handled = true;
5586 }
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;
5610 }
5611 }
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;
5626 }
5627 }
5629 if (handled) {
5630 evt.preventDefault();
5631 return;
5632 }
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;
5644 }
5645 }
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;
5655 }
5656 /* in presentation mode */
5657 /* falls through */
5658 case 37: // left arrow
5659 // horizontal scrolling using arrow keys
5660 if (PDFView.isHorizontalScrollbarEnabled) {
5661 break;
5662 }
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;
5673 }
5674 if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) {
5675 PDFFindBar.close();
5676 handled = true;
5677 }
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;
5685 }
5686 /* falls through */
5687 case 39: // right arrow
5688 // horizontal scrolling using arrow keys
5689 if (PDFView.isHorizontalScrollbarEnabled) {
5690 break;
5691 }
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;
5703 }
5704 break;
5705 case 35: // end
5706 if (PresentationMode.active) {
5707 PDFView.page = PDFView.pdfDocument.numPages;
5708 handled = true;
5709 }
5710 break;
5712 case 72: // 'h'
5713 if (!PresentationMode.active) {
5714 HandTool.toggle();
5715 }
5716 break;
5717 case 82: // 'r'
5718 PDFView.rotatePages(90);
5719 break;
5720 }
5721 }
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;
5729 }
5730 PDFView.page--;
5731 handled = true;
5732 break;
5734 case 82: // 'r'
5735 PDFView.rotatePages(-90);
5736 break;
5737 }
5738 }
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();
5749 }
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();
5755 }
5756 }
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;
5764 }
5765 break;
5766 case 39: // right arrow
5767 if (PresentationMode.active) {
5768 PDFHistory.forward();
5769 handled = true;
5770 }
5771 break;
5772 }
5773 }
5775 if (handled) {
5776 evt.preventDefault();
5777 PDFView.clearMouseScrollState();
5778 }
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 })();