· 8 years ago · May 23, 2018, 03:46 AM
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, ProgressBar,
18 DownloadManager, getFileName, scrollIntoView, getPDFFileNameFromURL,
19 PDFHistory, Preferences, SidebarView, ViewHistory, PageView,
20 PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar,
21 PasswordPrompt, PresentationMode, HandTool, Promise,
22 DocumentProperties, DocumentOutlineView, DocumentAttachmentsView,
23 OverlayManager, PDFFindController, PDFFindBar, getVisibleElements,
24 watchScroll, PDFViewer, PDFRenderingQueue, PresentationModeState,
25 RenderingStates, DEFAULT_SCALE, UNKNOWN_SCALE,
26 IGNORE_CURRENT_POSITION_ON_ZOOM: true */
27
28'use strict';
29
30var DEFAULT_URL = 'https://cors-anywhere.herokuapp.com/http://114.108.252.52:8020/SuperContainer/RawData/Library/Catalog/Items/2744?a=0';
31var DEFAULT_SCALE_DELTA = 1.1;
32var MIN_SCALE = 0.25;
33var MAX_SCALE = 10.0;
34var VIEW_HISTORY_MEMORY = 20;
35var SCALE_SELECT_CONTAINER_PADDING = 8;
36var SCALE_SELECT_PADDING = 22;
37var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading';
38var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000;
39
40PDFJS.imageResourcesPath = './images/';
41PDFJS.workerSrc = 'https://dl.dropbox.com/s/nvhmrlp3vrdaxye/pdf.worker.js';
42PDFJS.cMapUrl = '../web/cmaps/';
43PDFJS.cMapPacked = true;
44
45var mozL10n = document.mozL10n || document.webL10n;
46
47
48var CSS_UNITS = 96.0 / 72.0;
49var DEFAULT_SCALE = 'auto';
50var UNKNOWN_SCALE = 0;
51var MAX_AUTO_SCALE = 1.25;
52var SCROLLBAR_PADDING = 40;
53var VERTICAL_PADDING = 5;
54var DEFAULT_CACHE_SIZE = 10;
55
56// optimised CSS custom property getter/setter
57var CustomStyle = (function CustomStyleClosure() {
58
59 // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
60 // animate-css-transforms-firefox-webkit.html
61 // in some versions of IE9 it is critical that ms appear in this list
62 // before Moz
63 var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
64 var _cache = {};
65
66 function CustomStyle() {}
67
68 CustomStyle.getProp = function get(propName, element) {
69 // check cache only when no element is given
70 if (arguments.length === 1 && typeof _cache[propName] === 'string') {
71 return _cache[propName];
72 }
73
74 element = element || document.documentElement;
75 var style = element.style, prefixed, uPropName;
76
77 // test standard property first
78 if (typeof style[propName] === 'string') {
79 return (_cache[propName] = propName);
80 }
81
82 // capitalize
83 uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
84
85 // test vendor specific properties
86 for (var i = 0, l = prefixes.length; i < l; i++) {
87 prefixed = prefixes[i] + uPropName;
88 if (typeof style[prefixed] === 'string') {
89 return (_cache[propName] = prefixed);
90 }
91 }
92
93 //if all fails then set to undefined
94 return (_cache[propName] = 'undefined');
95 };
96
97 CustomStyle.setProp = function set(propName, element, str) {
98 var prop = this.getProp(propName);
99 if (prop !== 'undefined') {
100 element.style[prop] = str;
101 }
102 };
103
104 return CustomStyle;
105})();
106
107function getFileName(url) {
108 var anchor = url.indexOf('#');
109 var query = url.indexOf('?');
110 var end = Math.min(
111 anchor > 0 ? anchor : url.length,
112 query > 0 ? query : url.length);
113 return url.substring(url.lastIndexOf('/', end) + 1, end);
114}
115
116/**
117 * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
118 * @return {Object} The object with horizontal (sx) and vertical (sy)
119 scales. The scaled property is set to false if scaling is
120 not required, true otherwise.
121 */
122function getOutputScale(ctx) {
123 var devicePixelRatio = window.devicePixelRatio || 1;
124 var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
125 ctx.mozBackingStorePixelRatio ||
126 ctx.msBackingStorePixelRatio ||
127 ctx.oBackingStorePixelRatio ||
128 ctx.backingStorePixelRatio || 1;
129 var pixelRatio = devicePixelRatio / backingStoreRatio;
130 return {
131 sx: pixelRatio,
132 sy: pixelRatio,
133 scaled: pixelRatio !== 1
134 };
135}
136
137/**
138 * Scrolls specified element into view of its parent.
139 * element {Object} The element to be visible.
140 * spot {Object} An object with optional top and left properties,
141 * specifying the offset from the top left edge.
142 */
143function scrollIntoView(element, spot) {
144 // Assuming offsetParent is available (it's not available when viewer is in
145 // hidden iframe or object). We have to scroll: if the offsetParent is not set
146 // producing the error. See also animationStartedClosure.
147 var parent = element.offsetParent;
148 var offsetY = element.offsetTop + element.clientTop;
149 var offsetX = element.offsetLeft + element.clientLeft;
150 if (!parent) {
151 console.error('offsetParent is not set -- cannot scroll');
152 return;
153 }
154 while (parent.clientHeight === parent.scrollHeight) {
155 if (parent.dataset._scaleY) {
156 offsetY /= parent.dataset._scaleY;
157 offsetX /= parent.dataset._scaleX;
158 }
159 offsetY += parent.offsetTop;
160 offsetX += parent.offsetLeft;
161 parent = parent.offsetParent;
162 if (!parent) {
163 return; // no need to scroll
164 }
165 }
166 if (spot) {
167 if (spot.top !== undefined) {
168 offsetY += spot.top;
169 }
170 if (spot.left !== undefined) {
171 offsetX += spot.left;
172 parent.scrollLeft = offsetX;
173 }
174 }
175 parent.scrollTop = offsetY;
176}
177
178/**
179 * Helper function to start monitoring the scroll event and converting them into
180 * PDF.js friendly one: with scroll debounce and scroll direction.
181 */
182function watchScroll(viewAreaElement, callback) {
183 var debounceScroll = function debounceScroll(evt) {
184 if (rAF) {
185 return;
186 }
187 // schedule an invocation of scroll for next animation frame.
188 rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
189 rAF = null;
190
191 var currentY = viewAreaElement.scrollTop;
192 var lastY = state.lastY;
193 if (currentY > lastY) {
194 state.down = true;
195 } else if (currentY < lastY) {
196 state.down = false;
197 }
198 state.lastY = currentY;
199 // else do nothing and use previous value
200 callback(state);
201 });
202 };
203
204 var state = {
205 down: true,
206 lastY: viewAreaElement.scrollTop,
207 _eventHandler: debounceScroll
208 };
209
210 var rAF = null;
211 viewAreaElement.addEventListener('scroll', debounceScroll, true);
212 return state;
213}
214
215/**
216 * Generic helper to find out what elements are visible within a scroll pane.
217 */
218function getVisibleElements(scrollEl, views, sortByVisibility) {
219 var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
220 var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
221
222 var visible = [], view;
223 var currentHeight, viewHeight, hiddenHeight, percentHeight;
224 var currentWidth, viewWidth;
225 for (var i = 0, ii = views.length; i < ii; ++i) {
226 view = views[i];
227 currentHeight = view.el.offsetTop + view.el.clientTop;
228 viewHeight = view.el.clientHeight;
229 if ((currentHeight + viewHeight) < top) {
230 continue;
231 }
232 if (currentHeight > bottom) {
233 break;
234 }
235 currentWidth = view.el.offsetLeft + view.el.clientLeft;
236 viewWidth = view.el.clientWidth;
237 if ((currentWidth + viewWidth) < left || currentWidth > right) {
238 continue;
239 }
240 hiddenHeight = Math.max(0, top - currentHeight) +
241 Math.max(0, currentHeight + viewHeight - bottom);
242 percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
243
244 visible.push({ id: view.id, x: currentWidth, y: currentHeight,
245 view: view, percent: percentHeight });
246 }
247
248 var first = visible[0];
249 var last = visible[visible.length - 1];
250
251 if (sortByVisibility) {
252 visible.sort(function(a, b) {
253 var pc = a.percent - b.percent;
254 if (Math.abs(pc) > 0.001) {
255 return -pc;
256 }
257 return a.id - b.id; // ensure stability
258 });
259 }
260 return {first: first, last: last, views: visible};
261}
262
263/**
264 * Event handler to suppress context menu.
265 */
266function noContextMenuHandler(e) {
267 e.preventDefault();
268}
269
270/**
271 * Returns the filename or guessed filename from the url (see issue 3455).
272 * url {String} The original PDF location.
273 * @return {String} Guessed PDF file name.
274 */
275function getPDFFileNameFromURL(url) {
276 var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
277 // SCHEME HOST 1.PATH 2.QUERY 3.REF
278 // Pattern to get last matching NAME.pdf
279 var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
280 var splitURI = reURI.exec(url);
281 var suggestedFilename = reFilename.exec(splitURI[1]) ||
282 reFilename.exec(splitURI[2]) ||
283 reFilename.exec(splitURI[3]);
284 if (suggestedFilename) {
285 suggestedFilename = suggestedFilename[0];
286 if (suggestedFilename.indexOf('%') !== -1) {
287 // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
288 try {
289 suggestedFilename =
290 reFilename.exec(decodeURIComponent(suggestedFilename))[0];
291 } catch(e) { // Possible (extremely rare) errors:
292 // URIError "Malformed URI", e.g. for "%AA.pdf"
293 // TypeError "null has no properties", e.g. for "%2F.pdf"
294 }
295 }
296 }
297 return suggestedFilename || 'document.pdf';
298}
299
300var ProgressBar = (function ProgressBarClosure() {
301
302 function clamp(v, min, max) {
303 return Math.min(Math.max(v, min), max);
304 }
305
306 function ProgressBar(id, opts) {
307 this.visible = true;
308
309 // Fetch the sub-elements for later.
310 this.div = document.querySelector(id + ' .progress');
311
312 // Get the loading bar element, so it can be resized to fit the viewer.
313 this.bar = this.div.parentNode;
314
315 // Get options, with sensible defaults.
316 this.height = opts.height || 100;
317 this.width = opts.width || 100;
318 this.units = opts.units || '%';
319
320 // Initialize heights.
321 this.div.style.height = this.height + this.units;
322 this.percent = 0;
323 }
324
325 ProgressBar.prototype = {
326
327 updateBar: function ProgressBar_updateBar() {
328 if (this._indeterminate) {
329 this.div.classList.add('indeterminate');
330 this.div.style.width = this.width + this.units;
331 return;
332 }
333
334 this.div.classList.remove('indeterminate');
335 var progressSize = this.width * this._percent / 100;
336 this.div.style.width = progressSize + this.units;
337 },
338
339 get percent() {
340 return this._percent;
341 },
342
343 set percent(val) {
344 this._indeterminate = isNaN(val);
345 this._percent = clamp(val, 0, 100);
346 this.updateBar();
347 },
348
349 setWidth: function ProgressBar_setWidth(viewer) {
350 if (viewer) {
351 var container = viewer.parentNode;
352 var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
353 if (scrollbarWidth > 0) {
354 this.bar.setAttribute('style', 'width: calc(100% - ' +
355 scrollbarWidth + 'px);');
356 }
357 }
358 },
359
360 hide: function ProgressBar_hide() {
361 if (!this.visible) {
362 return;
363 }
364 this.visible = false;
365 this.bar.classList.add('hidden');
366 document.body.classList.remove('loadingInProgress');
367 },
368
369 show: function ProgressBar_show() {
370 if (this.visible) {
371 return;
372 }
373 this.visible = true;
374 document.body.classList.add('loadingInProgress');
375 this.bar.classList.remove('hidden');
376 }
377 };
378
379 return ProgressBar;
380})();
381
382var Cache = function cacheCache(size) {
383 var data = [];
384 this.push = function cachePush(view) {
385 var i = data.indexOf(view);
386 if (i >= 0) {
387 data.splice(i, 1);
388 }
389 data.push(view);
390 if (data.length > size) {
391 data.shift().destroy();
392 }
393 };
394 this.resize = function (newSize) {
395 size = newSize;
396 while (data.length > size) {
397 data.shift().destroy();
398 }
399 };
400};
401
402
403
404var DEFAULT_PREFERENCES = {
405 showPreviousViewOnLoad: true,
406 defaultZoomValue: '',
407 sidebarViewOnLoad: 0,
408 enableHandToolOnLoad: false,
409 enableWebGL: false,
410 pdfBugEnabled: false,
411 disableRange: false,
412 disableStream: false,
413 disableAutoFetch: false,
414 disableFontFace: false,
415 disableTextLayer: false,
416 useOnlyCssZoom: false
417};
418
419
420var SidebarView = {
421 NONE: 0,
422 THUMBS: 1,
423 OUTLINE: 2,
424 ATTACHMENTS: 3
425};
426
427/**
428 * Preferences - Utility for storing persistent settings.
429 * Used for settings that should be applied to all opened documents,
430 * or every time the viewer is loaded.
431 */
432var Preferences = {
433 prefs: Object.create(DEFAULT_PREFERENCES),
434 isInitializedPromiseResolved: false,
435 initializedPromise: null,
436
437 /**
438 * Initialize and fetch the current preference values from storage.
439 * @return {Promise} A promise that is resolved when the preferences
440 * have been initialized.
441 */
442 initialize: function preferencesInitialize() {
443 return this.initializedPromise =
444 this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
445 this.isInitializedPromiseResolved = true;
446 if (prefObj) {
447 this.prefs = prefObj;
448 }
449 }.bind(this));
450 },
451
452 /**
453 * Stub function for writing preferences to storage.
454 * NOTE: This should be overridden by a build-specific function defined below.
455 * @param {Object} prefObj The preferences that should be written to storage.
456 * @return {Promise} A promise that is resolved when the preference values
457 * have been written.
458 */
459 _writeToStorage: function preferences_writeToStorage(prefObj) {
460 return Promise.resolve();
461 },
462
463 /**
464 * Stub function for reading preferences from storage.
465 * NOTE: This should be overridden by a build-specific function defined below.
466 * @param {Object} prefObj The preferences that should be read from storage.
467 * @return {Promise} A promise that is resolved with an {Object} containing
468 * the preferences that have been read.
469 */
470 _readFromStorage: function preferences_readFromStorage(prefObj) {
471 return Promise.resolve();
472 },
473
474 /**
475 * Reset the preferences to their default values and update storage.
476 * @return {Promise} A promise that is resolved when the preference values
477 * have been reset.
478 */
479 reset: function preferencesReset() {
480 return this.initializedPromise.then(function() {
481 this.prefs = Object.create(DEFAULT_PREFERENCES);
482 return this._writeToStorage(DEFAULT_PREFERENCES);
483 }.bind(this));
484 },
485
486 /**
487 * Replace the current preference values with the ones from storage.
488 * @return {Promise} A promise that is resolved when the preference values
489 * have been updated.
490 */
491 reload: function preferencesReload() {
492 return this.initializedPromise.then(function () {
493 this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
494 if (prefObj) {
495 this.prefs = prefObj;
496 }
497 }.bind(this));
498 }.bind(this));
499 },
500
501 /**
502 * Set the value of a preference.
503 * @param {string} name The name of the preference that should be changed.
504 * @param {boolean|number|string} value The new value of the preference.
505 * @return {Promise} A promise that is resolved when the value has been set,
506 * provided that the preference exists and the types match.
507 */
508 set: function preferencesSet(name, value) {
509 return this.initializedPromise.then(function () {
510 if (DEFAULT_PREFERENCES[name] === undefined) {
511 throw new Error('preferencesSet: \'' + name + '\' is undefined.');
512 } else if (value === undefined) {
513 throw new Error('preferencesSet: no value is specified.');
514 }
515 var valueType = typeof value;
516 var defaultType = typeof DEFAULT_PREFERENCES[name];
517
518 if (valueType !== defaultType) {
519 if (valueType === 'number' && defaultType === 'string') {
520 value = value.toString();
521 } else {
522 throw new Error('Preferences_set: \'' + value + '\' is a \"' +
523 valueType + '\", expected \"' + defaultType + '\".');
524 }
525 } else {
526 if (valueType === 'number' && (value | 0) !== value) {
527 throw new Error('Preferences_set: \'' + value +
528 '\' must be an \"integer\".');
529 }
530 }
531 this.prefs[name] = value;
532 return this._writeToStorage(this.prefs);
533 }.bind(this));
534 },
535
536 /**
537 * Get the value of a preference.
538 * @param {string} name The name of the preference whose value is requested.
539 * @return {Promise} A promise that is resolved with a {boolean|number|string}
540 * containing the value of the preference.
541 */
542 get: function preferencesGet(name) {
543 return this.initializedPromise.then(function () {
544 var defaultValue = DEFAULT_PREFERENCES[name];
545
546 if (defaultValue === undefined) {
547 throw new Error('preferencesGet: \'' + name + '\' is undefined.');
548 } else {
549 var prefValue = this.prefs[name];
550
551 if (prefValue !== undefined) {
552 return prefValue;
553 }
554 }
555 return defaultValue;
556 }.bind(this));
557 }
558};
559
560
561
562Preferences._writeToStorage = function (prefObj) {
563 return new Promise(function (resolve) {
564 localStorage.setItem('pdfjs.preferences', JSON.stringify(prefObj));
565 resolve();
566 });
567};
568
569Preferences._readFromStorage = function (prefObj) {
570 return new Promise(function (resolve) {
571 var readPrefs = JSON.parse(localStorage.getItem('pdfjs.preferences'));
572 resolve(readPrefs);
573 });
574};
575
576
577(function mozPrintCallbackPolyfillClosure() {
578 if ('mozPrintCallback' in document.createElement('canvas')) {
579 return;
580 }
581 // Cause positive result on feature-detection:
582 HTMLCanvasElement.prototype.mozPrintCallback = undefined;
583
584 var canvases; // During print task: non-live NodeList of <canvas> elements
585 var index; // Index of <canvas> element that is being processed
586
587 var print = window.print;
588 window.print = function print() {
589 if (canvases) {
590 console.warn('Ignored window.print() because of a pending print job.');
591 return;
592 }
593 try {
594 dispatchEvent('beforeprint');
595 } finally {
596 canvases = document.querySelectorAll('canvas');
597 index = -1;
598 next();
599 }
600 };
601
602 function dispatchEvent(eventType) {
603 var event = document.createEvent('CustomEvent');
604 event.initCustomEvent(eventType, false, false, 'custom');
605 window.dispatchEvent(event);
606 }
607
608 function next() {
609 if (!canvases) {
610 return; // Print task cancelled by user (state reset in abort())
611 }
612
613 renderProgress();
614 if (++index < canvases.length) {
615 var canvas = canvases[index];
616 if (typeof canvas.mozPrintCallback === 'function') {
617 canvas.mozPrintCallback({
618 context: canvas.getContext('2d'),
619 abort: abort,
620 done: next
621 });
622 } else {
623 next();
624 }
625 } else {
626 renderProgress();
627 print.call(window);
628 setTimeout(abort, 20); // Tidy-up
629 }
630 }
631
632 function abort() {
633 if (canvases) {
634 canvases = null;
635 renderProgress();
636 dispatchEvent('afterprint');
637 }
638 }
639
640 function renderProgress() {
641 var progressContainer = document.getElementById('mozPrintCallback-shim');
642 if (canvases) {
643 var progress = Math.round(100 * index / canvases.length);
644 var progressBar = progressContainer.querySelector('progress');
645 var progressPerc = progressContainer.querySelector('.relative-progress');
646 progressBar.value = progress;
647 progressPerc.textContent = progress + '%';
648 progressContainer.removeAttribute('hidden');
649 progressContainer.onclick = abort;
650 } else {
651 progressContainer.setAttribute('hidden', '');
652 }
653 }
654
655 var hasAttachEvent = !!document.attachEvent;
656
657 window.addEventListener('keydown', function(event) {
658 // Intercept Cmd/Ctrl + P in all browsers.
659 // Also intercept Cmd/Ctrl + Shift + P in Chrome and Opera
660 if (event.keyCode === 80/*P*/ && (event.ctrlKey || event.metaKey) &&
661 !event.altKey && (!event.shiftKey || window.chrome || window.opera)) {
662 window.print();
663 if (hasAttachEvent) {
664 // Only attachEvent can cancel Ctrl + P dialog in IE <=10
665 // attachEvent is gone in IE11, so the dialog will re-appear in IE11.
666 return;
667 }
668 event.preventDefault();
669 if (event.stopImmediatePropagation) {
670 event.stopImmediatePropagation();
671 } else {
672 event.stopPropagation();
673 }
674 return;
675 }
676 if (event.keyCode === 27 && canvases) { // Esc
677 abort();
678 }
679 }, true);
680 if (hasAttachEvent) {
681 document.attachEvent('onkeydown', function(event) {
682 event = event || window.event;
683 if (event.keyCode === 80/*P*/ && event.ctrlKey) {
684 event.keyCode = 0;
685 return false;
686 }
687 });
688 }
689
690 if ('onbeforeprint' in window) {
691 // Do not propagate before/afterprint events when they are not triggered
692 // from within this polyfill. (FF/IE).
693 var stopPropagationIfNeeded = function(event) {
694 if (event.detail !== 'custom' && event.stopImmediatePropagation) {
695 event.stopImmediatePropagation();
696 }
697 };
698 window.addEventListener('beforeprint', stopPropagationIfNeeded, false);
699 window.addEventListener('afterprint', stopPropagationIfNeeded, false);
700 }
701})();
702
703
704
705var DownloadManager = (function DownloadManagerClosure() {
706
707 function download(blobUrl, filename) {
708 var a = document.createElement('a');
709 if (a.click) {
710 // Use a.click() if available. Otherwise, Chrome might show
711 // "Unsafe JavaScript attempt to initiate a navigation change
712 // for frame with URL" and not open the PDF at all.
713 // Supported by (not mentioned = untested):
714 // - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click)
715 // - Chrome 19 - 26 (18- does not support a.click)
716 // - Opera 9 - 12.15
717 // - Internet Explorer 6 - 10
718 // - Safari 6 (5.1- does not support a.click)
719 a.href = blobUrl;
720 a.target = '_parent';
721 // Use a.download if available. This increases the likelihood that
722 // the file is downloaded instead of opened by another PDF plugin.
723 if ('download' in a) {
724 a.download = filename;
725 }
726 // <a> must be in the document for IE and recent Firefox versions.
727 // (otherwise .click() is ignored)
728 (document.body || document.documentElement).appendChild(a);
729 a.click();
730 a.parentNode.removeChild(a);
731 } else {
732 if (window.top === window &&
733 blobUrl.split('#')[0] === window.location.href.split('#')[0]) {
734 // If _parent == self, then opening an identical URL with different
735 // location hash will only cause a navigation, not a download.
736 var padCharacter = blobUrl.indexOf('?') === -1 ? '?' : '&';
737 blobUrl = blobUrl.replace(/#|$/, padCharacter + '$&');
738 }
739 window.open(blobUrl, '_parent');
740 }
741 }
742
743 function DownloadManager() {}
744
745 DownloadManager.prototype = {
746 downloadUrl: function DownloadManager_downloadUrl(url, filename) {
747 if (!PDFJS.isValidUrl(url, true)) {
748 return; // restricted/invalid URL
749 }
750
751 download(url + '#pdfjs.action=download', filename);
752 },
753
754 downloadData: function DownloadManager_downloadData(data, filename,
755 contentType) {
756 if (navigator.msSaveBlob) { // IE10 and above
757 return navigator.msSaveBlob(new Blob([data], { type: contentType }),
758 filename);
759 }
760
761 var blobUrl = PDFJS.createObjectURL(data, contentType);
762 download(blobUrl, filename);
763 },
764
765 download: function DownloadManager_download(blob, url, filename) {
766 if (!URL) {
767 // URL.createObjectURL is not supported
768 this.downloadUrl(url, filename);
769 return;
770 }
771
772 if (navigator.msSaveBlob) {
773 // IE10 / IE11
774 if (!navigator.msSaveBlob(blob, filename)) {
775 this.downloadUrl(url, filename);
776 }
777 return;
778 }
779
780 var blobUrl = URL.createObjectURL(blob);
781 download(blobUrl, filename);
782 }
783 };
784
785 return DownloadManager;
786})();
787
788
789
790
791
792/**
793 * View History - This is a utility for saving various view parameters for
794 * recently opened files.
795 *
796 * The way that the view parameters are stored depends on how PDF.js is built,
797 * for 'node make <flag>' the following cases exist:
798 * - FIREFOX or MOZCENTRAL - uses sessionStorage.
799 * - B2G - uses asyncStorage.
800 * - GENERIC or CHROME - uses localStorage, if it is available.
801 */
802var ViewHistory = (function ViewHistoryClosure() {
803 function ViewHistory(fingerprint) {
804 this.fingerprint = fingerprint;
805 this.isInitializedPromiseResolved = false;
806 this.initializedPromise =
807 this._readFromStorage().then(function (databaseStr) {
808 this.isInitializedPromiseResolved = true;
809
810 var database = JSON.parse(databaseStr || '{}');
811 if (!('files' in database)) {
812 database.files = [];
813 }
814 if (database.files.length >= VIEW_HISTORY_MEMORY) {
815 database.files.shift();
816 }
817 var index;
818 for (var i = 0, length = database.files.length; i < length; i++) {
819 var branch = database.files[i];
820 if (branch.fingerprint === this.fingerprint) {
821 index = i;
822 break;
823 }
824 }
825 if (typeof index !== 'number') {
826 index = database.files.push({fingerprint: this.fingerprint}) - 1;
827 }
828 this.file = database.files[index];
829 this.database = database;
830 }.bind(this));
831 }
832
833 ViewHistory.prototype = {
834 _writeToStorage: function ViewHistory_writeToStorage() {
835 return new Promise(function (resolve) {
836 var databaseStr = JSON.stringify(this.database);
837
838
839
840 localStorage.setItem('database', databaseStr);
841 resolve();
842 }.bind(this));
843 },
844
845 _readFromStorage: function ViewHistory_readFromStorage() {
846 return new Promise(function (resolve) {
847
848
849 resolve(localStorage.getItem('database'));
850 });
851 },
852
853 set: function ViewHistory_set(name, val) {
854 if (!this.isInitializedPromiseResolved) {
855 return;
856 }
857 this.file[name] = val;
858 return this._writeToStorage();
859 },
860
861 setMultiple: function ViewHistory_setMultiple(properties) {
862 if (!this.isInitializedPromiseResolved) {
863 return;
864 }
865 for (var name in properties) {
866 this.file[name] = properties[name];
867 }
868 return this._writeToStorage();
869 },
870
871 get: function ViewHistory_get(name, defaultValue) {
872 if (!this.isInitializedPromiseResolved) {
873 return defaultValue;
874 }
875 return this.file[name] || defaultValue;
876 }
877 };
878
879 return ViewHistory;
880})();
881
882
883/**
884 * Creates a "search bar" given a set of DOM elements that act as controls
885 * for searching or for setting search preferences in the UI. This object
886 * also sets up the appropriate events for the controls. Actual searching
887 * is done by PDFFindController.
888 */
889var PDFFindBar = (function PDFFindBarClosure() {
890 function PDFFindBar(options) {
891 this.opened = false;
892 this.bar = options.bar || null;
893 this.toggleButton = options.toggleButton || null;
894 this.findField = options.findField || null;
895 this.highlightAll = options.highlightAllCheckbox || null;
896 this.caseSensitive = options.caseSensitiveCheckbox || null;
897 this.findMsg = options.findMsg || null;
898 this.findStatusIcon = options.findStatusIcon || null;
899 this.findPreviousButton = options.findPreviousButton || null;
900 this.findNextButton = options.findNextButton || null;
901 this.findController = options.findController || null;
902
903 if (this.findController === null) {
904 throw new Error('PDFFindBar cannot be used without a ' +
905 'PDFFindController instance.');
906 }
907
908 // Add event listeners to the DOM elements.
909 var self = this;
910 this.toggleButton.addEventListener('click', function() {
911 self.toggle();
912 });
913
914 this.findField.addEventListener('input', function() {
915 self.dispatchEvent('');
916 });
917
918 this.bar.addEventListener('keydown', function(evt) {
919 switch (evt.keyCode) {
920 case 13: // Enter
921 if (evt.target === self.findField) {
922 self.dispatchEvent('again', evt.shiftKey);
923 }
924 break;
925 case 27: // Escape
926 self.close();
927 break;
928 }
929 });
930
931 this.findPreviousButton.addEventListener('click', function() {
932 self.dispatchEvent('again', true);
933 });
934
935 this.findNextButton.addEventListener('click', function() {
936 self.dispatchEvent('again', false);
937 });
938
939 this.highlightAll.addEventListener('click', function() {
940 self.dispatchEvent('highlightallchange');
941 });
942
943 this.caseSensitive.addEventListener('click', function() {
944 self.dispatchEvent('casesensitivitychange');
945 });
946 }
947
948 PDFFindBar.prototype = {
949 dispatchEvent: function PDFFindBar_dispatchEvent(type, findPrev) {
950 var event = document.createEvent('CustomEvent');
951 event.initCustomEvent('find' + type, true, true, {
952 query: this.findField.value,
953 caseSensitive: this.caseSensitive.checked,
954 highlightAll: this.highlightAll.checked,
955 findPrevious: findPrev
956 });
957 return window.dispatchEvent(event);
958 },
959
960 updateUIState: function PDFFindBar_updateUIState(state, previous) {
961 var notFound = false;
962 var findMsg = '';
963 var status = '';
964
965 switch (state) {
966 case FindStates.FIND_FOUND:
967 break;
968
969 case FindStates.FIND_PENDING:
970 status = 'pending';
971 break;
972
973 case FindStates.FIND_NOTFOUND:
974 findMsg = mozL10n.get('find_not_found', null, 'Phrase not found');
975 notFound = true;
976 break;
977
978 case FindStates.FIND_WRAPPED:
979 if (previous) {
980 findMsg = mozL10n.get('find_reached_top', null,
981 'Reached top of document, continued from bottom');
982 } else {
983 findMsg = mozL10n.get('find_reached_bottom', null,
984 'Reached end of document, continued from top');
985 }
986 break;
987 }
988
989 if (notFound) {
990 this.findField.classList.add('notFound');
991 } else {
992 this.findField.classList.remove('notFound');
993 }
994
995 this.findField.setAttribute('data-status', status);
996 this.findMsg.textContent = findMsg;
997 },
998
999 open: function PDFFindBar_open() {
1000 if (!this.opened) {
1001 this.opened = true;
1002 this.toggleButton.classList.add('toggled');
1003 this.bar.classList.remove('hidden');
1004 }
1005 this.findField.select();
1006 this.findField.focus();
1007 },
1008
1009 close: function PDFFindBar_close() {
1010 if (!this.opened) {
1011 return;
1012 }
1013 this.opened = false;
1014 this.toggleButton.classList.remove('toggled');
1015 this.bar.classList.add('hidden');
1016 this.findController.active = false;
1017 },
1018
1019 toggle: function PDFFindBar_toggle() {
1020 if (this.opened) {
1021 this.close();
1022 } else {
1023 this.open();
1024 }
1025 }
1026 };
1027 return PDFFindBar;
1028})();
1029
1030
1031
1032var FindStates = {
1033 FIND_FOUND: 0,
1034 FIND_NOTFOUND: 1,
1035 FIND_WRAPPED: 2,
1036 FIND_PENDING: 3
1037};
1038
1039/**
1040 * Provides "search" or "find" functionality for the PDF.
1041 * This object actually performs the search for a given string.
1042 */
1043var PDFFindController = (function PDFFindControllerClosure() {
1044 function PDFFindController(options) {
1045 this.startedTextExtraction = false;
1046 this.extractTextPromises = [];
1047 this.pendingFindMatches = {};
1048 this.active = false; // If active, find results will be highlighted.
1049 this.pageContents = []; // Stores the text for each page.
1050 this.pageMatches = [];
1051 this.selected = { // Currently selected match.
1052 pageIdx: -1,
1053 matchIdx: -1
1054 };
1055 this.offset = { // Where the find algorithm currently is in the document.
1056 pageIdx: null,
1057 matchIdx: null
1058 };
1059 this.pagesToSearch = null;
1060 this.resumePageIdx = null;
1061 this.state = null;
1062 this.dirtyMatch = false;
1063 this.findTimeout = null;
1064 this.pdfViewer = options.pdfViewer || null;
1065 this.integratedFind = options.integratedFind || false;
1066 this.charactersToNormalize = {
1067 '\u2018': '\'', // Left single quotation mark
1068 '\u2019': '\'', // Right single quotation mark
1069 '\u201A': '\'', // Single low-9 quotation mark
1070 '\u201B': '\'', // Single high-reversed-9 quotation mark
1071 '\u201C': '"', // Left double quotation mark
1072 '\u201D': '"', // Right double quotation mark
1073 '\u201E': '"', // Double low-9 quotation mark
1074 '\u201F': '"', // Double high-reversed-9 quotation mark
1075 '\u00BC': '1/4', // Vulgar fraction one quarter
1076 '\u00BD': '1/2', // Vulgar fraction one half
1077 '\u00BE': '3/4' // Vulgar fraction three quarters
1078 };
1079 this.findBar = options.findBar || null;
1080
1081 // Compile the regular expression for text normalization once
1082 var replace = Object.keys(this.charactersToNormalize).join('');
1083 this.normalizationRegex = new RegExp('[' + replace + ']', 'g');
1084
1085 var events = [
1086 'find',
1087 'findagain',
1088 'findhighlightallchange',
1089 'findcasesensitivitychange'
1090 ];
1091
1092 this.firstPagePromise = new Promise(function (resolve) {
1093 this.resolveFirstPage = resolve;
1094 }.bind(this));
1095 this.handleEvent = this.handleEvent.bind(this);
1096
1097 for (var i = 0, len = events.length; i < len; i++) {
1098 window.addEventListener(events[i], this.handleEvent);
1099 }
1100 }
1101
1102 PDFFindController.prototype = {
1103 setFindBar: function PDFFindController_setFindBar(findBar) {
1104 this.findBar = findBar;
1105 },
1106
1107 reset: function PDFFindController_reset() {
1108 this.startedTextExtraction = false;
1109 this.extractTextPromises = [];
1110 this.active = false;
1111 },
1112
1113 normalize: function PDFFindController_normalize(text) {
1114 var self = this;
1115 return text.replace(this.normalizationRegex, function (ch) {
1116 return self.charactersToNormalize[ch];
1117 });
1118 },
1119
1120 calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) {
1121 var pageContent = this.normalize(this.pageContents[pageIndex]);
1122 var query = this.normalize(this.state.query);
1123 var caseSensitive = this.state.caseSensitive;
1124 var queryLen = query.length;
1125
1126 if (queryLen === 0) {
1127 return; // Do nothing: the matches should be wiped out already.
1128 }
1129
1130 if (!caseSensitive) {
1131 pageContent = pageContent.toLowerCase();
1132 query = query.toLowerCase();
1133 }
1134
1135 var matches = [];
1136 var matchIdx = -queryLen;
1137 while (true) {
1138 matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
1139 if (matchIdx === -1) {
1140 break;
1141 }
1142 matches.push(matchIdx);
1143 }
1144 this.pageMatches[pageIndex] = matches;
1145 this.updatePage(pageIndex);
1146 if (this.resumePageIdx === pageIndex) {
1147 this.resumePageIdx = null;
1148 this.nextPageMatch();
1149 }
1150 },
1151
1152 extractText: function PDFFindController_extractText() {
1153 if (this.startedTextExtraction) {
1154 return;
1155 }
1156 this.startedTextExtraction = true;
1157
1158 this.pageContents = [];
1159 var extractTextPromisesResolves = [];
1160 var numPages = this.pdfViewer.pagesCount;
1161 for (var i = 0; i < numPages; i++) {
1162 this.extractTextPromises.push(new Promise(function (resolve) {
1163 extractTextPromisesResolves.push(resolve);
1164 }));
1165 }
1166
1167 var self = this;
1168 function extractPageText(pageIndex) {
1169 self.pdfViewer.getPageTextContent(pageIndex).then(
1170 function textContentResolved(textContent) {
1171 var textItems = textContent.items;
1172 var str = [];
1173
1174 for (var i = 0, len = textItems.length; i < len; i++) {
1175 str.push(textItems[i].str);
1176 }
1177
1178 // Store the pageContent as a string.
1179 self.pageContents.push(str.join(''));
1180
1181 extractTextPromisesResolves[pageIndex](pageIndex);
1182 if ((pageIndex + 1) < self.pdfViewer.pagesCount) {
1183 extractPageText(pageIndex + 1);
1184 }
1185 }
1186 );
1187 }
1188 extractPageText(0);
1189 },
1190
1191 handleEvent: function PDFFindController_handleEvent(e) {
1192 if (this.state === null || e.type !== 'findagain') {
1193 this.dirtyMatch = true;
1194 }
1195 this.state = e.detail;
1196 this.updateUIState(FindStates.FIND_PENDING);
1197
1198 this.firstPagePromise.then(function() {
1199 this.extractText();
1200
1201 clearTimeout(this.findTimeout);
1202 if (e.type === 'find') {
1203 // Only trigger the find action after 250ms of silence.
1204 this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
1205 } else {
1206 this.nextMatch();
1207 }
1208 }.bind(this));
1209 },
1210
1211 updatePage: function PDFFindController_updatePage(index) {
1212 var page = this.pdfViewer.getPageView(index);
1213
1214 if (this.selected.pageIdx === index) {
1215 // If the page is selected, scroll the page into view, which triggers
1216 // rendering the page, which adds the textLayer. Once the textLayer is
1217 // build, it will scroll onto the selected match.
1218 this.pdfViewer.scrollPageIntoView(index + 1);
1219 }
1220
1221 if (page.textLayer) {
1222 page.textLayer.updateMatches();
1223 }
1224 },
1225
1226 nextMatch: function PDFFindController_nextMatch() {
1227 var previous = this.state.findPrevious;
1228 var currentPageIndex = this.pdfViewer.currentPageNumber - 1;
1229 var numPages = this.pdfViewer.pagesCount;
1230
1231 this.active = true;
1232
1233 if (this.dirtyMatch) {
1234 // Need to recalculate the matches, reset everything.
1235 this.dirtyMatch = false;
1236 this.selected.pageIdx = this.selected.matchIdx = -1;
1237 this.offset.pageIdx = currentPageIndex;
1238 this.offset.matchIdx = null;
1239 this.hadMatch = false;
1240 this.resumePageIdx = null;
1241 this.pageMatches = [];
1242 var self = this;
1243
1244 for (var i = 0; i < numPages; i++) {
1245 // Wipe out any previous highlighted matches.
1246 this.updatePage(i);
1247
1248 // As soon as the text is extracted start finding the matches.
1249 if (!(i in this.pendingFindMatches)) {
1250 this.pendingFindMatches[i] = true;
1251 this.extractTextPromises[i].then(function(pageIdx) {
1252 delete self.pendingFindMatches[pageIdx];
1253 self.calcFindMatch(pageIdx);
1254 });
1255 }
1256 }
1257 }
1258
1259 // If there's no query there's no point in searching.
1260 if (this.state.query === '') {
1261 this.updateUIState(FindStates.FIND_FOUND);
1262 return;
1263 }
1264
1265 // If we're waiting on a page, we return since we can't do anything else.
1266 if (this.resumePageIdx) {
1267 return;
1268 }
1269
1270 var offset = this.offset;
1271 // Keep track of how many pages we should maximally iterate through.
1272 this.pagesToSearch = numPages;
1273 // If there's already a matchIdx that means we are iterating through a
1274 // page's matches.
1275 if (offset.matchIdx !== null) {
1276 var numPageMatches = this.pageMatches[offset.pageIdx].length;
1277 if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
1278 (previous && offset.matchIdx > 0)) {
1279 // The simple case; we just have advance the matchIdx to select
1280 // the next match on the page.
1281 this.hadMatch = true;
1282 offset.matchIdx = (previous ? offset.matchIdx - 1 :
1283 offset.matchIdx + 1);
1284 this.updateMatch(true);
1285 return;
1286 }
1287 // We went beyond the current page's matches, so we advance to
1288 // the next page.
1289 this.advanceOffsetPage(previous);
1290 }
1291 // Start searching through the page.
1292 this.nextPageMatch();
1293 },
1294
1295 matchesReady: function PDFFindController_matchesReady(matches) {
1296 var offset = this.offset;
1297 var numMatches = matches.length;
1298 var previous = this.state.findPrevious;
1299
1300 if (numMatches) {
1301 // There were matches for the page, so initialize the matchIdx.
1302 this.hadMatch = true;
1303 offset.matchIdx = (previous ? numMatches - 1 : 0);
1304 this.updateMatch(true);
1305 return true;
1306 } else {
1307 // No matches, so attempt to search the next page.
1308 this.advanceOffsetPage(previous);
1309 if (offset.wrapped) {
1310 offset.matchIdx = null;
1311 if (this.pagesToSearch < 0) {
1312 // No point in wrapping again, there were no matches.
1313 this.updateMatch(false);
1314 // while matches were not found, searching for a page
1315 // with matches should nevertheless halt.
1316 return true;
1317 }
1318 }
1319 // Matches were not found (and searching is not done).
1320 return false;
1321 }
1322 },
1323
1324 nextPageMatch: function PDFFindController_nextPageMatch() {
1325 if (this.resumePageIdx !== null) {
1326 console.error('There can only be one pending page.');
1327 }
1328 do {
1329 var pageIdx = this.offset.pageIdx;
1330 var matches = this.pageMatches[pageIdx];
1331 if (!matches) {
1332 // The matches don't exist yet for processing by "matchesReady",
1333 // so set a resume point for when they do exist.
1334 this.resumePageIdx = pageIdx;
1335 break;
1336 }
1337 } while (!this.matchesReady(matches));
1338 },
1339
1340 advanceOffsetPage: function PDFFindController_advanceOffsetPage(previous) {
1341 var offset = this.offset;
1342 var numPages = this.extractTextPromises.length;
1343 offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1);
1344 offset.matchIdx = null;
1345
1346 this.pagesToSearch--;
1347
1348 if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
1349 offset.pageIdx = (previous ? numPages - 1 : 0);
1350 offset.wrapped = true;
1351 }
1352 },
1353
1354 updateMatch: function PDFFindController_updateMatch(found) {
1355 var state = FindStates.FIND_NOTFOUND;
1356 var wrapped = this.offset.wrapped;
1357 this.offset.wrapped = false;
1358
1359 if (found) {
1360 var previousPage = this.selected.pageIdx;
1361 this.selected.pageIdx = this.offset.pageIdx;
1362 this.selected.matchIdx = this.offset.matchIdx;
1363 state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND);
1364 // Update the currently selected page to wipe out any selected matches.
1365 if (previousPage !== -1 && previousPage !== this.selected.pageIdx) {
1366 this.updatePage(previousPage);
1367 }
1368 }
1369
1370 this.updateUIState(state, this.state.findPrevious);
1371 if (this.selected.pageIdx !== -1) {
1372 this.updatePage(this.selected.pageIdx);
1373 }
1374 },
1375
1376 updateUIState: function PDFFindController_updateUIState(state, previous) {
1377 if (this.integratedFind) {
1378 FirefoxCom.request('updateFindControlState',
1379 { result: state, findPrevious: previous });
1380 return;
1381 }
1382 if (this.findBar === null) {
1383 throw new Error('PDFFindController is not initialized with a ' +
1384 'PDFFindBar instance.');
1385 }
1386 this.findBar.updateUIState(state, previous);
1387 }
1388 };
1389 return PDFFindController;
1390})();
1391
1392
1393
1394var PDFHistory = {
1395 initialized: false,
1396 initialDestination: null,
1397
1398 /**
1399 * @param {string} fingerprint
1400 * @param {IPDFLinkService} linkService
1401 */
1402 initialize: function pdfHistoryInitialize(fingerprint, linkService) {
1403 this.initialized = true;
1404 this.reInitialized = false;
1405 this.allowHashChange = true;
1406 this.historyUnlocked = true;
1407
1408 this.previousHash = window.location.hash.substring(1);
1409 this.currentBookmark = '';
1410 this.currentPage = 0;
1411 this.updatePreviousBookmark = false;
1412 this.previousBookmark = '';
1413 this.previousPage = 0;
1414 this.nextHashParam = '';
1415
1416 this.fingerprint = fingerprint;
1417 this.linkService = linkService;
1418 this.currentUid = this.uid = 0;
1419 this.current = {};
1420
1421 var state = window.history.state;
1422 if (this._isStateObjectDefined(state)) {
1423 // This corresponds to navigating back to the document
1424 // from another page in the browser history.
1425 if (state.target.dest) {
1426 this.initialDestination = state.target.dest;
1427 } else {
1428 linkService.setHash(state.target.hash);
1429 }
1430 this.currentUid = state.uid;
1431 this.uid = state.uid + 1;
1432 this.current = state.target;
1433 } else {
1434 // This corresponds to the loading of a new document.
1435 if (state && state.fingerprint &&
1436 this.fingerprint !== state.fingerprint) {
1437 // Reinitialize the browsing history when a new document
1438 // is opened in the web viewer.
1439 this.reInitialized = true;
1440 }
1441 this._pushOrReplaceState({ fingerprint: this.fingerprint }, true);
1442 }
1443
1444 var self = this;
1445 window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
1446 evt.preventDefault();
1447 evt.stopPropagation();
1448
1449 if (!self.historyUnlocked) {
1450 return;
1451 }
1452 if (evt.state) {
1453 // Move back/forward in the history.
1454 self._goTo(evt.state);
1455 } else {
1456 // Handle the user modifying the hash of a loaded document.
1457 self.previousHash = window.location.hash.substring(1);
1458
1459 // If the history is empty when the hash changes,
1460 // update the previous entry in the browser history.
1461 if (self.uid === 0) {
1462 var previousParams = (self.previousHash && self.currentBookmark &&
1463 self.previousHash !== self.currentBookmark) ?
1464 { hash: self.currentBookmark, page: self.currentPage } :
1465 { page: 1 };
1466 self.historyUnlocked = false;
1467 self.allowHashChange = false;
1468 window.history.back();
1469 self._pushToHistory(previousParams, false, true);
1470 window.history.forward();
1471 self.historyUnlocked = true;
1472 }
1473 self._pushToHistory({ hash: self.previousHash }, false, true);
1474 self._updatePreviousBookmark();
1475 }
1476 }, false);
1477
1478 function pdfHistoryBeforeUnload() {
1479 var previousParams = self._getPreviousParams(null, true);
1480 if (previousParams) {
1481 var replacePrevious = (!self.current.dest &&
1482 self.current.hash !== self.previousHash);
1483 self._pushToHistory(previousParams, false, replacePrevious);
1484 self._updatePreviousBookmark();
1485 }
1486 // Remove the event listener when navigating away from the document,
1487 // since 'beforeunload' prevents Firefox from caching the document.
1488 window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false);
1489 }
1490 window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
1491
1492 window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
1493 // If the entire viewer (including the PDF file) is cached in the browser,
1494 // we need to reattach the 'beforeunload' event listener since
1495 // the 'DOMContentLoaded' event is not fired on 'pageshow'.
1496 window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
1497 }, false);
1498 },
1499
1500 _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
1501 return (state && state.uid >= 0 &&
1502 state.fingerprint && this.fingerprint === state.fingerprint &&
1503 state.target && state.target.hash) ? true : false;
1504 },
1505
1506 _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj,
1507 replace) {
1508 if (replace) {
1509 window.history.replaceState(stateObj, '', document.URL);
1510 } else {
1511 window.history.pushState(stateObj, '', document.URL);
1512 }
1513 },
1514
1515 get isHashChangeUnlocked() {
1516 if (!this.initialized) {
1517 return true;
1518 }
1519 // If the current hash changes when moving back/forward in the history,
1520 // this will trigger a 'popstate' event *as well* as a 'hashchange' event.
1521 // Since the hash generally won't correspond to the exact the position
1522 // stored in the history's state object, triggering the 'hashchange' event
1523 // can thus corrupt the browser history.
1524 //
1525 // When the hash changes during a 'popstate' event, we *only* prevent the
1526 // first 'hashchange' event and immediately reset allowHashChange.
1527 // If it is not reset, the user would not be able to change the hash.
1528
1529 var temp = this.allowHashChange;
1530 this.allowHashChange = true;
1531 return temp;
1532 },
1533
1534 _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() {
1535 if (this.updatePreviousBookmark &&
1536 this.currentBookmark && this.currentPage) {
1537 this.previousBookmark = this.currentBookmark;
1538 this.previousPage = this.currentPage;
1539 this.updatePreviousBookmark = false;
1540 }
1541 },
1542
1543 updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark,
1544 pageNum) {
1545 if (this.initialized) {
1546 this.currentBookmark = bookmark.substring(1);
1547 this.currentPage = pageNum | 0;
1548 this._updatePreviousBookmark();
1549 }
1550 },
1551
1552 updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
1553 if (this.initialized) {
1554 this.nextHashParam = param;
1555 }
1556 },
1557
1558 push: function pdfHistoryPush(params, isInitialBookmark) {
1559 if (!(this.initialized && this.historyUnlocked)) {
1560 return;
1561 }
1562 if (params.dest && !params.hash) {
1563 params.hash = (this.current.hash && this.current.dest &&
1564 this.current.dest === params.dest) ?
1565 this.current.hash :
1566 this.linkService.getDestinationHash(params.dest).split('#')[1];
1567 }
1568 if (params.page) {
1569 params.page |= 0;
1570 }
1571 if (isInitialBookmark) {
1572 var target = window.history.state.target;
1573 if (!target) {
1574 // Invoked when the user specifies an initial bookmark,
1575 // thus setting initialBookmark, when the document is loaded.
1576 this._pushToHistory(params, false);
1577 this.previousHash = window.location.hash.substring(1);
1578 }
1579 this.updatePreviousBookmark = this.nextHashParam ? false : true;
1580 if (target) {
1581 // If the current document is reloaded,
1582 // avoid creating duplicate entries in the history.
1583 this._updatePreviousBookmark();
1584 }
1585 return;
1586 }
1587 if (this.nextHashParam) {
1588 if (this.nextHashParam === params.hash) {
1589 this.nextHashParam = null;
1590 this.updatePreviousBookmark = true;
1591 return;
1592 } else {
1593 this.nextHashParam = null;
1594 }
1595 }
1596
1597 if (params.hash) {
1598 if (this.current.hash) {
1599 if (this.current.hash !== params.hash) {
1600 this._pushToHistory(params, true);
1601 } else {
1602 if (!this.current.page && params.page) {
1603 this._pushToHistory(params, false, true);
1604 }
1605 this.updatePreviousBookmark = true;
1606 }
1607 } else {
1608 this._pushToHistory(params, true);
1609 }
1610 } else if (this.current.page && params.page &&
1611 this.current.page !== params.page) {
1612 this._pushToHistory(params, true);
1613 }
1614 },
1615
1616 _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage,
1617 beforeUnload) {
1618 if (!(this.currentBookmark && this.currentPage)) {
1619 return null;
1620 } else if (this.updatePreviousBookmark) {
1621 this.updatePreviousBookmark = false;
1622 }
1623 if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
1624 // Prevent the history from getting stuck in the current state,
1625 // effectively preventing the user from going back/forward in the history.
1626 //
1627 // This happens if the current position in the document didn't change when
1628 // the history was previously updated. The reasons for this are either:
1629 // 1. The current zoom value is such that the document does not need to,
1630 // or cannot, be scrolled to display the destination.
1631 // 2. The previous destination is broken, and doesn't actally point to a
1632 // position within the document.
1633 // (This is either due to a bad PDF generator, or the user making a
1634 // mistake when entering a destination in the hash parameters.)
1635 return null;
1636 }
1637 if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
1638 if (this.previousBookmark === this.currentBookmark) {
1639 return null;
1640 }
1641 } else if (this.current.page || onlyCheckPage) {
1642 if (this.previousPage === this.currentPage) {
1643 return null;
1644 }
1645 } else {
1646 return null;
1647 }
1648 var params = { hash: this.currentBookmark, page: this.currentPage };
1649 if (PresentationMode.active) {
1650 params.hash = null;
1651 }
1652 return params;
1653 },
1654
1655 _stateObj: function pdfHistory_stateObj(params) {
1656 return { fingerprint: this.fingerprint, uid: this.uid, target: params };
1657 },
1658
1659 _pushToHistory: function pdfHistory_pushToHistory(params,
1660 addPrevious, overwrite) {
1661 if (!this.initialized) {
1662 return;
1663 }
1664 if (!params.hash && params.page) {
1665 params.hash = ('page=' + params.page);
1666 }
1667 if (addPrevious && !overwrite) {
1668 var previousParams = this._getPreviousParams();
1669 if (previousParams) {
1670 var replacePrevious = (!this.current.dest &&
1671 this.current.hash !== this.previousHash);
1672 this._pushToHistory(previousParams, false, replacePrevious);
1673 }
1674 }
1675 this._pushOrReplaceState(this._stateObj(params),
1676 (overwrite || this.uid === 0));
1677 this.currentUid = this.uid++;
1678 this.current = params;
1679 this.updatePreviousBookmark = true;
1680 },
1681
1682 _goTo: function pdfHistory_goTo(state) {
1683 if (!(this.initialized && this.historyUnlocked &&
1684 this._isStateObjectDefined(state))) {
1685 return;
1686 }
1687 if (!this.reInitialized && state.uid < this.currentUid) {
1688 var previousParams = this._getPreviousParams(true);
1689 if (previousParams) {
1690 this._pushToHistory(this.current, false);
1691 this._pushToHistory(previousParams, false);
1692 this.currentUid = state.uid;
1693 window.history.back();
1694 return;
1695 }
1696 }
1697 this.historyUnlocked = false;
1698
1699 if (state.target.dest) {
1700 this.linkService.navigateTo(state.target.dest);
1701 } else {
1702 this.linkService.setHash(state.target.hash);
1703 }
1704 this.currentUid = state.uid;
1705 if (state.uid > this.uid) {
1706 this.uid = state.uid;
1707 }
1708 this.current = state.target;
1709 this.updatePreviousBookmark = true;
1710
1711 var currentHash = window.location.hash.substring(1);
1712 if (this.previousHash !== currentHash) {
1713 this.allowHashChange = false;
1714 }
1715 this.previousHash = currentHash;
1716
1717 this.historyUnlocked = true;
1718 },
1719
1720 back: function pdfHistoryBack() {
1721 this.go(-1);
1722 },
1723
1724 forward: function pdfHistoryForward() {
1725 this.go(1);
1726 },
1727
1728 go: function pdfHistoryGo(direction) {
1729 if (this.initialized && this.historyUnlocked) {
1730 var state = window.history.state;
1731 if (direction === -1 && state && state.uid > 0) {
1732 window.history.back();
1733 } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
1734 window.history.forward();
1735 }
1736 }
1737 }
1738};
1739
1740
1741var SecondaryToolbar = {
1742 opened: false,
1743 previousContainerHeight: null,
1744 newContainerHeight: null,
1745
1746 initialize: function secondaryToolbarInitialize(options) {
1747 this.toolbar = options.toolbar;
1748 this.presentationMode = options.presentationMode;
1749 this.documentProperties = options.documentProperties;
1750 this.buttonContainer = this.toolbar.firstElementChild;
1751
1752 // Define the toolbar buttons.
1753 this.toggleButton = options.toggleButton;
1754 this.presentationModeButton = options.presentationModeButton;
1755 this.openFile = options.openFile;
1756 this.print = options.print;
1757 this.download = options.download;
1758 this.viewBookmark = options.viewBookmark;
1759 this.firstPage = options.firstPage;
1760 this.lastPage = options.lastPage;
1761 this.pageRotateCw = options.pageRotateCw;
1762 this.pageRotateCcw = options.pageRotateCcw;
1763 this.documentPropertiesButton = options.documentPropertiesButton;
1764
1765 // Attach the event listeners.
1766 var elements = [
1767 // Button to toggle the visibility of the secondary toolbar:
1768 { element: this.toggleButton, handler: this.toggle },
1769 // All items within the secondary toolbar
1770 // (except for toggleHandTool, hand_tool.js is responsible for it):
1771 { element: this.presentationModeButton,
1772 handler: this.presentationModeClick },
1773 { element: this.openFile, handler: this.openFileClick },
1774 { element: this.print, handler: this.printClick },
1775 { element: this.download, handler: this.downloadClick },
1776 { element: this.viewBookmark, handler: this.viewBookmarkClick },
1777 { element: this.firstPage, handler: this.firstPageClick },
1778 { element: this.lastPage, handler: this.lastPageClick },
1779 { element: this.pageRotateCw, handler: this.pageRotateCwClick },
1780 { element: this.pageRotateCcw, handler: this.pageRotateCcwClick },
1781 { element: this.documentPropertiesButton,
1782 handler: this.documentPropertiesClick }
1783 ];
1784
1785 for (var item in elements) {
1786 var element = elements[item].element;
1787 if (element) {
1788 element.addEventListener('click', elements[item].handler.bind(this));
1789 }
1790 }
1791 },
1792
1793 // Event handling functions.
1794 presentationModeClick: function secondaryToolbarPresentationModeClick(evt) {
1795 this.presentationMode.request();
1796 this.close();
1797 },
1798
1799 openFileClick: function secondaryToolbarOpenFileClick(evt) {
1800 document.getElementById('fileInput').click();
1801 this.close();
1802 },
1803
1804 printClick: function secondaryToolbarPrintClick(evt) {
1805 window.print();
1806 this.close();
1807 },
1808
1809 downloadClick: function secondaryToolbarDownloadClick(evt) {
1810 PDFViewerApplication.download();
1811 this.close();
1812 },
1813
1814 viewBookmarkClick: function secondaryToolbarViewBookmarkClick(evt) {
1815 this.close();
1816 },
1817
1818 firstPageClick: function secondaryToolbarFirstPageClick(evt) {
1819 PDFViewerApplication.page = 1;
1820 this.close();
1821 },
1822
1823 lastPageClick: function secondaryToolbarLastPageClick(evt) {
1824 if (PDFViewerApplication.pdfDocument) {
1825 PDFViewerApplication.page = PDFViewerApplication.pagesCount;
1826 }
1827 this.close();
1828 },
1829
1830 pageRotateCwClick: function secondaryToolbarPageRotateCwClick(evt) {
1831 PDFViewerApplication.rotatePages(90);
1832 },
1833
1834 pageRotateCcwClick: function secondaryToolbarPageRotateCcwClick(evt) {
1835 PDFViewerApplication.rotatePages(-90);
1836 },
1837
1838 documentPropertiesClick: function secondaryToolbarDocumentPropsClick(evt) {
1839 this.documentProperties.open();
1840 this.close();
1841 },
1842
1843 // Misc. functions for interacting with the toolbar.
1844 setMaxHeight: function secondaryToolbarSetMaxHeight(container) {
1845 if (!container || !this.buttonContainer) {
1846 return;
1847 }
1848 this.newContainerHeight = container.clientHeight;
1849 if (this.previousContainerHeight === this.newContainerHeight) {
1850 return;
1851 }
1852 this.buttonContainer.setAttribute('style',
1853 'max-height: ' + (this.newContainerHeight - SCROLLBAR_PADDING) + 'px;');
1854 this.previousContainerHeight = this.newContainerHeight;
1855 },
1856
1857 open: function secondaryToolbarOpen() {
1858 if (this.opened) {
1859 return;
1860 }
1861 this.opened = true;
1862 this.toggleButton.classList.add('toggled');
1863 this.toolbar.classList.remove('hidden');
1864 },
1865
1866 close: function secondaryToolbarClose(target) {
1867 if (!this.opened) {
1868 return;
1869 } else if (target && !this.toolbar.contains(target)) {
1870 return;
1871 }
1872 this.opened = false;
1873 this.toolbar.classList.add('hidden');
1874 this.toggleButton.classList.remove('toggled');
1875 },
1876
1877 toggle: function secondaryToolbarToggle() {
1878 if (this.opened) {
1879 this.close();
1880 } else {
1881 this.open();
1882 }
1883 }
1884};
1885
1886
1887var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms
1888var SELECTOR = 'presentationControls';
1889var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms
1890
1891var PresentationMode = {
1892 active: false,
1893 args: null,
1894 contextMenuOpen: false,
1895 prevCoords: { x: null, y: null },
1896
1897 initialize: function presentationModeInitialize(options) {
1898 this.container = options.container;
1899 this.secondaryToolbar = options.secondaryToolbar;
1900
1901 this.viewer = this.container.firstElementChild;
1902
1903 this.firstPage = options.firstPage;
1904 this.lastPage = options.lastPage;
1905 this.pageRotateCw = options.pageRotateCw;
1906 this.pageRotateCcw = options.pageRotateCcw;
1907
1908 this.firstPage.addEventListener('click', function() {
1909 this.contextMenuOpen = false;
1910 this.secondaryToolbar.firstPageClick();
1911 }.bind(this));
1912 this.lastPage.addEventListener('click', function() {
1913 this.contextMenuOpen = false;
1914 this.secondaryToolbar.lastPageClick();
1915 }.bind(this));
1916
1917 this.pageRotateCw.addEventListener('click', function() {
1918 this.contextMenuOpen = false;
1919 this.secondaryToolbar.pageRotateCwClick();
1920 }.bind(this));
1921 this.pageRotateCcw.addEventListener('click', function() {
1922 this.contextMenuOpen = false;
1923 this.secondaryToolbar.pageRotateCcwClick();
1924 }.bind(this));
1925 },
1926
1927 get isFullscreen() {
1928 return (document.fullscreenElement ||
1929 document.mozFullScreen ||
1930 document.webkitIsFullScreen ||
1931 document.msFullscreenElement);
1932 },
1933
1934 /**
1935 * Initialize a timeout that is used to specify switchInProgress when the
1936 * browser transitions to fullscreen mode. Since resize events are triggered
1937 * multiple times during the switch to fullscreen mode, this is necessary in
1938 * order to prevent the page from being scrolled partially, or completely,
1939 * out of view when Presentation Mode is enabled.
1940 * Note: This is only an issue at certain zoom levels, e.g. 'page-width'.
1941 */
1942 _setSwitchInProgress: function presentationMode_setSwitchInProgress() {
1943 if (this.switchInProgress) {
1944 clearTimeout(this.switchInProgress);
1945 }
1946 this.switchInProgress = setTimeout(function switchInProgressTimeout() {
1947 delete this.switchInProgress;
1948 this._notifyStateChange();
1949 }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS);
1950 },
1951
1952 _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() {
1953 if (this.switchInProgress) {
1954 clearTimeout(this.switchInProgress);
1955 delete this.switchInProgress;
1956 }
1957 },
1958
1959 request: function presentationModeRequest() {
1960 if (!PDFViewerApplication.supportsFullscreen || this.isFullscreen ||
1961 !this.viewer.hasChildNodes()) {
1962 return false;
1963 }
1964 this._setSwitchInProgress();
1965 this._notifyStateChange();
1966
1967 if (this.container.requestFullscreen) {
1968 this.container.requestFullscreen();
1969 } else if (this.container.mozRequestFullScreen) {
1970 this.container.mozRequestFullScreen();
1971 } else if (this.container.webkitRequestFullScreen) {
1972 this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
1973 } else if (this.container.msRequestFullscreen) {
1974 this.container.msRequestFullscreen();
1975 } else {
1976 return false;
1977 }
1978
1979 this.args = {
1980 page: PDFViewerApplication.page,
1981 previousScale: PDFViewerApplication.currentScaleValue
1982 };
1983
1984 return true;
1985 },
1986
1987 _notifyStateChange: function presentationModeNotifyStateChange() {
1988 var event = document.createEvent('CustomEvent');
1989 event.initCustomEvent('presentationmodechanged', true, true, {
1990 active: PresentationMode.active,
1991 switchInProgress: !!PresentationMode.switchInProgress
1992 });
1993 window.dispatchEvent(event);
1994 },
1995
1996 enter: function presentationModeEnter() {
1997 this.active = true;
1998 this._resetSwitchInProgress();
1999 this._notifyStateChange();
2000
2001 // Ensure that the correct page is scrolled into view when entering
2002 // Presentation Mode, by waiting until fullscreen mode in enabled.
2003 // Note: This is only necessary in non-Mozilla browsers.
2004 setTimeout(function enterPresentationModeTimeout() {
2005 PDFViewerApplication.page = this.args.page;
2006 PDFViewerApplication.setScale('page-fit', true);
2007 }.bind(this), 0);
2008
2009 window.addEventListener('mousemove', this.mouseMove, false);
2010 window.addEventListener('mousedown', this.mouseDown, false);
2011 window.addEventListener('contextmenu', this.contextMenu, false);
2012
2013 this.showControls();
2014 HandTool.enterPresentationMode();
2015 this.contextMenuOpen = false;
2016 this.container.setAttribute('contextmenu', 'viewerContextMenu');
2017
2018 // Text selection is disabled in Presentation Mode, thus it's not possible
2019 // for the user to deselect text that is selected (e.g. with "Select all")
2020 // when entering Presentation Mode, hence we remove any active selection.
2021 window.getSelection().removeAllRanges();
2022 },
2023
2024 exit: function presentationModeExit() {
2025 var page = PDFViewerApplication.page;
2026
2027 // Ensure that the correct page is scrolled into view when exiting
2028 // Presentation Mode, by waiting until fullscreen mode is disabled.
2029 // Note: This is only necessary in non-Mozilla browsers.
2030 setTimeout(function exitPresentationModeTimeout() {
2031 this.active = false;
2032 this._notifyStateChange();
2033
2034 PDFViewerApplication.setScale(this.args.previousScale, true);
2035 PDFViewerApplication.page = page;
2036 this.args = null;
2037 }.bind(this), 0);
2038
2039 window.removeEventListener('mousemove', this.mouseMove, false);
2040 window.removeEventListener('mousedown', this.mouseDown, false);
2041 window.removeEventListener('contextmenu', this.contextMenu, false);
2042
2043 this.hideControls();
2044 PDFViewerApplication.clearMouseScrollState();
2045 HandTool.exitPresentationMode();
2046 this.container.removeAttribute('contextmenu');
2047 this.contextMenuOpen = false;
2048
2049 // Ensure that the thumbnail of the current page is visible
2050 // when exiting presentation mode.
2051 scrollIntoView(document.getElementById('thumbnailContainer' + page));
2052 },
2053
2054 showControls: function presentationModeShowControls() {
2055 if (this.controlsTimeout) {
2056 clearTimeout(this.controlsTimeout);
2057 } else {
2058 this.container.classList.add(SELECTOR);
2059 }
2060 this.controlsTimeout = setTimeout(function hideControlsTimeout() {
2061 this.container.classList.remove(SELECTOR);
2062 delete this.controlsTimeout;
2063 }.bind(this), DELAY_BEFORE_HIDING_CONTROLS);
2064 },
2065
2066 hideControls: function presentationModeHideControls() {
2067 if (!this.controlsTimeout) {
2068 return;
2069 }
2070 this.container.classList.remove(SELECTOR);
2071 clearTimeout(this.controlsTimeout);
2072 delete this.controlsTimeout;
2073 },
2074
2075 mouseMove: function presentationModeMouseMove(evt) {
2076 // Workaround for a bug in WebKit browsers that causes the 'mousemove' event
2077 // to be fired when the cursor is changed. For details, see:
2078 // http://code.google.com/p/chromium/issues/detail?id=103041.
2079
2080 var currCoords = { x: evt.clientX, y: evt.clientY };
2081 var prevCoords = PresentationMode.prevCoords;
2082 PresentationMode.prevCoords = currCoords;
2083
2084 if (currCoords.x === prevCoords.x && currCoords.y === prevCoords.y) {
2085 return;
2086 }
2087 PresentationMode.showControls();
2088 },
2089
2090 mouseDown: function presentationModeMouseDown(evt) {
2091 var self = PresentationMode;
2092 if (self.contextMenuOpen) {
2093 self.contextMenuOpen = false;
2094 evt.preventDefault();
2095 return;
2096 }
2097
2098 if (evt.button === 0) {
2099 // Enable clicking of links in presentation mode. Please note:
2100 // Only links pointing to destinations in the current PDF document work.
2101 var isInternalLink = (evt.target.href &&
2102 evt.target.classList.contains('internalLink'));
2103 if (!isInternalLink) {
2104 // Unless an internal link was clicked, advance one page.
2105 evt.preventDefault();
2106 PDFViewerApplication.page += (evt.shiftKey ? -1 : 1);
2107 }
2108 }
2109 },
2110
2111 contextMenu: function presentationModeContextMenu(evt) {
2112 PresentationMode.contextMenuOpen = true;
2113 }
2114};
2115
2116(function presentationModeClosure() {
2117 function presentationModeChange(e) {
2118 if (PresentationMode.isFullscreen) {
2119 PresentationMode.enter();
2120 } else {
2121 PresentationMode.exit();
2122 }
2123 }
2124
2125 window.addEventListener('fullscreenchange', presentationModeChange, false);
2126 window.addEventListener('mozfullscreenchange', presentationModeChange, false);
2127 window.addEventListener('webkitfullscreenchange', presentationModeChange,
2128 false);
2129 window.addEventListener('MSFullscreenChange', presentationModeChange, false);
2130})();
2131
2132
2133/* Copyright 2013 Rob Wu <gwnRob@gmail.com>
2134 * https://github.com/Rob--W/grab-to-pan.js
2135 *
2136 * Licensed under the Apache License, Version 2.0 (the "License");
2137 * you may not use this file except in compliance with the License.
2138 * You may obtain a copy of the License at
2139 *
2140 * http://www.apache.org/licenses/LICENSE-2.0
2141 *
2142 * Unless required by applicable law or agreed to in writing, software
2143 * distributed under the License is distributed on an "AS IS" BASIS,
2144 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2145 * See the License for the specific language governing permissions and
2146 * limitations under the License.
2147 */
2148
2149'use strict';
2150
2151var GrabToPan = (function GrabToPanClosure() {
2152 /**
2153 * Construct a GrabToPan instance for a given HTML element.
2154 * @param options.element {Element}
2155 * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)`
2156 * @param options.onActiveChanged {function(boolean)} optional. Called
2157 * when grab-to-pan is (de)activated. The first argument is a boolean that
2158 * shows whether grab-to-pan is activated.
2159 */
2160 function GrabToPan(options) {
2161 this.element = options.element;
2162 this.document = options.element.ownerDocument;
2163 if (typeof options.ignoreTarget === 'function') {
2164 this.ignoreTarget = options.ignoreTarget;
2165 }
2166 this.onActiveChanged = options.onActiveChanged;
2167
2168 // Bind the contexts to ensure that `this` always points to
2169 // the GrabToPan instance.
2170 this.activate = this.activate.bind(this);
2171 this.deactivate = this.deactivate.bind(this);
2172 this.toggle = this.toggle.bind(this);
2173 this._onmousedown = this._onmousedown.bind(this);
2174 this._onmousemove = this._onmousemove.bind(this);
2175 this._endPan = this._endPan.bind(this);
2176
2177 // This overlay will be inserted in the document when the mouse moves during
2178 // a grab operation, to ensure that the cursor has the desired appearance.
2179 var overlay = this.overlay = document.createElement('div');
2180 overlay.className = 'grab-to-pan-grabbing';
2181 }
2182 GrabToPan.prototype = {
2183 /**
2184 * Class name of element which can be grabbed
2185 */
2186 CSS_CLASS_GRAB: 'grab-to-pan-grab',
2187
2188 /**
2189 * Bind a mousedown event to the element to enable grab-detection.
2190 */
2191 activate: function GrabToPan_activate() {
2192 if (!this.active) {
2193 this.active = true;
2194 this.element.addEventListener('mousedown', this._onmousedown, true);
2195 this.element.classList.add(this.CSS_CLASS_GRAB);
2196 if (this.onActiveChanged) {
2197 this.onActiveChanged(true);
2198 }
2199 }
2200 },
2201
2202 /**
2203 * Removes all events. Any pending pan session is immediately stopped.
2204 */
2205 deactivate: function GrabToPan_deactivate() {
2206 if (this.active) {
2207 this.active = false;
2208 this.element.removeEventListener('mousedown', this._onmousedown, true);
2209 this._endPan();
2210 this.element.classList.remove(this.CSS_CLASS_GRAB);
2211 if (this.onActiveChanged) {
2212 this.onActiveChanged(false);
2213 }
2214 }
2215 },
2216
2217 toggle: function GrabToPan_toggle() {
2218 if (this.active) {
2219 this.deactivate();
2220 } else {
2221 this.activate();
2222 }
2223 },
2224
2225 /**
2226 * Whether to not pan if the target element is clicked.
2227 * Override this method to change the default behaviour.
2228 *
2229 * @param node {Element} The target of the event
2230 * @return {boolean} Whether to not react to the click event.
2231 */
2232 ignoreTarget: function GrabToPan_ignoreTarget(node) {
2233 // Use matchesSelector to check whether the clicked element
2234 // is (a child of) an input element / link
2235 return node[matchesSelector](
2236 'a[href], a[href] *, input, textarea, button, button *, select, option'
2237 );
2238 },
2239
2240 /**
2241 * @private
2242 */
2243 _onmousedown: function GrabToPan__onmousedown(event) {
2244 if (event.button !== 0 || this.ignoreTarget(event.target)) {
2245 return;
2246 }
2247 if (event.originalTarget) {
2248 try {
2249 /* jshint expr:true */
2250 event.originalTarget.tagName;
2251 } catch (e) {
2252 // Mozilla-specific: element is a scrollbar (XUL element)
2253 return;
2254 }
2255 }
2256
2257 this.scrollLeftStart = this.element.scrollLeft;
2258 this.scrollTopStart = this.element.scrollTop;
2259 this.clientXStart = event.clientX;
2260 this.clientYStart = event.clientY;
2261 this.document.addEventListener('mousemove', this._onmousemove, true);
2262 this.document.addEventListener('mouseup', this._endPan, true);
2263 // When a scroll event occurs before a mousemove, assume that the user
2264 // dragged a scrollbar (necessary for Opera Presto, Safari and IE)
2265 // (not needed for Chrome/Firefox)
2266 this.element.addEventListener('scroll', this._endPan, true);
2267 event.preventDefault();
2268 event.stopPropagation();
2269 this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING);
2270
2271 var focusedElement = document.activeElement;
2272 if (focusedElement && !focusedElement.contains(event.target)) {
2273 focusedElement.blur();
2274 }
2275 },
2276
2277 /**
2278 * @private
2279 */
2280 _onmousemove: function GrabToPan__onmousemove(event) {
2281 this.element.removeEventListener('scroll', this._endPan, true);
2282 if (isLeftMouseReleased(event)) {
2283 this._endPan();
2284 return;
2285 }
2286 var xDiff = event.clientX - this.clientXStart;
2287 var yDiff = event.clientY - this.clientYStart;
2288 this.element.scrollTop = this.scrollTopStart - yDiff;
2289 this.element.scrollLeft = this.scrollLeftStart - xDiff;
2290 if (!this.overlay.parentNode) {
2291 document.body.appendChild(this.overlay);
2292 }
2293 },
2294
2295 /**
2296 * @private
2297 */
2298 _endPan: function GrabToPan__endPan() {
2299 this.element.removeEventListener('scroll', this._endPan, true);
2300 this.document.removeEventListener('mousemove', this._onmousemove, true);
2301 this.document.removeEventListener('mouseup', this._endPan, true);
2302 if (this.overlay.parentNode) {
2303 this.overlay.parentNode.removeChild(this.overlay);
2304 }
2305 }
2306 };
2307
2308 // Get the correct (vendor-prefixed) name of the matches method.
2309 var matchesSelector;
2310 ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) {
2311 var name = prefix + 'atches';
2312 if (name in document.documentElement) {
2313 matchesSelector = name;
2314 }
2315 name += 'Selector';
2316 if (name in document.documentElement) {
2317 matchesSelector = name;
2318 }
2319 return matchesSelector; // If found, then truthy, and [].some() ends.
2320 });
2321
2322 // Browser sniffing because it's impossible to feature-detect
2323 // whether event.which for onmousemove is reliable
2324 var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9;
2325 var chrome = window.chrome;
2326 var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app);
2327 // ^ Chrome 15+ ^ Opera 15+
2328 var isSafari6plus = /Apple/.test(navigator.vendor) &&
2329 /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent);
2330
2331 /**
2332 * Whether the left mouse is not pressed.
2333 * @param event {MouseEvent}
2334 * @return {boolean} True if the left mouse button is not pressed.
2335 * False if unsure or if the left mouse button is pressed.
2336 */
2337 function isLeftMouseReleased(event) {
2338 if ('buttons' in event && isNotIEorIsIE10plus) {
2339 // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons
2340 // Firefox 15+
2341 // Internet Explorer 10+
2342 return !(event.buttons | 1);
2343 }
2344 if (isChrome15OrOpera15plus || isSafari6plus) {
2345 // Chrome 14+
2346 // Opera 15+
2347 // Safari 6.0+
2348 return event.which === 0;
2349 }
2350 }
2351
2352 return GrabToPan;
2353})();
2354
2355var HandTool = {
2356 initialize: function handToolInitialize(options) {
2357 var toggleHandTool = options.toggleHandTool;
2358 this.handTool = new GrabToPan({
2359 element: options.container,
2360 onActiveChanged: function(isActive) {
2361 if (!toggleHandTool) {
2362 return;
2363 }
2364 if (isActive) {
2365 toggleHandTool.title =
2366 mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool');
2367 toggleHandTool.firstElementChild.textContent =
2368 mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool');
2369 } else {
2370 toggleHandTool.title =
2371 mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool');
2372 toggleHandTool.firstElementChild.textContent =
2373 mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool');
2374 }
2375 }
2376 });
2377 if (toggleHandTool) {
2378 toggleHandTool.addEventListener('click', this.toggle.bind(this), false);
2379
2380 window.addEventListener('localized', function (evt) {
2381 Preferences.get('enableHandToolOnLoad').then(function resolved(value) {
2382 if (value) {
2383 this.handTool.activate();
2384 }
2385 }.bind(this), function rejected(reason) {});
2386 }.bind(this));
2387 }
2388 },
2389
2390 toggle: function handToolToggle() {
2391 this.handTool.toggle();
2392 SecondaryToolbar.close();
2393 },
2394
2395 enterPresentationMode: function handToolEnterPresentationMode() {
2396 if (this.handTool.active) {
2397 this.wasActive = true;
2398 this.handTool.deactivate();
2399 }
2400 },
2401
2402 exitPresentationMode: function handToolExitPresentationMode() {
2403 if (this.wasActive) {
2404 this.wasActive = null;
2405 this.handTool.activate();
2406 }
2407 }
2408};
2409
2410
2411var OverlayManager = {
2412 overlays: {},
2413 active: null,
2414
2415 /**
2416 * @param {string} name The name of the overlay that is registered. This must
2417 * be equal to the ID of the overlay's DOM element.
2418 * @param {function} callerCloseMethod (optional) The method that, if present,
2419 * will call OverlayManager.close from the Object
2420 * registering the overlay. Access to this method is
2421 * necessary in order to run cleanup code when e.g.
2422 * the overlay is force closed. The default is null.
2423 * @param {boolean} canForceClose (optional) Indicates if opening the overlay
2424 * will close an active overlay. The default is false.
2425 * @returns {Promise} A promise that is resolved when the overlay has been
2426 * registered.
2427 */
2428 register: function overlayManagerRegister(name,
2429 callerCloseMethod, canForceClose) {
2430 return new Promise(function (resolve) {
2431 var element, container;
2432 if (!name || !(element = document.getElementById(name)) ||
2433 !(container = element.parentNode)) {
2434 throw new Error('Not enough parameters.');
2435 } else if (this.overlays[name]) {
2436 throw new Error('The overlay is already registered.');
2437 }
2438 this.overlays[name] = { element: element,
2439 container: container,
2440 callerCloseMethod: (callerCloseMethod || null),
2441 canForceClose: (canForceClose || false) };
2442 resolve();
2443 }.bind(this));
2444 },
2445
2446 /**
2447 * @param {string} name The name of the overlay that is unregistered.
2448 * @returns {Promise} A promise that is resolved when the overlay has been
2449 * unregistered.
2450 */
2451 unregister: function overlayManagerUnregister(name) {
2452 return new Promise(function (resolve) {
2453 if (!this.overlays[name]) {
2454 throw new Error('The overlay does not exist.');
2455 } else if (this.active === name) {
2456 throw new Error('The overlay cannot be removed while it is active.');
2457 }
2458 delete this.overlays[name];
2459
2460 resolve();
2461 }.bind(this));
2462 },
2463
2464 /**
2465 * @param {string} name The name of the overlay that should be opened.
2466 * @returns {Promise} A promise that is resolved when the overlay has been
2467 * opened.
2468 */
2469 open: function overlayManagerOpen(name) {
2470 return new Promise(function (resolve) {
2471 if (!this.overlays[name]) {
2472 throw new Error('The overlay does not exist.');
2473 } else if (this.active) {
2474 if (this.overlays[name].canForceClose) {
2475 this._closeThroughCaller();
2476 } else if (this.active === name) {
2477 throw new Error('The overlay is already active.');
2478 } else {
2479 throw new Error('Another overlay is currently active.');
2480 }
2481 }
2482 this.active = name;
2483 this.overlays[this.active].element.classList.remove('hidden');
2484 this.overlays[this.active].container.classList.remove('hidden');
2485
2486 window.addEventListener('keydown', this._keyDown);
2487 resolve();
2488 }.bind(this));
2489 },
2490
2491 /**
2492 * @param {string} name The name of the overlay that should be closed.
2493 * @returns {Promise} A promise that is resolved when the overlay has been
2494 * closed.
2495 */
2496 close: function overlayManagerClose(name) {
2497 return new Promise(function (resolve) {
2498 if (!this.overlays[name]) {
2499 throw new Error('The overlay does not exist.');
2500 } else if (!this.active) {
2501 throw new Error('The overlay is currently not active.');
2502 } else if (this.active !== name) {
2503 throw new Error('Another overlay is currently active.');
2504 }
2505 this.overlays[this.active].container.classList.add('hidden');
2506 this.overlays[this.active].element.classList.add('hidden');
2507 this.active = null;
2508
2509 window.removeEventListener('keydown', this._keyDown);
2510 resolve();
2511 }.bind(this));
2512 },
2513
2514 /**
2515 * @private
2516 */
2517 _keyDown: function overlayManager_keyDown(evt) {
2518 var self = OverlayManager;
2519 if (self.active && evt.keyCode === 27) { // Esc key.
2520 self._closeThroughCaller();
2521 evt.preventDefault();
2522 }
2523 },
2524
2525 /**
2526 * @private
2527 */
2528 _closeThroughCaller: function overlayManager_closeThroughCaller() {
2529 if (this.overlays[this.active].callerCloseMethod) {
2530 this.overlays[this.active].callerCloseMethod();
2531 }
2532 if (this.active) {
2533 this.close(this.active);
2534 }
2535 }
2536};
2537
2538
2539var PasswordPrompt = {
2540 overlayName: null,
2541 updatePassword: null,
2542 reason: null,
2543 passwordField: null,
2544 passwordText: null,
2545 passwordSubmit: null,
2546 passwordCancel: null,
2547
2548 initialize: function secondaryToolbarInitialize(options) {
2549 this.overlayName = options.overlayName;
2550 this.passwordField = options.passwordField;
2551 this.passwordText = options.passwordText;
2552 this.passwordSubmit = options.passwordSubmit;
2553 this.passwordCancel = options.passwordCancel;
2554
2555 // Attach the event listeners.
2556 this.passwordSubmit.addEventListener('click',
2557 this.verifyPassword.bind(this));
2558
2559 this.passwordCancel.addEventListener('click', this.close.bind(this));
2560
2561 this.passwordField.addEventListener('keydown', function (e) {
2562 if (e.keyCode === 13) { // Enter key
2563 this.verifyPassword();
2564 }
2565 }.bind(this));
2566
2567 OverlayManager.register(this.overlayName, this.close.bind(this), true);
2568 },
2569
2570 open: function passwordPromptOpen() {
2571 OverlayManager.open(this.overlayName).then(function () {
2572 this.passwordField.focus();
2573
2574 var promptString = mozL10n.get('password_label', null,
2575 'Enter the password to open this PDF file.');
2576
2577 if (this.reason === PDFJS.PasswordResponses.INCORRECT_PASSWORD) {
2578 promptString = mozL10n.get('password_invalid', null,
2579 'Invalid password. Please try again.');
2580 }
2581
2582 this.passwordText.textContent = promptString;
2583 }.bind(this));
2584 },
2585
2586 close: function passwordPromptClose() {
2587 OverlayManager.close(this.overlayName).then(function () {
2588 this.passwordField.value = '';
2589 }.bind(this));
2590 },
2591
2592 verifyPassword: function passwordPromptVerifyPassword() {
2593 var password = this.passwordField.value;
2594 if (password && password.length > 0) {
2595 this.close();
2596 return this.updatePassword(password);
2597 }
2598 }
2599};
2600
2601
2602var DocumentProperties = {
2603 overlayName: null,
2604 rawFileSize: 0,
2605
2606 // Document property fields (in the viewer).
2607 fileNameField: null,
2608 fileSizeField: null,
2609 titleField: null,
2610 authorField: null,
2611 subjectField: null,
2612 keywordsField: null,
2613 creationDateField: null,
2614 modificationDateField: null,
2615 creatorField: null,
2616 producerField: null,
2617 versionField: null,
2618 pageCountField: null,
2619 url: null,
2620 pdfDocument: null,
2621
2622 initialize: function documentPropertiesInitialize(options) {
2623 this.overlayName = options.overlayName;
2624
2625 // Set the document property fields.
2626 this.fileNameField = options.fileNameField;
2627 this.fileSizeField = options.fileSizeField;
2628 this.titleField = options.titleField;
2629 this.authorField = options.authorField;
2630 this.subjectField = options.subjectField;
2631 this.keywordsField = options.keywordsField;
2632 this.creationDateField = options.creationDateField;
2633 this.modificationDateField = options.modificationDateField;
2634 this.creatorField = options.creatorField;
2635 this.producerField = options.producerField;
2636 this.versionField = options.versionField;
2637 this.pageCountField = options.pageCountField;
2638
2639 // Bind the event listener for the Close button.
2640 if (options.closeButton) {
2641 options.closeButton.addEventListener('click', this.close.bind(this));
2642 }
2643
2644 this.dataAvailablePromise = new Promise(function (resolve) {
2645 this.resolveDataAvailable = resolve;
2646 }.bind(this));
2647
2648 OverlayManager.register(this.overlayName, this.close.bind(this));
2649 },
2650
2651 getProperties: function documentPropertiesGetProperties() {
2652 if (!OverlayManager.active) {
2653 // If the dialog was closed before dataAvailablePromise was resolved,
2654 // don't bother updating the properties.
2655 return;
2656 }
2657 // Get the file size (if it hasn't already been set).
2658 this.pdfDocument.getDownloadInfo().then(function(data) {
2659 if (data.length === this.rawFileSize) {
2660 return;
2661 }
2662 this.setFileSize(data.length);
2663 this.updateUI(this.fileSizeField, this.parseFileSize());
2664 }.bind(this));
2665
2666 // Get the document properties.
2667 this.pdfDocument.getMetadata().then(function(data) {
2668 var fields = [
2669 { field: this.fileNameField,
2670 content: getPDFFileNameFromURL(this.url) },
2671 { field: this.fileSizeField, content: this.parseFileSize() },
2672 { field: this.titleField, content: data.info.Title },
2673 { field: this.authorField, content: data.info.Author },
2674 { field: this.subjectField, content: data.info.Subject },
2675 { field: this.keywordsField, content: data.info.Keywords },
2676 { field: this.creationDateField,
2677 content: this.parseDate(data.info.CreationDate) },
2678 { field: this.modificationDateField,
2679 content: this.parseDate(data.info.ModDate) },
2680 { field: this.creatorField, content: data.info.Creator },
2681 { field: this.producerField, content: data.info.Producer },
2682 { field: this.versionField, content: data.info.PDFFormatVersion },
2683 { field: this.pageCountField, content: this.pdfDocument.numPages }
2684 ];
2685
2686 // Show the properties in the dialog.
2687 for (var item in fields) {
2688 var element = fields[item];
2689 this.updateUI(element.field, element.content);
2690 }
2691 }.bind(this));
2692 },
2693
2694 updateUI: function documentPropertiesUpdateUI(field, content) {
2695 if (field && content !== undefined && content !== '') {
2696 field.textContent = content;
2697 }
2698 },
2699
2700 setFileSize: function documentPropertiesSetFileSize(fileSize) {
2701 if (fileSize > 0) {
2702 this.rawFileSize = fileSize;
2703 }
2704 },
2705
2706 parseFileSize: function documentPropertiesParseFileSize() {
2707 var fileSize = this.rawFileSize, kb = fileSize / 1024;
2708 if (!kb) {
2709 return;
2710 } else if (kb < 1024) {
2711 return mozL10n.get('document_properties_kb', {
2712 size_kb: (+kb.toPrecision(3)).toLocaleString(),
2713 size_b: fileSize.toLocaleString()
2714 }, '{{size_kb}} KB ({{size_b}} bytes)');
2715 } else {
2716 return mozL10n.get('document_properties_mb', {
2717 size_mb: (+(kb / 1024).toPrecision(3)).toLocaleString(),
2718 size_b: fileSize.toLocaleString()
2719 }, '{{size_mb}} MB ({{size_b}} bytes)');
2720 }
2721 },
2722
2723 open: function documentPropertiesOpen() {
2724 Promise.all([OverlayManager.open(this.overlayName),
2725 this.dataAvailablePromise]).then(function () {
2726 this.getProperties();
2727 }.bind(this));
2728 },
2729
2730 close: function documentPropertiesClose() {
2731 OverlayManager.close(this.overlayName);
2732 },
2733
2734 parseDate: function documentPropertiesParseDate(inputDate) {
2735 // This is implemented according to the PDF specification (see
2736 // http://www.gnupdf.org/Date for an overview), but note that
2737 // Adobe Reader doesn't handle changing the date to universal time
2738 // and doesn't use the user's time zone (they're effectively ignoring
2739 // the HH' and mm' parts of the date string).
2740 var dateToParse = inputDate;
2741 if (dateToParse === undefined) {
2742 return '';
2743 }
2744
2745 // Remove the D: prefix if it is available.
2746 if (dateToParse.substring(0,2) === 'D:') {
2747 dateToParse = dateToParse.substring(2);
2748 }
2749
2750 // Get all elements from the PDF date string.
2751 // JavaScript's Date object expects the month to be between
2752 // 0 and 11 instead of 1 and 12, so we're correcting for this.
2753 var year = parseInt(dateToParse.substring(0,4), 10);
2754 var month = parseInt(dateToParse.substring(4,6), 10) - 1;
2755 var day = parseInt(dateToParse.substring(6,8), 10);
2756 var hours = parseInt(dateToParse.substring(8,10), 10);
2757 var minutes = parseInt(dateToParse.substring(10,12), 10);
2758 var seconds = parseInt(dateToParse.substring(12,14), 10);
2759 var utRel = dateToParse.substring(14,15);
2760 var offsetHours = parseInt(dateToParse.substring(15,17), 10);
2761 var offsetMinutes = parseInt(dateToParse.substring(18,20), 10);
2762
2763 // As per spec, utRel = 'Z' means equal to universal time.
2764 // The other cases ('-' and '+') have to be handled here.
2765 if (utRel === '-') {
2766 hours += offsetHours;
2767 minutes += offsetMinutes;
2768 } else if (utRel === '+') {
2769 hours -= offsetHours;
2770 minutes -= offsetMinutes;
2771 }
2772
2773 // Return the new date format from the user's locale.
2774 var date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
2775 var dateString = date.toLocaleDateString();
2776 var timeString = date.toLocaleTimeString();
2777 return mozL10n.get('document_properties_date_string',
2778 {date: dateString, time: timeString},
2779 '{{date}}, {{time}}');
2780 }
2781};
2782
2783
2784var PresentationModeState = {
2785 UNKNOWN: 0,
2786 NORMAL: 1,
2787 CHANGING: 2,
2788 FULLSCREEN: 3,
2789};
2790
2791var IGNORE_CURRENT_POSITION_ON_ZOOM = false;
2792
2793
2794var CLEANUP_TIMEOUT = 30000;
2795
2796var RenderingStates = {
2797 INITIAL: 0,
2798 RUNNING: 1,
2799 PAUSED: 2,
2800 FINISHED: 3
2801};
2802
2803/**
2804 * Controls rendering of the views for pages and thumbnails.
2805 * @class
2806 */
2807var PDFRenderingQueue = (function PDFRenderingQueueClosure() {
2808 /**
2809 * @constructs
2810 */
2811 function PDFRenderingQueue() {
2812 this.pdfViewer = null;
2813 this.pdfThumbnailViewer = null;
2814 this.onIdle = null;
2815
2816 this.highestPriorityPage = null;
2817 this.idleTimeout = null;
2818 this.printing = false;
2819 this.isThumbnailViewEnabled = false;
2820 }
2821
2822 PDFRenderingQueue.prototype = /** @lends PDFRenderingQueue.prototype */ {
2823 /**
2824 * @param {PDFViewer} pdfViewer
2825 */
2826 setViewer: function PDFRenderingQueue_setViewer(pdfViewer) {
2827 this.pdfViewer = pdfViewer;
2828 },
2829
2830 /**
2831 * @param {PDFThumbnailViewer} pdfThumbnailViewer
2832 */
2833 setThumbnailViewer:
2834 function PDFRenderingQueue_setThumbnailViewer(pdfThumbnailViewer) {
2835 this.pdfThumbnailViewer = pdfThumbnailViewer;
2836 },
2837
2838 /**
2839 * @param {IRenderableView} view
2840 * @returns {boolean}
2841 */
2842 isHighestPriority: function PDFRenderingQueue_isHighestPriority(view) {
2843 return this.highestPriorityPage === view.renderingId;
2844 },
2845
2846 renderHighestPriority: function
2847 PDFRenderingQueue_renderHighestPriority(currentlyVisiblePages) {
2848 if (this.idleTimeout) {
2849 clearTimeout(this.idleTimeout);
2850 this.idleTimeout = null;
2851 }
2852
2853 // Pages have a higher priority than thumbnails, so check them first.
2854 if (this.pdfViewer.forceRendering(currentlyVisiblePages)) {
2855 return;
2856 }
2857 // No pages needed rendering so check thumbnails.
2858 if (this.pdfThumbnailViewer && this.isThumbnailViewEnabled) {
2859 if (this.pdfThumbnailViewer.forceRendering()) {
2860 return;
2861 }
2862 }
2863
2864 if (this.printing) {
2865 // If printing is currently ongoing do not reschedule cleanup.
2866 return;
2867 }
2868
2869 if (this.onIdle) {
2870 this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT);
2871 }
2872 },
2873
2874 getHighestPriority: function
2875 PDFRenderingQueue_getHighestPriority(visible, views, scrolledDown) {
2876 // The state has changed figure out which page has the highest priority to
2877 // render next (if any).
2878 // Priority:
2879 // 1 visible pages
2880 // 2 if last scrolled down page after the visible pages
2881 // 2 if last scrolled up page before the visible pages
2882 var visibleViews = visible.views;
2883
2884 var numVisible = visibleViews.length;
2885 if (numVisible === 0) {
2886 return false;
2887 }
2888 for (var i = 0; i < numVisible; ++i) {
2889 var view = visibleViews[i].view;
2890 if (!this.isViewFinished(view)) {
2891 return view;
2892 }
2893 }
2894
2895 // All the visible views have rendered, try to render next/previous pages.
2896 if (scrolledDown) {
2897 var nextPageIndex = visible.last.id;
2898 // ID's start at 1 so no need to add 1.
2899 if (views[nextPageIndex] &&
2900 !this.isViewFinished(views[nextPageIndex])) {
2901 return views[nextPageIndex];
2902 }
2903 } else {
2904 var previousPageIndex = visible.first.id - 2;
2905 if (views[previousPageIndex] &&
2906 !this.isViewFinished(views[previousPageIndex])) {
2907 return views[previousPageIndex];
2908 }
2909 }
2910 // Everything that needs to be rendered has been.
2911 return null;
2912 },
2913
2914 /**
2915 * @param {IRenderableView} view
2916 * @returns {boolean}
2917 */
2918 isViewFinished: function PDFRenderingQueue_isViewFinished(view) {
2919 return view.renderingState === RenderingStates.FINISHED;
2920 },
2921
2922 /**
2923 * Render a page or thumbnail view. This calls the appropriate function
2924 * based on the views state. If the view is already rendered it will return
2925 * false.
2926 * @param {IRenderableView} view
2927 */
2928 renderView: function PDFRenderingQueue_renderView(view) {
2929 var state = view.renderingState;
2930 switch (state) {
2931 case RenderingStates.FINISHED:
2932 return false;
2933 case RenderingStates.PAUSED:
2934 this.highestPriorityPage = view.renderingId;
2935 view.resume();
2936 break;
2937 case RenderingStates.RUNNING:
2938 this.highestPriorityPage = view.renderingId;
2939 break;
2940 case RenderingStates.INITIAL:
2941 this.highestPriorityPage = view.renderingId;
2942 view.draw(this.renderHighestPriority.bind(this));
2943 break;
2944 }
2945 return true;
2946 },
2947 };
2948
2949 return PDFRenderingQueue;
2950})();
2951
2952
2953/**
2954 * @constructor
2955 * @param {HTMLDivElement} container - The viewer element.
2956 * @param {number} id - The page unique ID (normally its number).
2957 * @param {number} scale - The page scale display.
2958 * @param {PageViewport} defaultViewport - The page viewport.
2959 * @param {IPDFLinkService} linkService - The navigation/linking service.
2960 * @param {PDFRenderingQueue} renderingQueue - The rendering queue object.
2961 * @param {Cache} cache - The page cache.
2962 * @param {PDFPageSource} pageSource
2963 * @param {PDFViewer} viewer
2964 *
2965 * @implements {IRenderableView}
2966 */
2967var PageView = function pageView(container, id, scale, defaultViewport,
2968 linkService, renderingQueue, cache,
2969 pageSource, viewer) {
2970 this.id = id;
2971 this.renderingId = 'page' + id;
2972
2973 this.rotation = 0;
2974 this.scale = scale || 1.0;
2975 this.viewport = defaultViewport;
2976 this.pdfPageRotate = defaultViewport.rotation;
2977 this.hasRestrictedScaling = false;
2978
2979 this.linkService = linkService;
2980 this.renderingQueue = renderingQueue;
2981 this.cache = cache;
2982 this.pageSource = pageSource;
2983 this.viewer = viewer;
2984
2985 this.renderingState = RenderingStates.INITIAL;
2986 this.resume = null;
2987
2988 this.textLayer = null;
2989
2990 this.zoomLayer = null;
2991
2992 this.annotationLayer = null;
2993
2994 var anchor = document.createElement('a');
2995 anchor.name = '' + this.id;
2996
2997 var div = this.el = document.createElement('div');
2998 div.id = 'pageContainer' + this.id;
2999 div.className = 'page';
3000 div.style.width = Math.floor(this.viewport.width) + 'px';
3001 div.style.height = Math.floor(this.viewport.height) + 'px';
3002
3003 container.appendChild(anchor);
3004 container.appendChild(div);
3005
3006 this.setPdfPage = function pageViewSetPdfPage(pdfPage) {
3007 this.pdfPage = pdfPage;
3008 this.pdfPageRotate = pdfPage.rotate;
3009 var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
3010 this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, totalRotation);
3011 this.stats = pdfPage.stats;
3012 this.reset();
3013 };
3014
3015 this.destroy = function pageViewDestroy() {
3016 this.zoomLayer = null;
3017 this.reset();
3018 if (this.pdfPage) {
3019 this.pdfPage.destroy();
3020 }
3021 };
3022
3023 this.reset = function pageViewReset(keepAnnotations) {
3024 if (this.renderTask) {
3025 this.renderTask.cancel();
3026 }
3027 this.resume = null;
3028 this.renderingState = RenderingStates.INITIAL;
3029
3030 div.style.width = Math.floor(this.viewport.width) + 'px';
3031 div.style.height = Math.floor(this.viewport.height) + 'px';
3032
3033 var childNodes = div.childNodes;
3034 for (var i = div.childNodes.length - 1; i >= 0; i--) {
3035 var node = childNodes[i];
3036 if ((this.zoomLayer && this.zoomLayer === node) ||
3037 (keepAnnotations && this.annotationLayer === node)) {
3038 continue;
3039 }
3040 div.removeChild(node);
3041 }
3042 div.removeAttribute('data-loaded');
3043
3044 if (keepAnnotations) {
3045 if (this.annotationLayer) {
3046 // Hide annotationLayer until all elements are resized
3047 // so they are not displayed on the already-resized page
3048 this.annotationLayer.setAttribute('hidden', 'true');
3049 }
3050 } else {
3051 this.annotationLayer = null;
3052 }
3053
3054 if (this.canvas) {
3055 // Zeroing the width and height causes Firefox to release graphics
3056 // resources immediately, which can greatly reduce memory consumption.
3057 this.canvas.width = 0;
3058 this.canvas.height = 0;
3059 delete this.canvas;
3060 }
3061
3062 this.loadingIconDiv = document.createElement('div');
3063 this.loadingIconDiv.className = 'loadingIcon';
3064 div.appendChild(this.loadingIconDiv);
3065 };
3066
3067 this.update = function pageViewUpdate(scale, rotation) {
3068 this.scale = scale || this.scale;
3069
3070 if (typeof rotation !== 'undefined') {
3071 this.rotation = rotation;
3072 }
3073
3074 var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
3075 this.viewport = this.viewport.clone({
3076 scale: this.scale * CSS_UNITS,
3077 rotation: totalRotation
3078 });
3079
3080 var isScalingRestricted = false;
3081 if (this.canvas && PDFJS.maxCanvasPixels > 0) {
3082 var ctx = this.canvas.getContext('2d');
3083 var outputScale = getOutputScale(ctx);
3084 var pixelsInViewport = this.viewport.width * this.viewport.height;
3085 var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport);
3086 if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) *
3087 ((Math.floor(this.viewport.height) * outputScale.sy) | 0) >
3088 PDFJS.maxCanvasPixels) {
3089 isScalingRestricted = true;
3090 }
3091 }
3092
3093 if (this.canvas &&
3094 (PDFJS.useOnlyCssZoom ||
3095 (this.hasRestrictedScaling && isScalingRestricted))) {
3096 this.cssTransform(this.canvas, true);
3097 return;
3098 } else if (this.canvas && !this.zoomLayer) {
3099 this.zoomLayer = this.canvas.parentNode;
3100 this.zoomLayer.style.position = 'absolute';
3101 }
3102 if (this.zoomLayer) {
3103 this.cssTransform(this.zoomLayer.firstChild);
3104 }
3105 this.reset(true);
3106 };
3107
3108 this.cssTransform = function pageCssTransform(canvas, redrawAnnotations) {
3109 // Scale canvas, canvas wrapper, and page container.
3110 var width = this.viewport.width;
3111 var height = this.viewport.height;
3112 canvas.style.width = canvas.parentNode.style.width = div.style.width =
3113 Math.floor(width) + 'px';
3114 canvas.style.height = canvas.parentNode.style.height = div.style.height =
3115 Math.floor(height) + 'px';
3116 // The canvas may have been originally rotated, so rotate relative to that.
3117 var relativeRotation = this.viewport.rotation - canvas._viewport.rotation;
3118 var absRotation = Math.abs(relativeRotation);
3119 var scaleX = 1, scaleY = 1;
3120 if (absRotation === 90 || absRotation === 270) {
3121 // Scale x and y because of the rotation.
3122 scaleX = height / width;
3123 scaleY = width / height;
3124 }
3125 var cssTransform = 'rotate(' + relativeRotation + 'deg) ' +
3126 'scale(' + scaleX + ',' + scaleY + ')';
3127 CustomStyle.setProp('transform', canvas, cssTransform);
3128
3129 if (this.textLayer) {
3130 // Rotating the text layer is more complicated since the divs inside the
3131 // the text layer are rotated.
3132 // TODO: This could probably be simplified by drawing the text layer in
3133 // one orientation then rotating overall.
3134 var textLayerViewport = this.textLayer.viewport;
3135 var textRelativeRotation = this.viewport.rotation -
3136 textLayerViewport.rotation;
3137 var textAbsRotation = Math.abs(textRelativeRotation);
3138 var scale = width / textLayerViewport.width;
3139 if (textAbsRotation === 90 || textAbsRotation === 270) {
3140 scale = width / textLayerViewport.height;
3141 }
3142 var textLayerDiv = this.textLayer.textLayerDiv;
3143 var transX, transY;
3144 switch (textAbsRotation) {
3145 case 0:
3146 transX = transY = 0;
3147 break;
3148 case 90:
3149 transX = 0;
3150 transY = '-' + textLayerDiv.style.height;
3151 break;
3152 case 180:
3153 transX = '-' + textLayerDiv.style.width;
3154 transY = '-' + textLayerDiv.style.height;
3155 break;
3156 case 270:
3157 transX = '-' + textLayerDiv.style.width;
3158 transY = 0;
3159 break;
3160 default:
3161 console.error('Bad rotation value.');
3162 break;
3163 }
3164 CustomStyle.setProp('transform', textLayerDiv,
3165 'rotate(' + textAbsRotation + 'deg) ' +
3166 'scale(' + scale + ', ' + scale + ') ' +
3167 'translate(' + transX + ', ' + transY + ')');
3168 CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%');
3169 }
3170
3171 if (redrawAnnotations && this.annotationLayer) {
3172 setupAnnotations(div, this.pdfPage, this.viewport);
3173 }
3174 };
3175
3176 Object.defineProperty(this, 'width', {
3177 get: function PageView_getWidth() {
3178 return this.viewport.width;
3179 },
3180 enumerable: true
3181 });
3182
3183 Object.defineProperty(this, 'height', {
3184 get: function PageView_getHeight() {
3185 return this.viewport.height;
3186 },
3187 enumerable: true
3188 });
3189
3190 var self = this;
3191
3192 function setupAnnotations(pageDiv, pdfPage, viewport) {
3193
3194 function bindLink(link, dest) {
3195 link.href = linkService.getDestinationHash(dest);
3196 link.onclick = function pageViewSetupLinksOnclick() {
3197 if (dest) {
3198 linkService.navigateTo(dest);
3199 }
3200 return false;
3201 };
3202 if (dest) {
3203 link.className = 'internalLink';
3204 }
3205 }
3206
3207 function bindNamedAction(link, action) {
3208 link.href = linkService.getAnchorUrl('');
3209 link.onclick = function pageViewSetupNamedActionOnClick() {
3210 linkService.executeNamedAction(action);
3211 return false;
3212 };
3213 link.className = 'internalLink';
3214 }
3215
3216 pdfPage.getAnnotations().then(function(annotationsData) {
3217 viewport = viewport.clone({ dontFlip: true });
3218 var transform = viewport.transform;
3219 var transformStr = 'matrix(' + transform.join(',') + ')';
3220 var data, element, i, ii;
3221
3222 if (self.annotationLayer) {
3223 // If an annotationLayer already exists, refresh its children's
3224 // transformation matrices
3225 for (i = 0, ii = annotationsData.length; i < ii; i++) {
3226 data = annotationsData[i];
3227 element = self.annotationLayer.querySelector(
3228 '[data-annotation-id="' + data.id + '"]');
3229 if (element) {
3230 CustomStyle.setProp('transform', element, transformStr);
3231 }
3232 }
3233 // See this.reset()
3234 self.annotationLayer.removeAttribute('hidden');
3235 } else {
3236 for (i = 0, ii = annotationsData.length; i < ii; i++) {
3237 data = annotationsData[i];
3238 if (!data || !data.hasHtml) {
3239 continue;
3240 }
3241
3242 element = PDFJS.AnnotationUtils.getHtmlElement(data,
3243 pdfPage.commonObjs);
3244 element.setAttribute('data-annotation-id', data.id);
3245 mozL10n.translate(element);
3246
3247 var rect = data.rect;
3248 var view = pdfPage.view;
3249 rect = PDFJS.Util.normalizeRect([
3250 rect[0],
3251 view[3] - rect[1] + view[1],
3252 rect[2],
3253 view[3] - rect[3] + view[1]
3254 ]);
3255 element.style.left = rect[0] + 'px';
3256 element.style.top = rect[1] + 'px';
3257 element.style.position = 'absolute';
3258
3259 CustomStyle.setProp('transform', element, transformStr);
3260 var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px';
3261 CustomStyle.setProp('transformOrigin', element, transformOriginStr);
3262
3263 if (data.subtype === 'Link' && !data.url) {
3264 var link = element.getElementsByTagName('a')[0];
3265 if (link) {
3266 if (data.action) {
3267 bindNamedAction(link, data.action);
3268 } else {
3269 bindLink(link, ('dest' in data) ? data.dest : null);
3270 }
3271 }
3272 }
3273
3274 if (!self.annotationLayer) {
3275 var annotationLayerDiv = document.createElement('div');
3276 annotationLayerDiv.className = 'annotationLayer';
3277 pageDiv.appendChild(annotationLayerDiv);
3278 self.annotationLayer = annotationLayerDiv;
3279 }
3280
3281 self.annotationLayer.appendChild(element);
3282 }
3283 }
3284 });
3285 }
3286
3287 this.getPagePoint = function pageViewGetPagePoint(x, y) {
3288 return this.viewport.convertToPdfPoint(x, y);
3289 };
3290
3291 this.draw = function pageviewDraw(callback) {
3292 var pdfPage = this.pdfPage;
3293
3294 if (this.pagePdfPromise) {
3295 return;
3296 }
3297 if (!pdfPage) {
3298 var promise = this.pageSource.getPage();
3299 promise.then(function(pdfPage) {
3300 delete this.pagePdfPromise;
3301 this.setPdfPage(pdfPage);
3302 this.draw(callback);
3303 }.bind(this));
3304 this.pagePdfPromise = promise;
3305 return;
3306 }
3307
3308 if (this.renderingState !== RenderingStates.INITIAL) {
3309 console.error('Must be in new state before drawing');
3310 }
3311
3312 this.renderingState = RenderingStates.RUNNING;
3313
3314 var viewport = this.viewport;
3315 // Wrap the canvas so if it has a css transform for highdpi the overflow
3316 // will be hidden in FF.
3317 var canvasWrapper = document.createElement('div');
3318 canvasWrapper.style.width = div.style.width;
3319 canvasWrapper.style.height = div.style.height;
3320 canvasWrapper.classList.add('canvasWrapper');
3321
3322 var canvas = document.createElement('canvas');
3323 canvas.id = 'page' + this.id;
3324 canvasWrapper.appendChild(canvas);
3325 if (this.annotationLayer) {
3326 // annotationLayer needs to stay on top
3327 div.insertBefore(canvasWrapper, this.annotationLayer);
3328 } else {
3329 div.appendChild(canvasWrapper);
3330 }
3331 this.canvas = canvas;
3332
3333 var ctx = canvas.getContext('2d');
3334 var outputScale = getOutputScale(ctx);
3335
3336 if (PDFJS.useOnlyCssZoom) {
3337 var actualSizeViewport = viewport.clone({ scale: CSS_UNITS });
3338 // Use a scale that will make the canvas be the original intended size
3339 // of the page.
3340 outputScale.sx *= actualSizeViewport.width / viewport.width;
3341 outputScale.sy *= actualSizeViewport.height / viewport.height;
3342 outputScale.scaled = true;
3343 }
3344
3345 if (PDFJS.maxCanvasPixels > 0) {
3346 var pixelsInViewport = viewport.width * viewport.height;
3347 var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport);
3348 if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
3349 outputScale.sx = maxScale;
3350 outputScale.sy = maxScale;
3351 outputScale.scaled = true;
3352 this.hasRestrictedScaling = true;
3353 } else {
3354 this.hasRestrictedScaling = false;
3355 }
3356 }
3357
3358 canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0;
3359 canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0;
3360 canvas.style.width = Math.floor(viewport.width) + 'px';
3361 canvas.style.height = Math.floor(viewport.height) + 'px';
3362 // Add the viewport so it's known what it was originally drawn with.
3363 canvas._viewport = viewport;
3364
3365 var textLayerDiv = null;
3366 var textLayer = null;
3367 if (!PDFJS.disableTextLayer) {
3368 textLayerDiv = document.createElement('div');
3369 textLayerDiv.className = 'textLayer';
3370 textLayerDiv.style.width = canvas.style.width;
3371 textLayerDiv.style.height = canvas.style.height;
3372 if (this.annotationLayer) {
3373 // annotationLayer needs to stay on top
3374 div.insertBefore(textLayerDiv, this.annotationLayer);
3375 } else {
3376 div.appendChild(textLayerDiv);
3377 }
3378
3379 textLayer = this.viewer.createTextLayerBuilder(textLayerDiv, this.id - 1,
3380 this.viewport);
3381 }
3382 this.textLayer = textLayer;
3383
3384 // TODO(mack): use data attributes to store these
3385 ctx._scaleX = outputScale.sx;
3386 ctx._scaleY = outputScale.sy;
3387 if (outputScale.scaled) {
3388 ctx.scale(outputScale.sx, outputScale.sy);
3389 }
3390
3391 // Rendering area
3392
3393 var self = this;
3394 function pageViewDrawCallback(error) {
3395 // The renderTask may have been replaced by a new one, so only remove the
3396 // reference to the renderTask if it matches the one that is triggering
3397 // this callback.
3398 if (renderTask === self.renderTask) {
3399 self.renderTask = null;
3400 }
3401
3402 if (error === 'cancelled') {
3403 return;
3404 }
3405
3406 self.renderingState = RenderingStates.FINISHED;
3407
3408 if (self.loadingIconDiv) {
3409 div.removeChild(self.loadingIconDiv);
3410 delete self.loadingIconDiv;
3411 }
3412
3413 if (self.zoomLayer) {
3414 div.removeChild(self.zoomLayer);
3415 self.zoomLayer = null;
3416 }
3417
3418 self.error = error;
3419 self.stats = pdfPage.stats;
3420 self.updateStats();
3421 if (self.onAfterDraw) {
3422 self.onAfterDraw();
3423 }
3424
3425 var event = document.createEvent('CustomEvent');
3426 event.initCustomEvent('pagerender', true, true, {
3427 pageNumber: pdfPage.pageNumber
3428 });
3429 div.dispatchEvent(event);
3430
3431 callback();
3432 }
3433
3434 var renderContext = {
3435 canvasContext: ctx,
3436 viewport: this.viewport,
3437 // intent: 'default', // === 'display'
3438 continueCallback: function pdfViewcContinueCallback(cont) {
3439 if (!self.renderingQueue.isHighestPriority(self)) {
3440 self.renderingState = RenderingStates.PAUSED;
3441 self.resume = function resumeCallback() {
3442 self.renderingState = RenderingStates.RUNNING;
3443 cont();
3444 };
3445 return;
3446 }
3447 cont();
3448 }
3449 };
3450 var renderTask = this.renderTask = this.pdfPage.render(renderContext);
3451
3452 this.renderTask.promise.then(
3453 function pdfPageRenderCallback() {
3454 pageViewDrawCallback(null);
3455 if (textLayer) {
3456 self.pdfPage.getTextContent().then(
3457 function textContentResolved(textContent) {
3458 textLayer.setTextContent(textContent);
3459 }
3460 );
3461 }
3462 },
3463 function pdfPageRenderError(error) {
3464 pageViewDrawCallback(error);
3465 }
3466 );
3467
3468 setupAnnotations(div, pdfPage, this.viewport);
3469 div.setAttribute('data-loaded', true);
3470
3471 // Add the page to the cache at the start of drawing. That way it can be
3472 // evicted from the cache and destroyed even if we pause its rendering.
3473 cache.push(this);
3474 };
3475
3476 this.beforePrint = function pageViewBeforePrint() {
3477 var pdfPage = this.pdfPage;
3478
3479 var viewport = pdfPage.getViewport(1);
3480 // Use the same hack we use for high dpi displays for printing to get better
3481 // output until bug 811002 is fixed in FF.
3482 var PRINT_OUTPUT_SCALE = 2;
3483 var canvas = document.createElement('canvas');
3484 canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE;
3485 canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE;
3486 canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt';
3487 canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt';
3488 var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' +
3489 (1 / PRINT_OUTPUT_SCALE) + ')';
3490 CustomStyle.setProp('transform' , canvas, cssScale);
3491 CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
3492
3493 var printContainer = document.getElementById('printContainer');
3494 var canvasWrapper = document.createElement('div');
3495 canvasWrapper.style.width = viewport.width + 'pt';
3496 canvasWrapper.style.height = viewport.height + 'pt';
3497 canvasWrapper.appendChild(canvas);
3498 printContainer.appendChild(canvasWrapper);
3499
3500 canvas.mozPrintCallback = function(obj) {
3501 var ctx = obj.context;
3502
3503 ctx.save();
3504 ctx.fillStyle = 'rgb(255, 255, 255)';
3505 ctx.fillRect(0, 0, canvas.width, canvas.height);
3506 ctx.restore();
3507 ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE);
3508
3509 var renderContext = {
3510 canvasContext: ctx,
3511 viewport: viewport,
3512 intent: 'print'
3513 };
3514
3515 pdfPage.render(renderContext).promise.then(function() {
3516 // Tell the printEngine that rendering this canvas/page has finished.
3517 obj.done();
3518 }, function(error) {
3519 console.error(error);
3520 // Tell the printEngine that rendering this canvas/page has failed.
3521 // This will make the print proces stop.
3522 if ('abort' in obj) {
3523 obj.abort();
3524 } else {
3525 obj.done();
3526 }
3527 });
3528 };
3529 };
3530
3531 this.updateStats = function pageViewUpdateStats() {
3532 if (!this.stats) {
3533 return;
3534 }
3535
3536 if (PDFJS.pdfBug && Stats.enabled) {
3537 var stats = this.stats;
3538 Stats.add(this.id, stats);
3539 }
3540 };
3541};
3542
3543
3544var FIND_SCROLL_OFFSET_TOP = -50;
3545var FIND_SCROLL_OFFSET_LEFT = -400;
3546var MAX_TEXT_DIVS_TO_RENDER = 100000;
3547var RENDER_DELAY = 200; // ms
3548
3549var NonWhitespaceRegexp = /\S/;
3550
3551function isAllWhitespace(str) {
3552 return !NonWhitespaceRegexp.test(str);
3553}
3554
3555/**
3556 * @typedef {Object} TextLayerBuilderOptions
3557 * @property {HTMLDivElement} textLayerDiv - The text layer container.
3558 * @property {number} pageIndex - The page index.
3559 * @property {PageViewport} viewport - The viewport of the text layer.
3560 * @property {ILastScrollSource} lastScrollSource - The object that records when
3561 * last time scroll happened.
3562 * @property {boolean} isViewerInPresentationMode
3563 * @property {PDFFindController} findController
3564 */
3565
3566/**
3567 * TextLayerBuilder provides text-selection functionality for the PDF.
3568 * It does this by creating overlay divs over the PDF text. These divs
3569 * contain text that matches the PDF text they are overlaying. This object
3570 * also provides a way to highlight text that is being searched for.
3571 * @class
3572 */
3573var TextLayerBuilder = (function TextLayerBuilderClosure() {
3574 function TextLayerBuilder(options) {
3575 this.textLayerDiv = options.textLayerDiv;
3576 this.layoutDone = false;
3577 this.divContentDone = false;
3578 this.pageIdx = options.pageIndex;
3579 this.matches = [];
3580 this.lastScrollSource = options.lastScrollSource || null;
3581 this.viewport = options.viewport;
3582 this.isViewerInPresentationMode = options.isViewerInPresentationMode;
3583 this.textDivs = [];
3584 this.findController = options.findController || null;
3585 }
3586
3587 TextLayerBuilder.prototype = {
3588 renderLayer: function TextLayerBuilder_renderLayer() {
3589 var textLayerFrag = document.createDocumentFragment();
3590 var textDivs = this.textDivs;
3591 var textDivsLength = textDivs.length;
3592 var canvas = document.createElement('canvas');
3593 var ctx = canvas.getContext('2d');
3594
3595 // No point in rendering many divs as it would make the browser
3596 // unusable even after the divs are rendered.
3597 if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
3598 return;
3599 }
3600
3601 var lastFontSize;
3602 var lastFontFamily;
3603 for (var i = 0; i < textDivsLength; i++) {
3604 var textDiv = textDivs[i];
3605 if (textDiv.dataset.isWhitespace !== undefined) {
3606 continue;
3607 }
3608
3609 var fontSize = textDiv.style.fontSize;
3610 var fontFamily = textDiv.style.fontFamily;
3611
3612 // Only build font string and set to context if different from last.
3613 if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) {
3614 ctx.font = fontSize + ' ' + fontFamily;
3615 lastFontSize = fontSize;
3616 lastFontFamily = fontFamily;
3617 }
3618
3619 var width = ctx.measureText(textDiv.textContent).width;
3620 if (width > 0) {
3621 textLayerFrag.appendChild(textDiv);
3622 var transform;
3623 if (textDiv.dataset.canvasWidth !== undefined) {
3624 // Dataset values come of type string.
3625 var textScale = textDiv.dataset.canvasWidth / width;
3626 transform = 'scaleX(' + textScale + ')';
3627 } else {
3628 transform = '';
3629 }
3630 var rotation = textDiv.dataset.angle;
3631 if (rotation) {
3632 transform = 'rotate(' + rotation + 'deg) ' + transform;
3633 }
3634 if (transform) {
3635 CustomStyle.setProp('transform' , textDiv, transform);
3636 }
3637 }
3638 }
3639
3640 this.textLayerDiv.appendChild(textLayerFrag);
3641 this.renderingDone = true;
3642 this.updateMatches();
3643 },
3644
3645 setupRenderLayoutTimer:
3646 function TextLayerBuilder_setupRenderLayoutTimer() {
3647 // Schedule renderLayout() if the user has been scrolling,
3648 // otherwise run it right away.
3649 var self = this;
3650 var lastScroll = (this.lastScrollSource === null ?
3651 0 : this.lastScrollSource.lastScroll);
3652
3653 if (Date.now() - lastScroll > RENDER_DELAY) { // Render right away
3654 this.renderLayer();
3655 } else { // Schedule
3656 if (this.renderTimer) {
3657 clearTimeout(this.renderTimer);
3658 }
3659 this.renderTimer = setTimeout(function() {
3660 self.setupRenderLayoutTimer();
3661 }, RENDER_DELAY);
3662 }
3663 },
3664
3665 appendText: function TextLayerBuilder_appendText(geom, styles) {
3666 var style = styles[geom.fontName];
3667 var textDiv = document.createElement('div');
3668 this.textDivs.push(textDiv);
3669 if (isAllWhitespace(geom.str)) {
3670 textDiv.dataset.isWhitespace = true;
3671 return;
3672 }
3673 var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform);
3674 var angle = Math.atan2(tx[1], tx[0]);
3675 if (style.vertical) {
3676 angle += Math.PI / 2;
3677 }
3678 var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]));
3679 var fontAscent = fontHeight;
3680 if (style.ascent) {
3681 fontAscent = style.ascent * fontAscent;
3682 } else if (style.descent) {
3683 fontAscent = (1 + style.descent) * fontAscent;
3684 }
3685
3686 var left;
3687 var top;
3688 if (angle === 0) {
3689 left = tx[4];
3690 top = tx[5] - fontAscent;
3691 } else {
3692 left = tx[4] + (fontAscent * Math.sin(angle));
3693 top = tx[5] - (fontAscent * Math.cos(angle));
3694 }
3695 textDiv.style.left = left + 'px';
3696 textDiv.style.top = top + 'px';
3697 textDiv.style.fontSize = fontHeight + 'px';
3698 textDiv.style.fontFamily = style.fontFamily;
3699
3700 textDiv.textContent = geom.str;
3701 // |fontName| is only used by the Font Inspector. This test will succeed
3702 // when e.g. the Font Inspector is off but the Stepper is on, but it's
3703 // not worth the effort to do a more accurate test.
3704 if (PDFJS.pdfBug) {
3705 textDiv.dataset.fontName = geom.fontName;
3706 }
3707 // Storing into dataset will convert number into string.
3708 if (angle !== 0) {
3709 textDiv.dataset.angle = angle * (180 / Math.PI);
3710 }
3711 // We don't bother scaling single-char text divs, because it has very
3712 // little effect on text highlighting. This makes scrolling on docs with
3713 // lots of such divs a lot faster.
3714 if (textDiv.textContent.length > 1) {
3715 if (style.vertical) {
3716 textDiv.dataset.canvasWidth = geom.height * this.viewport.scale;
3717 } else {
3718 textDiv.dataset.canvasWidth = geom.width * this.viewport.scale;
3719 }
3720 }
3721 },
3722
3723 setTextContent: function TextLayerBuilder_setTextContent(textContent) {
3724 this.textContent = textContent;
3725
3726 var textItems = textContent.items;
3727 for (var i = 0, len = textItems.length; i < len; i++) {
3728 this.appendText(textItems[i], textContent.styles);
3729 }
3730 this.divContentDone = true;
3731 this.setupRenderLayoutTimer();
3732 },
3733
3734 convertMatches: function TextLayerBuilder_convertMatches(matches) {
3735 var i = 0;
3736 var iIndex = 0;
3737 var bidiTexts = this.textContent.items;
3738 var end = bidiTexts.length - 1;
3739 var queryLen = (this.findController === null ?
3740 0 : this.findController.state.query.length);
3741 var ret = [];
3742
3743 for (var m = 0, len = matches.length; m < len; m++) {
3744 // Calculate the start position.
3745 var matchIdx = matches[m];
3746
3747 // Loop over the divIdxs.
3748 while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
3749 iIndex += bidiTexts[i].str.length;
3750 i++;
3751 }
3752
3753 if (i === bidiTexts.length) {
3754 console.error('Could not find a matching mapping');
3755 }
3756
3757 var match = {
3758 begin: {
3759 divIdx: i,
3760 offset: matchIdx - iIndex
3761 }
3762 };
3763
3764 // Calculate the end position.
3765 matchIdx += queryLen;
3766
3767 // Somewhat the same array as above, but use > instead of >= to get
3768 // the end position right.
3769 while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
3770 iIndex += bidiTexts[i].str.length;
3771 i++;
3772 }
3773
3774 match.end = {
3775 divIdx: i,
3776 offset: matchIdx - iIndex
3777 };
3778 ret.push(match);
3779 }
3780
3781 return ret;
3782 },
3783
3784 renderMatches: function TextLayerBuilder_renderMatches(matches) {
3785 // Early exit if there is nothing to render.
3786 if (matches.length === 0) {
3787 return;
3788 }
3789
3790 var bidiTexts = this.textContent.items;
3791 var textDivs = this.textDivs;
3792 var prevEnd = null;
3793 var isSelectedPage = (this.findController === null ?
3794 false : (this.pageIdx === this.findController.selected.pageIdx));
3795 var selectedMatchIdx = (this.findController === null ?
3796 -1 : this.findController.selected.matchIdx);
3797 var highlightAll = (this.findController === null ?
3798 false : this.findController.state.highlightAll);
3799 var infinity = {
3800 divIdx: -1,
3801 offset: undefined
3802 };
3803
3804 function beginText(begin, className) {
3805 var divIdx = begin.divIdx;
3806 textDivs[divIdx].textContent = '';
3807 appendTextToDiv(divIdx, 0, begin.offset, className);
3808 }
3809
3810 function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
3811 var div = textDivs[divIdx];
3812 var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
3813 var node = document.createTextNode(content);
3814 if (className) {
3815 var span = document.createElement('span');
3816 span.className = className;
3817 span.appendChild(node);
3818 div.appendChild(span);
3819 return;
3820 }
3821 div.appendChild(node);
3822 }
3823
3824 var i0 = selectedMatchIdx, i1 = i0 + 1;
3825 if (highlightAll) {
3826 i0 = 0;
3827 i1 = matches.length;
3828 } else if (!isSelectedPage) {
3829 // Not highlighting all and this isn't the selected page, so do nothing.
3830 return;
3831 }
3832
3833 for (var i = i0; i < i1; i++) {
3834 var match = matches[i];
3835 var begin = match.begin;
3836 var end = match.end;
3837 var isSelected = (isSelectedPage && i === selectedMatchIdx);
3838 var highlightSuffix = (isSelected ? ' selected' : '');
3839
3840 if (isSelected && !this.isViewerInPresentationMode) {
3841 scrollIntoView(textDivs[begin.divIdx],
3842 { top: FIND_SCROLL_OFFSET_TOP,
3843 left: FIND_SCROLL_OFFSET_LEFT });
3844 }
3845
3846 // Match inside new div.
3847 if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
3848 // If there was a previous div, then add the text at the end.
3849 if (prevEnd !== null) {
3850 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
3851 }
3852 // Clear the divs and set the content until the starting point.
3853 beginText(begin);
3854 } else {
3855 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
3856 }
3857
3858 if (begin.divIdx === end.divIdx) {
3859 appendTextToDiv(begin.divIdx, begin.offset, end.offset,
3860 'highlight' + highlightSuffix);
3861 } else {
3862 appendTextToDiv(begin.divIdx, begin.offset, infinity.offset,
3863 'highlight begin' + highlightSuffix);
3864 for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
3865 textDivs[n0].className = 'highlight middle' + highlightSuffix;
3866 }
3867 beginText(end, 'highlight end' + highlightSuffix);
3868 }
3869 prevEnd = end;
3870 }
3871
3872 if (prevEnd) {
3873 appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
3874 }
3875 },
3876
3877 updateMatches: function TextLayerBuilder_updateMatches() {
3878 // Only show matches when all rendering is done.
3879 if (!this.renderingDone) {
3880 return;
3881 }
3882
3883 // Clear all matches.
3884 var matches = this.matches;
3885 var textDivs = this.textDivs;
3886 var bidiTexts = this.textContent.items;
3887 var clearedUntilDivIdx = -1;
3888
3889 // Clear all current matches.
3890 for (var i = 0, len = matches.length; i < len; i++) {
3891 var match = matches[i];
3892 var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
3893 for (var n = begin, end = match.end.divIdx; n <= end; n++) {
3894 var div = textDivs[n];
3895 div.textContent = bidiTexts[n].str;
3896 div.className = '';
3897 }
3898 clearedUntilDivIdx = match.end.divIdx + 1;
3899 }
3900
3901 if (this.findController === null || !this.findController.active) {
3902 return;
3903 }
3904
3905 // Convert the matches on the page controller into the match format
3906 // used for the textLayer.
3907 this.matches = this.convertMatches(this.findController === null ?
3908 [] : (this.findController.pageMatches[this.pageIdx] || []));
3909 this.renderMatches(this.matches);
3910 }
3911 };
3912 return TextLayerBuilder;
3913})();
3914
3915
3916/**
3917 * @typedef {Object} PDFViewerOptions
3918 * @property {HTMLDivElement} container - The container for the viewer element.
3919 * @property {HTMLDivElement} viewer - (optional) The viewer element.
3920 * @property {IPDFLinkService} linkService - The navigation/linking service.
3921 * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering
3922 * queue object.
3923 */
3924
3925/**
3926 * Simple viewer control to display PDF content/pages.
3927 * @class
3928 * @implements {ILastScrollSource}
3929 * @implements {IRenderableView}
3930 */
3931var PDFViewer = (function pdfViewer() {
3932 /**
3933 * @constructs PDFViewer
3934 * @param {PDFViewerOptions} options
3935 */
3936 function PDFViewer(options) {
3937 this.container = options.container;
3938 this.viewer = options.viewer || options.container.firstElementChild;
3939 this.linkService = options.linkService || new SimpleLinkService(this);
3940
3941 this.defaultRenderingQueue = !options.renderingQueue;
3942 if (this.defaultRenderingQueue) {
3943 // Custom rendering queue is not specified, using default one
3944 this.renderingQueue = new PDFRenderingQueue();
3945 this.renderingQueue.setViewer(this);
3946 } else {
3947 this.renderingQueue = options.renderingQueue;
3948 }
3949
3950 this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this));
3951 this.lastScroll = 0;
3952 this.updateInProgress = false;
3953 this.presentationModeState = PresentationModeState.UNKNOWN;
3954 this._resetView();
3955 }
3956
3957 PDFViewer.prototype = /** @lends PDFViewer.prototype */{
3958 get pagesCount() {
3959 return this.pages.length;
3960 },
3961
3962 getPageView: function (index) {
3963 return this.pages[index];
3964 },
3965
3966 get currentPageNumber() {
3967 return this._currentPageNumber;
3968 },
3969
3970 set currentPageNumber(val) {
3971 if (!this.pdfDocument) {
3972 this._currentPageNumber = val;
3973 return;
3974 }
3975
3976 var event = document.createEvent('UIEvents');
3977 event.initUIEvent('pagechange', true, true, window, 0);
3978 event.updateInProgress = this.updateInProgress;
3979
3980 if (!(0 < val && val <= this.pagesCount)) {
3981 event.pageNumber = this._currentPageNumber;
3982 event.previousPageNumber = val;
3983 this.container.dispatchEvent(event);
3984 return;
3985 }
3986
3987 this.pages[val - 1].updateStats();
3988 event.previousPageNumber = this._currentPageNumber;
3989 this._currentPageNumber = val;
3990 event.pageNumber = val;
3991 this.container.dispatchEvent(event);
3992 },
3993
3994 /**
3995 * @returns {number}
3996 */
3997 get currentScale() {
3998 return this._currentScale;
3999 },
4000
4001 /**
4002 * @param {number} val - Scale of the pages in percents.
4003 */
4004 set currentScale(val) {
4005 if (isNaN(val)) {
4006 throw new Error('Invalid numeric scale');
4007 }
4008 if (!this.pdfDocument) {
4009 this._currentScale = val;
4010 this._currentScaleValue = val.toString();
4011 return;
4012 }
4013 this._setScale(val, false);
4014 },
4015
4016 /**
4017 * @returns {string}
4018 */
4019 get currentScaleValue() {
4020 return this._currentScaleValue;
4021 },
4022
4023 /**
4024 * @param val - The scale of the pages (in percent or predefined value).
4025 */
4026 set currentScaleValue(val) {
4027 if (!this.pdfDocument) {
4028 this._currentScale = isNaN(val) ? UNKNOWN_SCALE : val;
4029 this._currentScaleValue = val;
4030 return;
4031 }
4032 this._setScale(val, false);
4033 },
4034
4035 /**
4036 * @returns {number}
4037 */
4038 get pagesRotation() {
4039 return this._pagesRotation;
4040 },
4041
4042 /**
4043 * @param {number} rotation - The rotation of the pages (0, 90, 180, 270).
4044 */
4045 set pagesRotation(rotation) {
4046 this._pagesRotation = rotation;
4047
4048 for (var i = 0, l = this.pages.length; i < l; i++) {
4049 var page = this.pages[i];
4050 page.update(page.scale, rotation);
4051 }
4052
4053 this._setScale(this._currentScaleValue, true);
4054 },
4055
4056 /**
4057 * @param pdfDocument {PDFDocument}
4058 */
4059 setDocument: function (pdfDocument) {
4060 if (this.pdfDocument) {
4061 this._resetView();
4062 }
4063
4064 this.pdfDocument = pdfDocument;
4065 if (!pdfDocument) {
4066 return;
4067 }
4068
4069 var pagesCount = pdfDocument.numPages;
4070 var pagesRefMap = this.pagesRefMap = {};
4071 var self = this;
4072
4073 var resolvePagesPromise;
4074 var pagesPromise = new Promise(function (resolve) {
4075 resolvePagesPromise = resolve;
4076 });
4077 this.pagesPromise = pagesPromise;
4078 pagesPromise.then(function () {
4079 var event = document.createEvent('CustomEvent');
4080 event.initCustomEvent('pagesloaded', true, true, {
4081 pagesCount: pagesCount
4082 });
4083 self.container.dispatchEvent(event);
4084 });
4085
4086 var isOnePageRenderedResolved = false;
4087 var resolveOnePageRendered = null;
4088 var onePageRendered = new Promise(function (resolve) {
4089 resolveOnePageRendered = resolve;
4090 });
4091 this.onePageRendered = onePageRendered;
4092
4093 var bindOnAfterDraw = function (pageView) {
4094 // when page is painted, using the image as thumbnail base
4095 pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() {
4096 if (!isOnePageRenderedResolved) {
4097 isOnePageRenderedResolved = true;
4098 resolveOnePageRendered();
4099 }
4100 var event = document.createEvent('CustomEvent');
4101 event.initCustomEvent('pagerendered', true, true, {
4102 pageNumber: pageView.id
4103 });
4104 self.container.dispatchEvent(event);
4105 };
4106 };
4107
4108 var firstPagePromise = pdfDocument.getPage(1);
4109 this.firstPagePromise = firstPagePromise;
4110
4111 // Fetch a single page so we can get a viewport that will be the default
4112 // viewport for all pages
4113 return firstPagePromise.then(function(pdfPage) {
4114 var scale = this._currentScale || 1.0;
4115 var viewport = pdfPage.getViewport(scale * CSS_UNITS);
4116 for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
4117 var pageSource = new PDFPageSource(pdfDocument, pageNum);
4118 var pageView = new PageView(this.viewer, pageNum, scale,
4119 viewport.clone(), this.linkService,
4120 this.renderingQueue, this.cache,
4121 pageSource, this);
4122 bindOnAfterDraw(pageView);
4123 this.pages.push(pageView);
4124 }
4125
4126 // Fetch all the pages since the viewport is needed before printing
4127 // starts to create the correct size canvas. Wait until one page is
4128 // rendered so we don't tie up too many resources early on.
4129 onePageRendered.then(function () {
4130 if (!PDFJS.disableAutoFetch) {
4131 var getPagesLeft = pagesCount;
4132 for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
4133 pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) {
4134 var pageView = self.pages[pageNum - 1];
4135 if (!pageView.pdfPage) {
4136 pageView.setPdfPage(pdfPage);
4137 }
4138 var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R';
4139 pagesRefMap[refStr] = pageNum;
4140 getPagesLeft--;
4141 if (!getPagesLeft) {
4142 resolvePagesPromise();
4143 }
4144 }.bind(null, pageNum));
4145 }
4146 } else {
4147 // XXX: Printing is semi-broken with auto fetch disabled.
4148 resolvePagesPromise();
4149 }
4150 });
4151
4152 var event = document.createEvent('CustomEvent');
4153 event.initCustomEvent('pagesinit', true, true, null);
4154 self.container.dispatchEvent(event);
4155
4156 if (this.defaultRenderingQueue) {
4157 this.update();
4158 }
4159 }.bind(this));
4160 },
4161
4162 _resetView: function () {
4163 this.cache = new Cache(DEFAULT_CACHE_SIZE);
4164 this.pages = [];
4165 this._currentPageNumber = 1;
4166 this._currentScale = UNKNOWN_SCALE;
4167 this._currentScaleValue = null;
4168 this.location = null;
4169 this._pagesRotation = 0;
4170
4171 var container = this.viewer;
4172 while (container.hasChildNodes()) {
4173 container.removeChild(container.lastChild);
4174 }
4175 },
4176
4177 _scrollUpdate: function () {
4178 this.lastScroll = Date.now();
4179
4180 if (this.pagesCount === 0) {
4181 return;
4182 }
4183 this.update();
4184 },
4185
4186 _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages(
4187 newScale, newValue, noScroll, preset) {
4188 this._currentScaleValue = newValue;
4189 if (newScale === this._currentScale) {
4190 return;
4191 }
4192 for (var i = 0, ii = this.pages.length; i < ii; i++) {
4193 this.pages[i].update(newScale);
4194 }
4195 this._currentScale = newScale;
4196
4197 if (!noScroll) {
4198 var page = this._currentPageNumber, dest;
4199 var inPresentationMode =
4200 this.presentationModeState === PresentationModeState.CHANGING ||
4201 this.presentationModeState === PresentationModeState.FULLSCREEN;
4202 if (this.location && !inPresentationMode &&
4203 !IGNORE_CURRENT_POSITION_ON_ZOOM) {
4204 page = this.location.pageNumber;
4205 dest = [null, { name: 'XYZ' }, this.location.left,
4206 this.location.top, null];
4207 }
4208 this.scrollPageIntoView(page, dest);
4209 }
4210
4211 var event = document.createEvent('UIEvents');
4212 event.initUIEvent('scalechange', true, true, window, 0);
4213 event.scale = newScale;
4214 if (preset) {
4215 event.presetValue = newValue;
4216 }
4217 this.container.dispatchEvent(event);
4218 },
4219
4220 _setScale: function pdfViewer_setScale(value, noScroll) {
4221 if (value === 'custom') {
4222 return;
4223 }
4224 var scale = parseFloat(value);
4225
4226 if (scale > 0) {
4227 this._setScaleUpdatePages(scale, value, noScroll, false);
4228 } else {
4229 var currentPage = this.pages[this._currentPageNumber - 1];
4230 if (!currentPage) {
4231 return;
4232 }
4233 var inPresentationMode =
4234 this.presentationModeState === PresentationModeState.FULLSCREEN;
4235 var hPadding = inPresentationMode ? 0 : SCROLLBAR_PADDING;
4236 var vPadding = inPresentationMode ? 0 : VERTICAL_PADDING;
4237 var pageWidthScale = (this.container.clientWidth - hPadding) /
4238 currentPage.width * currentPage.scale;
4239 var pageHeightScale = (this.container.clientHeight - vPadding) /
4240 currentPage.height * currentPage.scale;
4241 switch (value) {
4242 case 'page-actual':
4243 scale = 1;
4244 break;
4245 case 'page-width':
4246 scale = pageWidthScale;
4247 break;
4248 case 'page-height':
4249 scale = pageHeightScale;
4250 break;
4251 case 'page-fit':
4252 scale = Math.min(pageWidthScale, pageHeightScale);
4253 break;
4254 case 'auto':
4255 var isLandscape = (currentPage.width > currentPage.height);
4256 // For pages in landscape mode, fit the page height to the viewer
4257 // *unless* the page would thus become too wide to fit horizontally.
4258 var horizontalScale = isLandscape ?
4259 Math.min(pageHeightScale, pageWidthScale) : pageWidthScale;
4260 scale = Math.min(MAX_AUTO_SCALE, horizontalScale);
4261 break;
4262 default:
4263 console.error('pdfViewSetScale: \'' + value +
4264 '\' is an unknown zoom value.');
4265 return;
4266 }
4267 this._setScaleUpdatePages(scale, value, noScroll, true);
4268 }
4269 },
4270
4271 /**
4272 * Scrolls page into view.
4273 * @param {number} pageNumber
4274 * @param {Array} dest - (optional) original PDF destination array:
4275 * <page-ref> </XYZ|FitXXX> <args..>
4276 */
4277 scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber,
4278 dest) {
4279 var pageView = this.pages[pageNumber - 1];
4280 var pageViewDiv = pageView.el;
4281
4282 if (this.presentationModeState ===
4283 PresentationModeState.FULLSCREEN) {
4284 if (this.linkService.page !== pageView.id) {
4285 // Avoid breaking getVisiblePages in presentation mode.
4286 this.linkService.page = pageView.id;
4287 return;
4288 }
4289 dest = null;
4290 // Fixes the case when PDF has different page sizes.
4291 this._setScale(this.currentScaleValue, true);
4292 }
4293 if (!dest) {
4294 scrollIntoView(pageViewDiv);
4295 return;
4296 }
4297
4298 var x = 0, y = 0;
4299 var width = 0, height = 0, widthScale, heightScale;
4300 var changeOrientation = (pageView.rotation % 180 === 0 ? false : true);
4301 var pageWidth = (changeOrientation ? pageView.height : pageView.width) /
4302 pageView.scale / CSS_UNITS;
4303 var pageHeight = (changeOrientation ? pageView.width : pageView.height) /
4304 pageView.scale / CSS_UNITS;
4305 var scale = 0;
4306 switch (dest[1].name) {
4307 case 'XYZ':
4308 x = dest[2];
4309 y = dest[3];
4310 scale = dest[4];
4311 // If x and/or y coordinates are not supplied, default to
4312 // _top_ left of the page (not the obvious bottom left,
4313 // since aligning the bottom of the intended page with the
4314 // top of the window is rarely helpful).
4315 x = x !== null ? x : 0;
4316 y = y !== null ? y : pageHeight;
4317 break;
4318 case 'Fit':
4319 case 'FitB':
4320 scale = 'page-fit';
4321 break;
4322 case 'FitH':
4323 case 'FitBH':
4324 y = dest[2];
4325 scale = 'page-width';
4326 break;
4327 case 'FitV':
4328 case 'FitBV':
4329 x = dest[2];
4330 width = pageWidth;
4331 height = pageHeight;
4332 scale = 'page-height';
4333 break;
4334 case 'FitR':
4335 x = dest[2];
4336 y = dest[3];
4337 width = dest[4] - x;
4338 height = dest[5] - y;
4339 var viewerContainer = this.container;
4340 widthScale = (viewerContainer.clientWidth - SCROLLBAR_PADDING) /
4341 width / CSS_UNITS;
4342 heightScale = (viewerContainer.clientHeight - SCROLLBAR_PADDING) /
4343 height / CSS_UNITS;
4344 scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
4345 break;
4346 default:
4347 return;
4348 }
4349
4350 if (scale && scale !== this.currentScale) {
4351 this.currentScaleValue = scale;
4352 } else if (this.currentScale === UNKNOWN_SCALE) {
4353 this.currentScaleValue = DEFAULT_SCALE;
4354 }
4355
4356 if (scale === 'page-fit' && !dest[4]) {
4357 scrollIntoView(pageViewDiv);
4358 return;
4359 }
4360
4361 var boundingRect = [
4362 pageView.viewport.convertToViewportPoint(x, y),
4363 pageView.viewport.convertToViewportPoint(x + width, y + height)
4364 ];
4365 var left = Math.min(boundingRect[0][0], boundingRect[1][0]);
4366 var top = Math.min(boundingRect[0][1], boundingRect[1][1]);
4367
4368 scrollIntoView(pageViewDiv, { left: left, top: top });
4369 },
4370
4371 _updateLocation: function (firstPage) {
4372 var currentScale = this._currentScale;
4373 var currentScaleValue = this._currentScaleValue;
4374 var normalizedScaleValue =
4375 parseFloat(currentScaleValue) === currentScale ?
4376 Math.round(currentScale * 10000) / 100 : currentScaleValue;
4377
4378 var pageNumber = firstPage.id;
4379 var pdfOpenParams = '#page=' + pageNumber;
4380 pdfOpenParams += '&zoom=' + normalizedScaleValue;
4381 var currentPageView = this.pages[pageNumber - 1];
4382 var container = this.container;
4383 var topLeft = currentPageView.getPagePoint(
4384 (container.scrollLeft - firstPage.x),
4385 (container.scrollTop - firstPage.y));
4386 var intLeft = Math.round(topLeft[0]);
4387 var intTop = Math.round(topLeft[1]);
4388 pdfOpenParams += ',' + intLeft + ',' + intTop;
4389
4390 this.location = {
4391 pageNumber: pageNumber,
4392 scale: normalizedScaleValue,
4393 top: intTop,
4394 left: intLeft,
4395 pdfOpenParams: pdfOpenParams
4396 };
4397 },
4398
4399 update: function () {
4400 var visible = this._getVisiblePages();
4401 var visiblePages = visible.views;
4402 if (visiblePages.length === 0) {
4403 return;
4404 }
4405
4406 this.updateInProgress = true;
4407
4408 var suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
4409 2 * visiblePages.length + 1);
4410 this.cache.resize(suggestedCacheSize);
4411
4412 this.renderingQueue.renderHighestPriority(visible);
4413
4414 var currentId = this.currentPageNumber;
4415 var firstPage = visible.first;
4416
4417 for (var i = 0, ii = visiblePages.length, stillFullyVisible = false;
4418 i < ii; ++i) {
4419 var page = visiblePages[i];
4420
4421 if (page.percent < 100) {
4422 break;
4423 }
4424 if (page.id === currentId) {
4425 stillFullyVisible = true;
4426 break;
4427 }
4428 }
4429
4430 if (!stillFullyVisible) {
4431 currentId = visiblePages[0].id;
4432 }
4433
4434 if (this.presentationModeState !== PresentationModeState.FULLSCREEN) {
4435 this.currentPageNumber = currentId;
4436 }
4437
4438 this._updateLocation(firstPage);
4439
4440 this.updateInProgress = false;
4441
4442 var event = document.createEvent('UIEvents');
4443 event.initUIEvent('updateviewarea', true, true, window, 0);
4444 this.container.dispatchEvent(event);
4445 },
4446
4447 containsElement: function (element) {
4448 return this.container.contains(element);
4449 },
4450
4451 focus: function () {
4452 this.container.focus();
4453 },
4454
4455 blur: function () {
4456 this.container.blur();
4457 },
4458
4459 get isHorizontalScrollbarEnabled() {
4460 return (this.presentationModeState === PresentationModeState.FULLSCREEN ?
4461 false : (this.container.scrollWidth > this.container.clientWidth));
4462 },
4463
4464 _getVisiblePages: function () {
4465 if (this.presentationModeState !== PresentationModeState.FULLSCREEN) {
4466 return getVisibleElements(this.container, this.pages, true);
4467 } else {
4468 // The algorithm in getVisibleElements doesn't work in all browsers and
4469 // configurations when presentation mode is active.
4470 var visible = [];
4471 var currentPage = this.pages[this._currentPageNumber - 1];
4472 visible.push({ id: currentPage.id, view: currentPage });
4473 return { first: currentPage, last: currentPage, views: visible };
4474 }
4475 },
4476
4477 cleanup: function () {
4478 for (var i = 0, ii = this.pages.length; i < ii; i++) {
4479 if (this.pages[i] &&
4480 this.pages[i].renderingState !== RenderingStates.FINISHED) {
4481 this.pages[i].reset();
4482 }
4483 }
4484 },
4485
4486 forceRendering: function (currentlyVisiblePages) {
4487 var visiblePages = currentlyVisiblePages || this._getVisiblePages();
4488 var pageView = this.renderingQueue.getHighestPriority(visiblePages,
4489 this.pages,
4490 this.scroll.down);
4491 if (pageView) {
4492 this.renderingQueue.renderView(pageView);
4493 return true;
4494 }
4495 return false;
4496 },
4497
4498 getPageTextContent: function (pageIndex) {
4499 return this.pdfDocument.getPage(pageIndex + 1).then(function (page) {
4500 return page.getTextContent();
4501 });
4502 },
4503
4504 /**
4505 * @param textLayerDiv {HTMLDivElement}
4506 * @param pageIndex {number}
4507 * @param viewport {PageViewport}
4508 * @returns {TextLayerBuilder}
4509 */
4510 createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
4511 var isViewerInPresentationMode =
4512 this.presentationModeState === PresentationModeState.FULLSCREEN;
4513 return new TextLayerBuilder({
4514 textLayerDiv: textLayerDiv,
4515 pageIndex: pageIndex,
4516 viewport: viewport,
4517 lastScrollSource: this,
4518 isViewerInPresentationMode: isViewerInPresentationMode,
4519 findController: this.findController
4520 });
4521 },
4522
4523 setFindController: function (findController) {
4524 this.findController = findController;
4525 },
4526 };
4527
4528 return PDFViewer;
4529})();
4530
4531var SimpleLinkService = (function SimpleLinkServiceClosure() {
4532 function SimpleLinkService(pdfViewer) {
4533 this.pdfViewer = pdfViewer;
4534 }
4535 SimpleLinkService.prototype = {
4536 /**
4537 * @returns {number}
4538 */
4539 get page() {
4540 return this.pdfViewer.currentPageNumber;
4541 },
4542 /**
4543 * @param {number} value
4544 */
4545 set page(value) {
4546 this.pdfViewer.currentPageNumber = value;
4547 },
4548 /**
4549 * @param dest - The PDF destination object.
4550 */
4551 navigateTo: function (dest) {},
4552 /**
4553 * @param dest - The PDF destination object.
4554 * @returns {string} The hyperlink to the PDF object.
4555 */
4556 getDestinationHash: function (dest) {
4557 return '#';
4558 },
4559 /**
4560 * @param hash - The PDF parameters/hash.
4561 * @returns {string} The hyperlink to the PDF object.
4562 */
4563 getAnchorUrl: function (hash) {
4564 return '#';
4565 },
4566 /**
4567 * @param {string} hash
4568 */
4569 setHash: function (hash) {},
4570 /**
4571 * @param {string} action
4572 */
4573 executeNamedAction: function (action) {},
4574 };
4575 return SimpleLinkService;
4576})();
4577
4578/**
4579 * PDFPage object source.
4580 * @class
4581 */
4582var PDFPageSource = (function PDFPageSourceClosure() {
4583 /**
4584 * @constructs
4585 * @param {PDFDocument} pdfDocument
4586 * @param {number} pageNumber
4587 * @constructor
4588 */
4589 function PDFPageSource(pdfDocument, pageNumber) {
4590 this.pdfDocument = pdfDocument;
4591 this.pageNumber = pageNumber;
4592 }
4593
4594 PDFPageSource.prototype = /** @lends PDFPageSource.prototype */ {
4595 /**
4596 * @returns {Promise<PDFPage>}
4597 */
4598 getPage: function () {
4599 return this.pdfDocument.getPage(this.pageNumber);
4600 }
4601 };
4602
4603 return PDFPageSource;
4604})();
4605
4606
4607var PDFViewerApplication = {
4608 initialBookmark: document.location.hash.substring(1),
4609 initialized: false,
4610 fellback: false,
4611 pdfDocument: null,
4612 sidebarOpen: false,
4613 printing: false,
4614 /** @type {PDFViewer} */
4615 pdfViewer: null,
4616 /** @type {PDFThumbnailViewer} */
4617 pdfThumbnailViewer: null,
4618 /** @type {PDFRenderingQueue} */
4619 pdfRenderingQueue: null,
4620 pageRotation: 0,
4621 updateScaleControls: true,
4622 isInitialViewSet: false,
4623 animationStartedPromise: null,
4624 mouseScrollTimeStamp: 0,
4625 mouseScrollDelta: 0,
4626 preferenceSidebarViewOnLoad: SidebarView.NONE,
4627 preferencePdfBugEnabled: false,
4628 isViewerEmbedded: (window.parent !== window),
4629 url: '',
4630
4631 // called once when the document is loaded
4632 initialize: function pdfViewInitialize() {
4633 var pdfRenderingQueue = new PDFRenderingQueue();
4634 pdfRenderingQueue.onIdle = this.cleanup.bind(this);
4635 this.pdfRenderingQueue = pdfRenderingQueue;
4636
4637 var container = document.getElementById('viewerContainer');
4638 var viewer = document.getElementById('viewer');
4639 this.pdfViewer = new PDFViewer({
4640 container: container,
4641 viewer: viewer,
4642 renderingQueue: pdfRenderingQueue,
4643 linkService: this
4644 });
4645 pdfRenderingQueue.setViewer(this.pdfViewer);
4646
4647 var thumbnailContainer = document.getElementById('thumbnailView');
4648 this.pdfThumbnailViewer = new PDFThumbnailViewer({
4649 container: thumbnailContainer,
4650 renderingQueue: pdfRenderingQueue,
4651 linkService: this
4652 });
4653 pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
4654
4655 Preferences.initialize();
4656
4657 this.findController = new PDFFindController({
4658 pdfViewer: this.pdfViewer,
4659 integratedFind: this.supportsIntegratedFind
4660 });
4661 this.pdfViewer.setFindController(this.findController);
4662
4663 this.findBar = new PDFFindBar({
4664 bar: document.getElementById('findbar'),
4665 toggleButton: document.getElementById('viewFind'),
4666 findField: document.getElementById('findInput'),
4667 highlightAllCheckbox: document.getElementById('findHighlightAll'),
4668 caseSensitiveCheckbox: document.getElementById('findMatchCase'),
4669 findMsg: document.getElementById('findMsg'),
4670 findStatusIcon: document.getElementById('findStatusIcon'),
4671 findPreviousButton: document.getElementById('findPrevious'),
4672 findNextButton: document.getElementById('findNext'),
4673 findController: this.findController
4674 });
4675
4676 this.findController.setFindBar(this.findBar);
4677
4678 HandTool.initialize({
4679 container: container,
4680 toggleHandTool: document.getElementById('toggleHandTool')
4681 });
4682
4683 SecondaryToolbar.initialize({
4684 toolbar: document.getElementById('secondaryToolbar'),
4685 presentationMode: PresentationMode,
4686 toggleButton: document.getElementById('secondaryToolbarToggle'),
4687 presentationModeButton:
4688 document.getElementById('secondaryPresentationMode'),
4689 openFile: document.getElementById('secondaryOpenFile'),
4690 print: document.getElementById('secondaryPrint'),
4691 download: document.getElementById('secondaryDownload'),
4692 viewBookmark: document.getElementById('secondaryViewBookmark'),
4693 firstPage: document.getElementById('firstPage'),
4694 lastPage: document.getElementById('lastPage'),
4695 pageRotateCw: document.getElementById('pageRotateCw'),
4696 pageRotateCcw: document.getElementById('pageRotateCcw'),
4697 documentProperties: DocumentProperties,
4698 documentPropertiesButton: document.getElementById('documentProperties')
4699 });
4700
4701 PresentationMode.initialize({
4702 container: container,
4703 secondaryToolbar: SecondaryToolbar,
4704 firstPage: document.getElementById('contextFirstPage'),
4705 lastPage: document.getElementById('contextLastPage'),
4706 pageRotateCw: document.getElementById('contextPageRotateCw'),
4707 pageRotateCcw: document.getElementById('contextPageRotateCcw')
4708 });
4709
4710 PasswordPrompt.initialize({
4711 overlayName: 'passwordOverlay',
4712 passwordField: document.getElementById('password'),
4713 passwordText: document.getElementById('passwordText'),
4714 passwordSubmit: document.getElementById('passwordSubmit'),
4715 passwordCancel: document.getElementById('passwordCancel')
4716 });
4717
4718 DocumentProperties.initialize({
4719 overlayName: 'documentPropertiesOverlay',
4720 closeButton: document.getElementById('documentPropertiesClose'),
4721 fileNameField: document.getElementById('fileNameField'),
4722 fileSizeField: document.getElementById('fileSizeField'),
4723 titleField: document.getElementById('titleField'),
4724 authorField: document.getElementById('authorField'),
4725 subjectField: document.getElementById('subjectField'),
4726 keywordsField: document.getElementById('keywordsField'),
4727 creationDateField: document.getElementById('creationDateField'),
4728 modificationDateField: document.getElementById('modificationDateField'),
4729 creatorField: document.getElementById('creatorField'),
4730 producerField: document.getElementById('producerField'),
4731 versionField: document.getElementById('versionField'),
4732 pageCountField: document.getElementById('pageCountField')
4733 });
4734
4735 var self = this;
4736 var initializedPromise = Promise.all([
4737 Preferences.get('enableWebGL').then(function resolved(value) {
4738 PDFJS.disableWebGL = !value;
4739 }),
4740 Preferences.get('sidebarViewOnLoad').then(function resolved(value) {
4741 self.preferenceSidebarViewOnLoad = value;
4742 }),
4743 Preferences.get('pdfBugEnabled').then(function resolved(value) {
4744 self.preferencePdfBugEnabled = value;
4745 }),
4746 Preferences.get('disableTextLayer').then(function resolved(value) {
4747 if (PDFJS.disableTextLayer === true) {
4748 return;
4749 }
4750 PDFJS.disableTextLayer = value;
4751 }),
4752 Preferences.get('disableRange').then(function resolved(value) {
4753 if (PDFJS.disableRange === true) {
4754 return;
4755 }
4756 PDFJS.disableRange = value;
4757 }),
4758 Preferences.get('disableAutoFetch').then(function resolved(value) {
4759 PDFJS.disableAutoFetch = value;
4760 }),
4761 Preferences.get('disableFontFace').then(function resolved(value) {
4762 if (PDFJS.disableFontFace === true) {
4763 return;
4764 }
4765 PDFJS.disableFontFace = value;
4766 }),
4767 Preferences.get('useOnlyCssZoom').then(function resolved(value) {
4768 PDFJS.useOnlyCssZoom = value;
4769 })
4770 // TODO move more preferences and other async stuff here
4771 ]).catch(function (reason) { });
4772
4773 return initializedPromise.then(function () {
4774 PDFViewerApplication.initialized = true;
4775 });
4776 },
4777
4778 zoomIn: function pdfViewZoomIn(ticks) {
4779 var newScale = this.pdfViewer.currentScale;
4780 do {
4781 newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
4782 newScale = Math.ceil(newScale * 10) / 10;
4783 newScale = Math.min(MAX_SCALE, newScale);
4784 } while (--ticks && newScale < MAX_SCALE);
4785 this.setScale(newScale, true);
4786 },
4787
4788 zoomOut: function pdfViewZoomOut(ticks) {
4789 var newScale = this.pdfViewer.currentScale;
4790 do {
4791 newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
4792 newScale = Math.floor(newScale * 10) / 10;
4793 newScale = Math.max(MIN_SCALE, newScale);
4794 } while (--ticks && newScale > MIN_SCALE);
4795 this.setScale(newScale, true);
4796 },
4797
4798 get currentScaleValue() {
4799 return this.pdfViewer.currentScaleValue;
4800 },
4801
4802 get pagesCount() {
4803 return this.pdfDocument.numPages;
4804 },
4805
4806 set page(val) {
4807 this.pdfViewer.currentPageNumber = val;
4808 },
4809
4810 get page() {
4811 return this.pdfViewer.currentPageNumber;
4812 },
4813
4814 get supportsPrinting() {
4815 var canvas = document.createElement('canvas');
4816 var value = 'mozPrintCallback' in canvas;
4817 // shadow
4818 Object.defineProperty(this, 'supportsPrinting', { value: value,
4819 enumerable: true,
4820 configurable: true,
4821 writable: false });
4822 return value;
4823 },
4824
4825 get supportsFullscreen() {
4826 var doc = document.documentElement;
4827 var support = doc.requestFullscreen || doc.mozRequestFullScreen ||
4828 doc.webkitRequestFullScreen || doc.msRequestFullscreen;
4829
4830 if (document.fullscreenEnabled === false ||
4831 document.mozFullScreenEnabled === false ||
4832 document.webkitFullscreenEnabled === false ||
4833 document.msFullscreenEnabled === false) {
4834 support = false;
4835 }
4836
4837 Object.defineProperty(this, 'supportsFullscreen', { value: support,
4838 enumerable: true,
4839 configurable: true,
4840 writable: false });
4841 return support;
4842 },
4843
4844 get supportsIntegratedFind() {
4845 var support = false;
4846 Object.defineProperty(this, 'supportsIntegratedFind', { value: support,
4847 enumerable: true,
4848 configurable: true,
4849 writable: false });
4850 return support;
4851 },
4852
4853 get supportsDocumentFonts() {
4854 var support = true;
4855 Object.defineProperty(this, 'supportsDocumentFonts', { value: support,
4856 enumerable: true,
4857 configurable: true,
4858 writable: false });
4859 return support;
4860 },
4861
4862 get supportsDocumentColors() {
4863 var support = true;
4864 Object.defineProperty(this, 'supportsDocumentColors', { value: support,
4865 enumerable: true,
4866 configurable: true,
4867 writable: false });
4868 return support;
4869 },
4870
4871 get loadingBar() {
4872 var bar = new ProgressBar('#loadingBar', {});
4873 Object.defineProperty(this, 'loadingBar', { value: bar,
4874 enumerable: true,
4875 configurable: true,
4876 writable: false });
4877 return bar;
4878 },
4879
4880
4881 setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) {
4882 this.url = url;
4883 try {
4884 this.setTitle(decodeURIComponent(getFileName(url)) || url);
4885 } catch (e) {
4886 // decodeURIComponent may throw URIError,
4887 // fall back to using the unprocessed url in that case
4888 this.setTitle(url);
4889 }
4890 },
4891
4892 setTitle: function pdfViewSetTitle(title) {
4893 document.title = title;
4894 },
4895
4896 close: function pdfViewClose() {
4897 var errorWrapper = document.getElementById('errorWrapper');
4898 errorWrapper.setAttribute('hidden', 'true');
4899
4900 if (!this.pdfDocument) {
4901 return;
4902 }
4903
4904 this.pdfDocument.destroy();
4905 this.pdfDocument = null;
4906
4907 this.pdfThumbnailViewer.setDocument(null);
4908 this.pdfViewer.setDocument(null);
4909
4910 if (typeof PDFBug !== 'undefined') {
4911 PDFBug.cleanup();
4912 }
4913 },
4914
4915 // TODO(mack): This function signature should really be pdfViewOpen(url, args)
4916 open: function pdfViewOpen(file, scale, password,
4917 pdfDataRangeTransport, args) {
4918 if (this.pdfDocument) {
4919 // Reload the preferences if a document was previously opened.
4920 Preferences.reload();
4921 }
4922 this.close();
4923
4924 var parameters = {password: password};
4925 if (typeof file === 'string') { // URL
4926 this.setTitleUsingUrl(file);
4927 parameters.url = file;
4928 } else if (file && 'byteLength' in file) { // ArrayBuffer
4929 parameters.data = file;
4930 } else if (file.url && file.originalUrl) {
4931 this.setTitleUsingUrl(file.originalUrl);
4932 parameters.url = file.url;
4933 }
4934 if (args) {
4935 for (var prop in args) {
4936 parameters[prop] = args[prop];
4937 }
4938 }
4939
4940 var self = this;
4941 self.loading = true;
4942 self.downloadComplete = false;
4943
4944 var passwordNeeded = function passwordNeeded(updatePassword, reason) {
4945 PasswordPrompt.updatePassword = updatePassword;
4946 PasswordPrompt.reason = reason;
4947 PasswordPrompt.open();
4948 };
4949
4950 function getDocumentProgress(progressData) {
4951 self.progress(progressData.loaded / progressData.total);
4952 }
4953
4954 PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded,
4955 getDocumentProgress).then(
4956 function getDocumentCallback(pdfDocument) {
4957 self.load(pdfDocument, scale);
4958 self.loading = false;
4959 },
4960 function getDocumentError(exception) {
4961 var message = exception && exception.message;
4962 var loadingErrorMessage = mozL10n.get('loading_error', null,
4963 'An error occurred while loading the PDF.');
4964
4965 if (exception instanceof PDFJS.InvalidPDFException) {
4966 // change error message also for other builds
4967 loadingErrorMessage = mozL10n.get('invalid_file_error', null,
4968 'Invalid or corrupted PDF file.');
4969 } else if (exception instanceof PDFJS.MissingPDFException) {
4970 // special message for missing PDF's
4971 loadingErrorMessage = mozL10n.get('missing_file_error', null,
4972 'Missing PDF file.');
4973 } else if (exception instanceof PDFJS.UnexpectedResponseException) {
4974 loadingErrorMessage = mozL10n.get('unexpected_response_error', null,
4975 'Unexpected server response.');
4976 }
4977
4978 var moreInfo = {
4979 message: message
4980 };
4981 self.error(loadingErrorMessage, moreInfo);
4982 self.loading = false;
4983 }
4984 );
4985
4986 if (args && args.length) {
4987 DocumentProperties.setFileSize(args.length);
4988 }
4989 },
4990
4991 download: function pdfViewDownload() {
4992 function downloadByUrl() {
4993 downloadManager.downloadUrl(url, filename);
4994 }
4995
4996 var url = this.url.split('#')[0];
4997 var filename = getPDFFileNameFromURL(url);
4998 var downloadManager = new DownloadManager();
4999 downloadManager.onerror = function (err) {
5000 // This error won't really be helpful because it's likely the
5001 // fallback won't work either (or is already open).
5002 PDFViewerApplication.error('PDF failed to download.');
5003 };
5004
5005 if (!this.pdfDocument) { // the PDF is not ready yet
5006 downloadByUrl();
5007 return;
5008 }
5009
5010 if (!this.downloadComplete) { // the PDF is still downloading
5011 downloadByUrl();
5012 return;
5013 }
5014
5015 this.pdfDocument.getData().then(
5016 function getDataSuccess(data) {
5017 var blob = PDFJS.createBlob(data, 'application/pdf');
5018 downloadManager.download(blob, url, filename);
5019 },
5020 downloadByUrl // Error occurred try downloading with just the url.
5021 ).then(null, downloadByUrl);
5022 },
5023
5024 fallback: function pdfViewFallback(featureId) {
5025 return;
5026 },
5027
5028 navigateTo: function pdfViewNavigateTo(dest) {
5029 var destString = '';
5030 var self = this;
5031
5032 var goToDestination = function(destRef) {
5033 self.pendingRefStr = null;
5034 // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
5035 var pageNumber = destRef instanceof Object ?
5036 self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
5037 (destRef + 1);
5038 if (pageNumber) {
5039 if (pageNumber > self.pagesCount) {
5040 pageNumber = self.pagesCount;
5041 }
5042 self.pdfViewer.scrollPageIntoView(pageNumber, dest);
5043
5044 // Update the browsing history.
5045 PDFHistory.push({ dest: dest, hash: destString, page: pageNumber });
5046 } else {
5047 self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) {
5048 var pageNum = pageIndex + 1;
5049 self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum;
5050 goToDestination(destRef);
5051 });
5052 }
5053 };
5054
5055 var destinationPromise;
5056 if (typeof dest === 'string') {
5057 destString = dest;
5058 destinationPromise = this.pdfDocument.getDestination(dest);
5059 } else {
5060 destinationPromise = Promise.resolve(dest);
5061 }
5062 destinationPromise.then(function(destination) {
5063 dest = destination;
5064 if (!(destination instanceof Array)) {
5065 return; // invalid destination
5066 }
5067 goToDestination(destination[0]);
5068 });
5069 },
5070
5071 executeNamedAction: function pdfViewExecuteNamedAction(action) {
5072 // See PDF reference, table 8.45 - Named action
5073 switch (action) {
5074 case 'GoToPage':
5075 document.getElementById('pageNumber').focus();
5076 break;
5077
5078 case 'GoBack':
5079 PDFHistory.back();
5080 break;
5081
5082 case 'GoForward':
5083 PDFHistory.forward();
5084 break;
5085
5086 case 'Find':
5087 if (!this.supportsIntegratedFind) {
5088 this.findBar.toggle();
5089 }
5090 break;
5091
5092 case 'NextPage':
5093 this.page++;
5094 break;
5095
5096 case 'PrevPage':
5097 this.page--;
5098 break;
5099
5100 case 'LastPage':
5101 this.page = this.pagesCount;
5102 break;
5103
5104 case 'FirstPage':
5105 this.page = 1;
5106 break;
5107
5108 default:
5109 break; // No action according to spec
5110 }
5111 },
5112
5113 getDestinationHash: function pdfViewGetDestinationHash(dest) {
5114 if (typeof dest === 'string') {
5115 return this.getAnchorUrl('#' + escape(dest));
5116 }
5117 if (dest instanceof Array) {
5118 var destRef = dest[0]; // see navigateTo method for dest format
5119 var pageNumber = destRef instanceof Object ?
5120 this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
5121 (destRef + 1);
5122 if (pageNumber) {
5123 var pdfOpenParams = this.getAnchorUrl('#page=' + pageNumber);
5124 var destKind = dest[1];
5125 if (typeof destKind === 'object' && 'name' in destKind &&
5126 destKind.name === 'XYZ') {
5127 var scale = (dest[4] || this.currentScaleValue);
5128 var scaleNumber = parseFloat(scale);
5129 if (scaleNumber) {
5130 scale = scaleNumber * 100;
5131 }
5132 pdfOpenParams += '&zoom=' + scale;
5133 if (dest[2] || dest[3]) {
5134 pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
5135 }
5136 }
5137 return pdfOpenParams;
5138 }
5139 }
5140 return '';
5141 },
5142
5143 /**
5144 * Prefix the full url on anchor links to make sure that links are resolved
5145 * relative to the current URL instead of the one defined in <base href>.
5146 * @param {String} anchor The anchor hash, including the #.
5147 */
5148 getAnchorUrl: function getAnchorUrl(anchor) {
5149 return anchor;
5150 },
5151
5152 /**
5153 * Show the error box.
5154 * @param {String} message A message that is human readable.
5155 * @param {Object} moreInfo (optional) Further information about the error
5156 * that is more technical. Should have a 'message'
5157 * and optionally a 'stack' property.
5158 */
5159 error: function pdfViewError(message, moreInfo) {
5160 var moreInfoText = mozL10n.get('error_version_info',
5161 {version: PDFJS.version || '?', build: PDFJS.build || '?'},
5162 'PDF.js v{{version}} (build: {{build}})') + '\n';
5163 if (moreInfo) {
5164 moreInfoText +=
5165 mozL10n.get('error_message', {message: moreInfo.message},
5166 'Message: {{message}}');
5167 if (moreInfo.stack) {
5168 moreInfoText += '\n' +
5169 mozL10n.get('error_stack', {stack: moreInfo.stack},
5170 'Stack: {{stack}}');
5171 } else {
5172 if (moreInfo.filename) {
5173 moreInfoText += '\n' +
5174 mozL10n.get('error_file', {file: moreInfo.filename},
5175 'File: {{file}}');
5176 }
5177 if (moreInfo.lineNumber) {
5178 moreInfoText += '\n' +
5179 mozL10n.get('error_line', {line: moreInfo.lineNumber},
5180 'Line: {{line}}');
5181 }
5182 }
5183 }
5184
5185 var errorWrapper = document.getElementById('errorWrapper');
5186 errorWrapper.removeAttribute('hidden');
5187
5188 var errorMessage = document.getElementById('errorMessage');
5189 errorMessage.textContent = message;
5190
5191 var closeButton = document.getElementById('errorClose');
5192 closeButton.onclick = function() {
5193 errorWrapper.setAttribute('hidden', 'true');
5194 };
5195
5196 var errorMoreInfo = document.getElementById('errorMoreInfo');
5197 var moreInfoButton = document.getElementById('errorShowMore');
5198 var lessInfoButton = document.getElementById('errorShowLess');
5199 moreInfoButton.onclick = function() {
5200 errorMoreInfo.removeAttribute('hidden');
5201 moreInfoButton.setAttribute('hidden', 'true');
5202 lessInfoButton.removeAttribute('hidden');
5203 errorMoreInfo.style.height = errorMoreInfo.scrollHeight + 'px';
5204 };
5205 lessInfoButton.onclick = function() {
5206 errorMoreInfo.setAttribute('hidden', 'true');
5207 moreInfoButton.removeAttribute('hidden');
5208 lessInfoButton.setAttribute('hidden', 'true');
5209 };
5210 moreInfoButton.oncontextmenu = noContextMenuHandler;
5211 lessInfoButton.oncontextmenu = noContextMenuHandler;
5212 closeButton.oncontextmenu = noContextMenuHandler;
5213 moreInfoButton.removeAttribute('hidden');
5214 lessInfoButton.setAttribute('hidden', 'true');
5215 errorMoreInfo.value = moreInfoText;
5216 },
5217
5218 progress: function pdfViewProgress(level) {
5219 var percent = Math.round(level * 100);
5220 // When we transition from full request to range requests, it's possible
5221 // that we discard some of the loaded data. This can cause the loading
5222 // bar to move backwards. So prevent this by only updating the bar if it
5223 // increases.
5224 if (percent > this.loadingBar.percent || isNaN(percent)) {
5225 this.loadingBar.percent = percent;
5226
5227 // When disableAutoFetch is enabled, it's not uncommon for the entire file
5228 // to never be fetched (depends on e.g. the file structure). In this case
5229 // the loading bar will not be completely filled, nor will it be hidden.
5230 // To prevent displaying a partially filled loading bar permanently, we
5231 // hide it when no data has been loaded during a certain amount of time.
5232 if (PDFJS.disableAutoFetch && percent) {
5233 if (this.disableAutoFetchLoadingBarTimeout) {
5234 clearTimeout(this.disableAutoFetchLoadingBarTimeout);
5235 this.disableAutoFetchLoadingBarTimeout = null;
5236 }
5237 this.loadingBar.show();
5238
5239 this.disableAutoFetchLoadingBarTimeout = setTimeout(function () {
5240 this.loadingBar.hide();
5241 this.disableAutoFetchLoadingBarTimeout = null;
5242 }.bind(this), DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT);
5243 }
5244 }
5245 },
5246
5247 load: function pdfViewLoad(pdfDocument, scale) {
5248 var self = this;
5249 scale = scale || UNKNOWN_SCALE;
5250
5251 this.findController.reset();
5252
5253 this.pdfDocument = pdfDocument;
5254
5255 DocumentProperties.url = this.url;
5256 DocumentProperties.pdfDocument = pdfDocument;
5257 DocumentProperties.resolveDataAvailable();
5258
5259 var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
5260 self.downloadComplete = true;
5261 self.loadingBar.hide();
5262 });
5263
5264 var pagesCount = pdfDocument.numPages;
5265 document.getElementById('numPages').textContent =
5266 mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
5267 document.getElementById('pageNumber').max = pagesCount;
5268
5269 var id = this.documentFingerprint = pdfDocument.fingerprint;
5270 var store = this.store = new ViewHistory(id);
5271
5272 var pdfViewer = this.pdfViewer;
5273 pdfViewer.currentScale = scale;
5274 pdfViewer.setDocument(pdfDocument);
5275 var firstPagePromise = pdfViewer.firstPagePromise;
5276 var pagesPromise = pdfViewer.pagesPromise;
5277 var onePageRendered = pdfViewer.onePageRendered;
5278
5279 this.pageRotation = 0;
5280 this.isInitialViewSet = false;
5281 this.pagesRefMap = pdfViewer.pagesRefMap;
5282
5283 this.pdfThumbnailViewer.setDocument(pdfDocument);
5284
5285 firstPagePromise.then(function(pdfPage) {
5286 downloadedPromise.then(function () {
5287 var event = document.createEvent('CustomEvent');
5288 event.initCustomEvent('documentload', true, true, {});
5289 window.dispatchEvent(event);
5290 });
5291
5292 self.loadingBar.setWidth(document.getElementById('viewer'));
5293
5294 self.findController.resolveFirstPage();
5295
5296 if (!PDFJS.disableHistory && !self.isViewerEmbedded) {
5297 // The browsing history is only enabled when the viewer is standalone,
5298 // i.e. not when it is embedded in a web page.
5299 PDFHistory.initialize(self.documentFingerprint, self);
5300 }
5301 });
5302
5303 // Fetch the necessary preference values.
5304 var showPreviousViewOnLoad;
5305 var showPreviousViewOnLoadPromise =
5306 Preferences.get('showPreviousViewOnLoad').then(function (prefValue) {
5307 showPreviousViewOnLoad = prefValue;
5308 });
5309 var defaultZoomValue;
5310 var defaultZoomValuePromise =
5311 Preferences.get('defaultZoomValue').then(function (prefValue) {
5312 defaultZoomValue = prefValue;
5313 });
5314
5315 var storePromise = store.initializedPromise;
5316 Promise.all([firstPagePromise, storePromise, showPreviousViewOnLoadPromise,
5317 defaultZoomValuePromise]).then(function resolved() {
5318 var storedHash = null;
5319 if (showPreviousViewOnLoad && store.get('exists', false)) {
5320 var pageNum = store.get('page', '1');
5321 var zoom = defaultZoomValue ||
5322 store.get('zoom', self.pdfViewer.currentScale);
5323 var left = store.get('scrollLeft', '0');
5324 var top = store.get('scrollTop', '0');
5325
5326 storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' +
5327 left + ',' + top;
5328 } else if (defaultZoomValue) {
5329 storedHash = 'page=1&zoom=' + defaultZoomValue;
5330 }
5331 self.setInitialView(storedHash, scale);
5332
5333 // Make all navigation keys work on document load,
5334 // unless the viewer is embedded in a web page.
5335 if (!self.isViewerEmbedded) {
5336 self.pdfViewer.focus();
5337 }
5338 }, function rejected(reason) {
5339 console.error(reason);
5340
5341 firstPagePromise.then(function () {
5342 self.setInitialView(null, scale);
5343 });
5344 });
5345
5346 pagesPromise.then(function() {
5347 if (self.supportsPrinting) {
5348 pdfDocument.getJavaScript().then(function(javaScript) {
5349 if (javaScript.length) {
5350 console.warn('Warning: JavaScript is not supported');
5351 self.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript);
5352 }
5353 // Hack to support auto printing.
5354 var regex = /\bprint\s*\(/g;
5355 for (var i = 0, ii = javaScript.length; i < ii; i++) {
5356 var js = javaScript[i];
5357 if (js && regex.test(js)) {
5358 setTimeout(function() {
5359 window.print();
5360 });
5361 return;
5362 }
5363 }
5364 });
5365 }
5366 });
5367
5368 // outline depends on pagesRefMap
5369 var promises = [pagesPromise, this.animationStartedPromise];
5370 Promise.all(promises).then(function() {
5371 pdfDocument.getOutline().then(function(outline) {
5372 var outlineView = document.getElementById('outlineView');
5373 self.outline = new DocumentOutlineView({
5374 outline: outline,
5375 outlineView: outlineView,
5376 linkService: self
5377 });
5378 document.getElementById('viewOutline').disabled = !outline;
5379
5380 if (!outline && !outlineView.classList.contains('hidden')) {
5381 self.switchSidebarView('thumbs');
5382 }
5383 if (outline &&
5384 self.preferenceSidebarViewOnLoad === SidebarView.OUTLINE) {
5385 self.switchSidebarView('outline', true);
5386 }
5387 });
5388 pdfDocument.getAttachments().then(function(attachments) {
5389 var attachmentsView = document.getElementById('attachmentsView');
5390 self.attachments = new DocumentAttachmentsView({
5391 attachments: attachments,
5392 attachmentsView: attachmentsView
5393 });
5394 document.getElementById('viewAttachments').disabled = !attachments;
5395
5396 if (!attachments && !attachmentsView.classList.contains('hidden')) {
5397 self.switchSidebarView('thumbs');
5398 }
5399 if (attachments &&
5400 self.preferenceSidebarViewOnLoad === SidebarView.ATTACHMENTS) {
5401 self.switchSidebarView('attachments', true);
5402 }
5403 });
5404 });
5405
5406 if (self.preferenceSidebarViewOnLoad === SidebarView.THUMBS) {
5407 Promise.all([firstPagePromise, onePageRendered]).then(function () {
5408 self.switchSidebarView('thumbs', true);
5409 });
5410 }
5411
5412 pdfDocument.getMetadata().then(function(data) {
5413 var info = data.info, metadata = data.metadata;
5414 self.documentInfo = info;
5415 self.metadata = metadata;
5416
5417 // Provides some basic debug information
5418 console.log('PDF ' + pdfDocument.fingerprint + ' [' +
5419 info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() +
5420 ' / ' + (info.Creator || '-').trim() + ']' +
5421 ' (PDF.js: ' + (PDFJS.version || '-') +
5422 (!PDFJS.disableWebGL ? ' [WebGL]' : '') + ')');
5423
5424 var pdfTitle;
5425 if (metadata && metadata.has('dc:title')) {
5426 var title = metadata.get('dc:title');
5427 // Ghostscript sometimes return 'Untitled', sets the title to 'Untitled'
5428 if (title !== 'Untitled') {
5429 pdfTitle = title;
5430 }
5431 }
5432
5433 if (!pdfTitle && info && info['Title']) {
5434 pdfTitle = info['Title'];
5435 }
5436
5437 if (pdfTitle) {
5438 self.setTitle(pdfTitle + ' - ' + document.title);
5439 }
5440
5441 if (info.IsAcroFormPresent) {
5442 console.warn('Warning: AcroForm/XFA is not supported');
5443 self.fallback(PDFJS.UNSUPPORTED_FEATURES.forms);
5444 }
5445
5446 });
5447 },
5448
5449 setInitialView: function pdfViewSetInitialView(storedHash, scale) {
5450 this.isInitialViewSet = true;
5451
5452 // When opening a new file (when one is already loaded in the viewer):
5453 // Reset 'currentPageNumber', since otherwise the page's scale will be wrong
5454 // if 'currentPageNumber' is larger than the number of pages in the file.
5455 document.getElementById('pageNumber').value =
5456 this.pdfViewer.currentPageNumber = 1;
5457
5458 if (PDFHistory.initialDestination) {
5459 this.navigateTo(PDFHistory.initialDestination);
5460 PDFHistory.initialDestination = null;
5461 } else if (this.initialBookmark) {
5462 this.setHash(this.initialBookmark);
5463 PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark);
5464 this.initialBookmark = null;
5465 } else if (storedHash) {
5466 this.setHash(storedHash);
5467 } else if (scale) {
5468 this.setScale(scale, true);
5469 this.page = 1;
5470 }
5471
5472 if (this.pdfViewer.currentScale === UNKNOWN_SCALE) {
5473 // Scale was not initialized: invalid bookmark or scale was not specified.
5474 // Setting the default one.
5475 this.setScale(DEFAULT_SCALE, true);
5476 }
5477 },
5478
5479 cleanup: function pdfViewCleanup() {
5480 this.pdfViewer.cleanup();
5481 this.pdfThumbnailViewer.cleanup();
5482 this.pdfDocument.cleanup();
5483 },
5484
5485 forceRendering: function pdfViewForceRendering() {
5486 this.pdfRenderingQueue.printing = this.printing;
5487 this.pdfRenderingQueue.isThumbnailViewEnabled = this.sidebarOpen;
5488 this.pdfRenderingQueue.renderHighestPriority();
5489 },
5490
5491 setHash: function pdfViewSetHash(hash) {
5492 if (!this.isInitialViewSet) {
5493 this.initialBookmark = hash;
5494 return;
5495 }
5496
5497 var validFitZoomValues = ['Fit','FitB','FitH','FitBH',
5498 'FitV','FitBV','FitR'];
5499
5500 if (!hash) {
5501 return;
5502 }
5503
5504 if (hash.indexOf('=') >= 0) {
5505 var params = this.parseQueryString(hash);
5506 // borrowing syntax from "Parameters for Opening PDF Files"
5507 if ('nameddest' in params) {
5508 PDFHistory.updateNextHashParam(params.nameddest);
5509 this.navigateTo(params.nameddest);
5510 return;
5511 }
5512 var pageNumber, dest;
5513 if ('page' in params) {
5514 pageNumber = (params.page | 0) || 1;
5515 }
5516 if ('zoom' in params) {
5517 var zoomArgs = params.zoom.split(','); // scale,left,top
5518 // building destination array
5519
5520 // If the zoom value, it has to get divided by 100. If it is a string,
5521 // it should stay as it is.
5522 var zoomArg = zoomArgs[0];
5523 var zoomArgNumber = parseFloat(zoomArg);
5524 var destName = 'XYZ';
5525 if (zoomArgNumber) {
5526 zoomArg = zoomArgNumber / 100;
5527 } else if (validFitZoomValues.indexOf(zoomArg) >= 0) {
5528 destName = zoomArg;
5529 }
5530 dest = [null, { name: destName },
5531 zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
5532 zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
5533 zoomArg];
5534 }
5535 if (dest) {
5536 this.pdfViewer.scrollPageIntoView(pageNumber || this.page, dest);
5537 } else if (pageNumber) {
5538 this.page = pageNumber; // simple page
5539 }
5540 if ('pagemode' in params) {
5541 if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' ||
5542 params.pagemode === 'attachments') {
5543 this.switchSidebarView((params.pagemode === 'bookmarks' ?
5544 'outline' : params.pagemode), true);
5545 } else if (params.pagemode === 'none' && this.sidebarOpen) {
5546 document.getElementById('sidebarToggle').click();
5547 }
5548 }
5549 } else if (/^\d+$/.test(hash)) { // page number
5550 this.page = hash;
5551 } else { // named destination
5552 PDFHistory.updateNextHashParam(unescape(hash));
5553 this.navigateTo(unescape(hash));
5554 }
5555 },
5556
5557 switchSidebarView: function pdfViewSwitchSidebarView(view, openSidebar) {
5558 if (openSidebar && !this.sidebarOpen) {
5559 document.getElementById('sidebarToggle').click();
5560 }
5561 var thumbsView = document.getElementById('thumbnailView');
5562 var outlineView = document.getElementById('outlineView');
5563 var attachmentsView = document.getElementById('attachmentsView');
5564
5565 var thumbsButton = document.getElementById('viewThumbnail');
5566 var outlineButton = document.getElementById('viewOutline');
5567 var attachmentsButton = document.getElementById('viewAttachments');
5568
5569 switch (view) {
5570 case 'thumbs':
5571 var wasAnotherViewVisible = thumbsView.classList.contains('hidden');
5572
5573 thumbsButton.classList.add('toggled');
5574 outlineButton.classList.remove('toggled');
5575 attachmentsButton.classList.remove('toggled');
5576 thumbsView.classList.remove('hidden');
5577 outlineView.classList.add('hidden');
5578 attachmentsView.classList.add('hidden');
5579
5580 this.forceRendering();
5581
5582 if (wasAnotherViewVisible) {
5583 this.pdfThumbnailViewer.ensureThumbnailVisible(this.page);
5584 }
5585 break;
5586
5587 case 'outline':
5588 thumbsButton.classList.remove('toggled');
5589 outlineButton.classList.add('toggled');
5590 attachmentsButton.classList.remove('toggled');
5591 thumbsView.classList.add('hidden');
5592 outlineView.classList.remove('hidden');
5593 attachmentsView.classList.add('hidden');
5594
5595 if (outlineButton.getAttribute('disabled')) {
5596 return;
5597 }
5598 break;
5599
5600 case 'attachments':
5601 thumbsButton.classList.remove('toggled');
5602 outlineButton.classList.remove('toggled');
5603 attachmentsButton.classList.add('toggled');
5604 thumbsView.classList.add('hidden');
5605 outlineView.classList.add('hidden');
5606 attachmentsView.classList.remove('hidden');
5607
5608 if (attachmentsButton.getAttribute('disabled')) {
5609 return;
5610 }
5611 break;
5612 }
5613 },
5614
5615 // Helper function to parse query string (e.g. ?param1=value&parm2=...).
5616 parseQueryString: function pdfViewParseQueryString(query) {
5617 var parts = query.split('&');
5618 var params = {};
5619 for (var i = 0, ii = parts.length; i < ii; ++i) {
5620 var param = parts[i].split('=');
5621 var key = param[0].toLowerCase();
5622 var value = param.length > 1 ? param[1] : null;
5623 params[decodeURIComponent(key)] = decodeURIComponent(value);
5624 }
5625 return params;
5626 },
5627
5628 beforePrint: function pdfViewSetupBeforePrint() {
5629 if (!this.supportsPrinting) {
5630 var printMessage = mozL10n.get('printing_not_supported', null,
5631 'Warning: Printing is not fully supported by this browser.');
5632 this.error(printMessage);
5633 return;
5634 }
5635
5636 var alertNotReady = false;
5637 var i, ii;
5638 if (!this.pagesCount) {
5639 alertNotReady = true;
5640 } else {
5641 for (i = 0, ii = this.pagesCount; i < ii; ++i) {
5642 if (!this.pdfViewer.getPageView(i).pdfPage) {
5643 alertNotReady = true;
5644 break;
5645 }
5646 }
5647 }
5648 if (alertNotReady) {
5649 var notReadyMessage = mozL10n.get('printing_not_ready', null,
5650 'Warning: The PDF is not fully loaded for printing.');
5651 window.alert(notReadyMessage);
5652 return;
5653 }
5654
5655 this.printing = true;
5656 this.forceRendering();
5657
5658 var body = document.querySelector('body');
5659 body.setAttribute('data-mozPrintCallback', true);
5660 for (i = 0, ii = this.pagesCount; i < ii; ++i) {
5661 this.pdfViewer.getPageView(i).beforePrint();
5662 }
5663
5664 },
5665
5666 afterPrint: function pdfViewSetupAfterPrint() {
5667 var div = document.getElementById('printContainer');
5668 while (div.hasChildNodes()) {
5669 div.removeChild(div.lastChild);
5670 }
5671
5672 this.printing = false;
5673 this.forceRendering();
5674 },
5675
5676 setScale: function (value, resetAutoSettings) {
5677 this.updateScaleControls = !!resetAutoSettings;
5678 this.pdfViewer.currentScaleValue = value;
5679 this.updateScaleControls = true;
5680 },
5681
5682 rotatePages: function pdfViewRotatePages(delta) {
5683 var pageNumber = this.page;
5684
5685 this.pageRotation = (this.pageRotation + 360 + delta) % 360;
5686 this.pdfViewer.pagesRotation = this.pageRotation;
5687 this.pdfThumbnailViewer.pagesRotation = this.pageRotation;
5688
5689 this.forceRendering();
5690
5691 this.pdfViewer.scrollPageIntoView(pageNumber);
5692 },
5693
5694 /**
5695 * This function flips the page in presentation mode if the user scrolls up
5696 * or down with large enough motion and prevents page flipping too often.
5697 *
5698 * @this {PDFView}
5699 * @param {number} mouseScrollDelta The delta value from the mouse event.
5700 */
5701 mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) {
5702 var MOUSE_SCROLL_COOLDOWN_TIME = 50;
5703
5704 var currentTime = (new Date()).getTime();
5705 var storedTime = this.mouseScrollTimeStamp;
5706
5707 // In case one page has already been flipped there is a cooldown time
5708 // which has to expire before next page can be scrolled on to.
5709 if (currentTime > storedTime &&
5710 currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) {
5711 return;
5712 }
5713
5714 // In case the user decides to scroll to the opposite direction than before
5715 // clear the accumulated delta.
5716 if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) ||
5717 (this.mouseScrollDelta < 0 && mouseScrollDelta > 0)) {
5718 this.clearMouseScrollState();
5719 }
5720
5721 this.mouseScrollDelta += mouseScrollDelta;
5722
5723 var PAGE_FLIP_THRESHOLD = 120;
5724 if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) {
5725
5726 var PageFlipDirection = {
5727 UP: -1,
5728 DOWN: 1
5729 };
5730
5731 // In presentation mode scroll one page at a time.
5732 var pageFlipDirection = (this.mouseScrollDelta > 0) ?
5733 PageFlipDirection.UP :
5734 PageFlipDirection.DOWN;
5735 this.clearMouseScrollState();
5736 var currentPage = this.page;
5737
5738 // In case we are already on the first or the last page there is no need
5739 // to do anything.
5740 if ((currentPage === 1 && pageFlipDirection === PageFlipDirection.UP) ||
5741 (currentPage === this.pagesCount &&
5742 pageFlipDirection === PageFlipDirection.DOWN)) {
5743 return;
5744 }
5745
5746 this.page += pageFlipDirection;
5747 this.mouseScrollTimeStamp = currentTime;
5748 }
5749 },
5750
5751 /**
5752 * This function clears the member attributes used with mouse scrolling in
5753 * presentation mode.
5754 *
5755 * @this {PDFView}
5756 */
5757 clearMouseScrollState: function pdfViewClearMouseScrollState() {
5758 this.mouseScrollTimeStamp = 0;
5759 this.mouseScrollDelta = 0;
5760 }
5761};
5762window.PDFView = PDFViewerApplication; // obsolete name, using it as an alias
5763
5764
5765var THUMBNAIL_SCROLL_MARGIN = -19;
5766
5767/**
5768 * @constructor
5769 * @param container
5770 * @param id
5771 * @param defaultViewport
5772 * @param linkService
5773 * @param renderingQueue
5774 * @param pageSource
5775 *
5776 * @implements {IRenderableView}
5777 */
5778var ThumbnailView = function thumbnailView(container, id, defaultViewport,
5779 linkService, renderingQueue,
5780 pageSource) {
5781 var anchor = document.createElement('a');
5782 anchor.href = linkService.getAnchorUrl('#page=' + id);
5783 anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
5784 anchor.onclick = function stopNavigation() {
5785 linkService.page = id;
5786 return false;
5787 };
5788
5789 this.pdfPage = undefined;
5790 this.viewport = defaultViewport;
5791 this.pdfPageRotate = defaultViewport.rotation;
5792
5793 this.rotation = 0;
5794 this.pageWidth = this.viewport.width;
5795 this.pageHeight = this.viewport.height;
5796 this.pageRatio = this.pageWidth / this.pageHeight;
5797 this.id = id;
5798 this.renderingId = 'thumbnail' + id;
5799
5800 this.canvasWidth = 98;
5801 this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
5802 this.scale = (this.canvasWidth / this.pageWidth);
5803
5804 var div = this.el = document.createElement('div');
5805 div.id = 'thumbnailContainer' + id;
5806 div.className = 'thumbnail';
5807
5808 if (id === 1) {
5809 // Highlight the thumbnail of the first page when no page number is
5810 // specified (or exists in cache) when the document is loaded.
5811 div.classList.add('selected');
5812 }
5813
5814 var ring = document.createElement('div');
5815 ring.className = 'thumbnailSelectionRing';
5816 ring.style.width = this.canvasWidth + 'px';
5817 ring.style.height = this.canvasHeight + 'px';
5818
5819 div.appendChild(ring);
5820 anchor.appendChild(div);
5821 container.appendChild(anchor);
5822
5823 this.hasImage = false;
5824 this.renderingState = RenderingStates.INITIAL;
5825 this.renderingQueue = renderingQueue;
5826 this.pageSource = pageSource;
5827
5828 this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) {
5829 this.pdfPage = pdfPage;
5830 this.pdfPageRotate = pdfPage.rotate;
5831 var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
5832 this.viewport = pdfPage.getViewport(1, totalRotation);
5833 this.update();
5834 };
5835
5836 this.update = function thumbnailViewUpdate(rotation) {
5837 if (rotation !== undefined) {
5838 this.rotation = rotation;
5839 }
5840 var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
5841 this.viewport = this.viewport.clone({
5842 scale: 1,
5843 rotation: totalRotation
5844 });
5845 this.pageWidth = this.viewport.width;
5846 this.pageHeight = this.viewport.height;
5847 this.pageRatio = this.pageWidth / this.pageHeight;
5848
5849 this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
5850 this.scale = (this.canvasWidth / this.pageWidth);
5851
5852 div.removeAttribute('data-loaded');
5853 ring.textContent = '';
5854 ring.style.width = this.canvasWidth + 'px';
5855 ring.style.height = this.canvasHeight + 'px';
5856
5857 this.hasImage = false;
5858 this.renderingState = RenderingStates.INITIAL;
5859 this.resume = null;
5860 };
5861
5862 this.getPageDrawContext = function thumbnailViewGetPageDrawContext() {
5863 var canvas = document.createElement('canvas');
5864 canvas.id = 'thumbnail' + id;
5865
5866 canvas.width = this.canvasWidth;
5867 canvas.height = this.canvasHeight;
5868 canvas.className = 'thumbnailImage';
5869 canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
5870 {page: id}, 'Thumbnail of Page {{page}}'));
5871
5872 div.setAttribute('data-loaded', true);
5873
5874 ring.appendChild(canvas);
5875
5876 var ctx = canvas.getContext('2d');
5877 ctx.save();
5878 ctx.fillStyle = 'rgb(255, 255, 255)';
5879 ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
5880 ctx.restore();
5881 return ctx;
5882 };
5883
5884 this.drawingRequired = function thumbnailViewDrawingRequired() {
5885 return !this.hasImage;
5886 };
5887
5888 this.draw = function thumbnailViewDraw(callback) {
5889 if (!this.pdfPage) {
5890 var promise = this.pageSource.getPage(this.id);
5891 promise.then(function(pdfPage) {
5892 this.setPdfPage(pdfPage);
5893 this.draw(callback);
5894 }.bind(this));
5895 return;
5896 }
5897
5898 if (this.renderingState !== RenderingStates.INITIAL) {
5899 console.error('Must be in new state before drawing');
5900 }
5901
5902 this.renderingState = RenderingStates.RUNNING;
5903 if (this.hasImage) {
5904 callback();
5905 return;
5906 }
5907
5908 var self = this;
5909 var ctx = this.getPageDrawContext();
5910 var drawViewport = this.viewport.clone({ scale: this.scale });
5911 var renderContext = {
5912 canvasContext: ctx,
5913 viewport: drawViewport,
5914 continueCallback: function(cont) {
5915 if (!self.renderingQueue.isHighestPriority(self)) {
5916 self.renderingState = RenderingStates.PAUSED;
5917 self.resume = function() {
5918 self.renderingState = RenderingStates.RUNNING;
5919 cont();
5920 };
5921 return;
5922 }
5923 cont();
5924 }
5925 };
5926 this.pdfPage.render(renderContext).promise.then(
5927 function pdfPageRenderCallback() {
5928 self.renderingState = RenderingStates.FINISHED;
5929 callback();
5930 },
5931 function pdfPageRenderError(error) {
5932 self.renderingState = RenderingStates.FINISHED;
5933 callback();
5934 }
5935 );
5936 this.hasImage = true;
5937 };
5938
5939 function getTempCanvas(width, height) {
5940 var tempCanvas = ThumbnailView.tempImageCache;
5941 if (!tempCanvas) {
5942 tempCanvas = document.createElement('canvas');
5943 ThumbnailView.tempImageCache = tempCanvas;
5944 }
5945 tempCanvas.width = width;
5946 tempCanvas.height = height;
5947 return tempCanvas;
5948 }
5949
5950 this.setImage = function thumbnailViewSetImage(img) {
5951 if (!this.pdfPage) {
5952 var promise = this.pageSource.getPage();
5953 promise.then(function(pdfPage) {
5954 this.setPdfPage(pdfPage);
5955 this.setImage(img);
5956 }.bind(this));
5957 return;
5958 }
5959 if (this.hasImage || !img) {
5960 return;
5961 }
5962 this.renderingState = RenderingStates.FINISHED;
5963 var ctx = this.getPageDrawContext();
5964
5965 var reducedImage = img;
5966 var reducedWidth = img.width;
5967 var reducedHeight = img.height;
5968
5969 // drawImage does an awful job of rescaling the image, doing it gradually
5970 var MAX_SCALE_FACTOR = 2.0;
5971 if (Math.max(img.width / ctx.canvas.width,
5972 img.height / ctx.canvas.height) > MAX_SCALE_FACTOR) {
5973 reducedWidth >>= 1;
5974 reducedHeight >>= 1;
5975 reducedImage = getTempCanvas(reducedWidth, reducedHeight);
5976 var reducedImageCtx = reducedImage.getContext('2d');
5977 reducedImageCtx.drawImage(img, 0, 0, img.width, img.height,
5978 0, 0, reducedWidth, reducedHeight);
5979 while (Math.max(reducedWidth / ctx.canvas.width,
5980 reducedHeight / ctx.canvas.height) > MAX_SCALE_FACTOR) {
5981 reducedImageCtx.drawImage(reducedImage,
5982 0, 0, reducedWidth, reducedHeight,
5983 0, 0, reducedWidth >> 1, reducedHeight >> 1);
5984 reducedWidth >>= 1;
5985 reducedHeight >>= 1;
5986 }
5987 }
5988
5989 ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight,
5990 0, 0, ctx.canvas.width, ctx.canvas.height);
5991
5992 this.hasImage = true;
5993 };
5994};
5995
5996ThumbnailView.tempImageCache = null;
5997
5998/**
5999 * @typedef {Object} PDFThumbnailViewerOptions
6000 * @property {HTMLDivElement} container - The container for the thumbs elements.
6001 * @property {IPDFLinkService} linkService - The navigation/linking service.
6002 * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
6003 */
6004
6005/**
6006 * Simple viewer control to display thumbs for pages.
6007 * @class
6008 */
6009var PDFThumbnailViewer = (function pdfThumbnailViewer() {
6010 /**
6011 * @constructs
6012 * @param {PDFThumbnailViewerOptions} options
6013 */
6014 function PDFThumbnailViewer(options) {
6015 this.container = options.container;
6016 this.renderingQueue = options.renderingQueue;
6017 this.linkService = options.linkService;
6018
6019 this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this));
6020 this._resetView();
6021 }
6022
6023 PDFThumbnailViewer.prototype = {
6024 _scrollUpdated: function PDFThumbnailViewer_scrollUpdated() {
6025 this.renderingQueue.renderHighestPriority();
6026 },
6027
6028 getThumbnail: function PDFThumbnailViewer_getThumbnail(index) {
6029 return this.thumbnails[index];
6030 },
6031
6032 _getVisibleThumbs: function PDFThumbnailViewer_getVisibleThumbs() {
6033 return getVisibleElements(this.container, this.thumbnails);
6034 },
6035
6036 scrollThumbnailIntoView: function (page) {
6037 var selected = document.querySelector('.thumbnail.selected');
6038 if (selected) {
6039 selected.classList.remove('selected');
6040 }
6041 var thumbnail = document.getElementById('thumbnailContainer' + page);
6042 thumbnail.classList.add('selected');
6043 var visibleThumbs = this._getVisibleThumbs();
6044 var numVisibleThumbs = visibleThumbs.views.length;
6045
6046 // If the thumbnail isn't currently visible, scroll it into view.
6047 if (numVisibleThumbs > 0) {
6048 var first = visibleThumbs.first.id;
6049 // Account for only one thumbnail being visible.
6050 var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first);
6051 if (page <= first || page >= last) {
6052 scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN });
6053 }
6054 }
6055 },
6056
6057 get pagesRotation() {
6058 return this._pagesRotation;
6059 },
6060
6061 set pagesRotation(rotation) {
6062 this._pagesRotation = rotation;
6063 for (var i = 0, l = this.thumbnails.length; i < l; i++) {
6064 var thumb = this.thumbnails[i];
6065 thumb.update(rotation);
6066 }
6067 },
6068
6069 cleanup: function PDFThumbnailViewer_cleanup() {
6070 ThumbnailView.tempImageCache = null;
6071 },
6072
6073 _resetView: function () {
6074 this.thumbnails = [];
6075 this._pagesRotation = 0;
6076 },
6077
6078 setDocument: function (pdfDocument) {
6079 if (this.pdfDocument) {
6080 // cleanup of the elements and views
6081 var thumbsView = this.container;
6082 while (thumbsView.hasChildNodes()) {
6083 thumbsView.removeChild(thumbsView.lastChild);
6084 }
6085 this._resetView();
6086 }
6087
6088 this.pdfDocument = pdfDocument;
6089 if (!pdfDocument) {
6090 return Promise.resolve();
6091 }
6092
6093 return pdfDocument.getPage(1).then(function (firstPage) {
6094 var pagesCount = pdfDocument.numPages;
6095 var viewport = firstPage.getViewport(1.0);
6096 for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
6097 var pageSource = new PDFPageSource(pdfDocument, pageNum);
6098 var thumbnail = new ThumbnailView(this.container, pageNum,
6099 viewport.clone(), this.linkService,
6100 this.renderingQueue, pageSource);
6101 this.thumbnails.push(thumbnail);
6102 }
6103 }.bind(this));
6104 },
6105
6106 ensureThumbnailVisible:
6107 function PDFThumbnailViewer_ensureThumbnailVisible(page) {
6108 // Ensure that the thumbnail of the current page is visible
6109 // when switching from another view.
6110 scrollIntoView(document.getElementById('thumbnailContainer' + page));
6111 },
6112
6113 forceRendering: function () {
6114 var visibleThumbs = this._getVisibleThumbs();
6115 var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs,
6116 this.thumbnails,
6117 this.scroll.down);
6118 if (thumbView) {
6119 this.renderingQueue.renderView(thumbView);
6120 return true;
6121 }
6122 return false;
6123 }
6124 };
6125
6126 return PDFThumbnailViewer;
6127})();
6128
6129
6130var DocumentOutlineView = function documentOutlineView(options) {
6131 var outline = options.outline;
6132 var outlineView = options.outlineView;
6133 while (outlineView.firstChild) {
6134 outlineView.removeChild(outlineView.firstChild);
6135 }
6136
6137 if (!outline) {
6138 return;
6139 }
6140
6141 var linkService = options.linkService;
6142
6143 function bindItemLink(domObj, item) {
6144 domObj.href = linkService.getDestinationHash(item.dest);
6145 domObj.onclick = function documentOutlineViewOnclick(e) {
6146 linkService.navigateTo(item.dest);
6147 return false;
6148 };
6149 }
6150
6151 var queue = [{parent: outlineView, items: outline}];
6152 while (queue.length > 0) {
6153 var levelData = queue.shift();
6154 var i, n = levelData.items.length;
6155 for (i = 0; i < n; i++) {
6156 var item = levelData.items[i];
6157 var div = document.createElement('div');
6158 div.className = 'outlineItem';
6159 var a = document.createElement('a');
6160 bindItemLink(a, item);
6161 a.textContent = item.title;
6162 div.appendChild(a);
6163
6164 if (item.items.length > 0) {
6165 var itemsDiv = document.createElement('div');
6166 itemsDiv.className = 'outlineItems';
6167 div.appendChild(itemsDiv);
6168 queue.push({parent: itemsDiv, items: item.items});
6169 }
6170
6171 levelData.parent.appendChild(div);
6172 }
6173 }
6174};
6175
6176
6177var DocumentAttachmentsView = function documentAttachmentsView(options) {
6178 var attachments = options.attachments;
6179 var attachmentsView = options.attachmentsView;
6180 while (attachmentsView.firstChild) {
6181 attachmentsView.removeChild(attachmentsView.firstChild);
6182 }
6183
6184 if (!attachments) {
6185 return;
6186 }
6187
6188 function bindItemLink(domObj, item) {
6189 domObj.onclick = function documentAttachmentsViewOnclick(e) {
6190 var downloadManager = new DownloadManager();
6191 downloadManager.downloadData(item.content, getFileName(item.filename),
6192 '');
6193 return false;
6194 };
6195 }
6196
6197 var names = Object.keys(attachments).sort(function(a,b) {
6198 return a.toLowerCase().localeCompare(b.toLowerCase());
6199 });
6200 for (var i = 0, ii = names.length; i < ii; i++) {
6201 var item = attachments[names[i]];
6202 var div = document.createElement('div');
6203 div.className = 'attachmentsItem';
6204 var button = document.createElement('button');
6205 bindItemLink(button, item);
6206 button.textContent = getFileName(item.filename);
6207 div.appendChild(button);
6208 attachmentsView.appendChild(div);
6209 }
6210};
6211
6212
6213
6214function webViewerLoad(evt) {
6215 PDFViewerApplication.initialize().then(webViewerInitialized);
6216}
6217
6218function webViewerInitialized() {
6219 var queryString = document.location.search.substring(1);
6220 var params = PDFViewerApplication.parseQueryString(queryString);
6221 var file = 'file' in params ? params.file : DEFAULT_URL;
6222
6223 var fileInput = document.createElement('input');
6224 fileInput.id = 'fileInput';
6225 fileInput.className = 'fileInput';
6226 fileInput.setAttribute('type', 'file');
6227 fileInput.oncontextmenu = noContextMenuHandler;
6228 document.body.appendChild(fileInput);
6229
6230 if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
6231 document.getElementById('openFile').setAttribute('hidden', 'true');
6232 document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true');
6233 } else {
6234 document.getElementById('fileInput').value = null;
6235 }
6236
6237 var locale = PDFJS.locale || navigator.language;
6238
6239 if (PDFViewerApplication.preferencePdfBugEnabled) {
6240 // Special debugging flags in the hash section of the URL.
6241 var hash = document.location.hash.substring(1);
6242 var hashParams = PDFViewerApplication.parseQueryString(hash);
6243
6244 if ('disableworker' in hashParams) {
6245 PDFJS.disableWorker = (hashParams['disableworker'] === 'true');
6246 }
6247 if ('disablerange' in hashParams) {
6248 PDFJS.disableRange = (hashParams['disablerange'] === 'true');
6249 }
6250 if ('disablestream' in hashParams) {
6251 PDFJS.disableStream = (hashParams['disablestream'] === 'true');
6252 }
6253 if ('disableautofetch' in hashParams) {
6254 PDFJS.disableAutoFetch = (hashParams['disableautofetch'] === 'true');
6255 }
6256 if ('disablefontface' in hashParams) {
6257 PDFJS.disableFontFace = (hashParams['disablefontface'] === 'true');
6258 }
6259 if ('disablehistory' in hashParams) {
6260 PDFJS.disableHistory = (hashParams['disablehistory'] === 'true');
6261 }
6262 if ('webgl' in hashParams) {
6263 PDFJS.disableWebGL = (hashParams['webgl'] !== 'true');
6264 }
6265 if ('useonlycsszoom' in hashParams) {
6266 PDFJS.useOnlyCssZoom = (hashParams['useonlycsszoom'] === 'true');
6267 }
6268 if ('verbosity' in hashParams) {
6269 PDFJS.verbosity = hashParams['verbosity'] | 0;
6270 }
6271 if ('ignorecurrentpositiononzoom' in hashParams) {
6272 IGNORE_CURRENT_POSITION_ON_ZOOM =
6273 (hashParams['ignorecurrentpositiononzoom'] === 'true');
6274 }
6275 if ('locale' in hashParams) {
6276 locale = hashParams['locale'];
6277 }
6278 if ('textlayer' in hashParams) {
6279 switch (hashParams['textlayer']) {
6280 case 'off':
6281 PDFJS.disableTextLayer = true;
6282 break;
6283 case 'visible':
6284 case 'shadow':
6285 case 'hover':
6286 var viewer = document.getElementById('viewer');
6287 viewer.classList.add('textLayer-' + hashParams['textlayer']);
6288 break;
6289 }
6290 }
6291 if ('pdfbug' in hashParams) {
6292 PDFJS.pdfBug = true;
6293 var pdfBug = hashParams['pdfbug'];
6294 var enabled = pdfBug.split(',');
6295 PDFBug.enable(enabled);
6296 PDFBug.init();
6297 }
6298 }
6299
6300 mozL10n.setLanguage(locale);
6301
6302 if (!PDFViewerApplication.supportsPrinting) {
6303 document.getElementById('print').classList.add('hidden');
6304 document.getElementById('secondaryPrint').classList.add('hidden');
6305 }
6306
6307 if (!PDFViewerApplication.supportsFullscreen) {
6308 document.getElementById('presentationMode').classList.add('hidden');
6309 document.getElementById('secondaryPresentationMode').
6310 classList.add('hidden');
6311 }
6312
6313 if (PDFViewerApplication.supportsIntegratedFind) {
6314 document.getElementById('viewFind').classList.add('hidden');
6315 }
6316
6317 // Listen for unsupported features to trigger the fallback UI.
6318 PDFJS.UnsupportedManager.listen(
6319 PDFViewerApplication.fallback.bind(PDFViewerApplication));
6320
6321 // Suppress context menus for some controls
6322 document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler;
6323
6324 var mainContainer = document.getElementById('mainContainer');
6325 var outerContainer = document.getElementById('outerContainer');
6326 mainContainer.addEventListener('transitionend', function(e) {
6327 if (e.target === mainContainer) {
6328 var event = document.createEvent('UIEvents');
6329 event.initUIEvent('resize', false, false, window, 0);
6330 window.dispatchEvent(event);
6331 outerContainer.classList.remove('sidebarMoving');
6332 }
6333 }, true);
6334
6335 document.getElementById('sidebarToggle').addEventListener('click',
6336 function() {
6337 this.classList.toggle('toggled');
6338 outerContainer.classList.add('sidebarMoving');
6339 outerContainer.classList.toggle('sidebarOpen');
6340 PDFViewerApplication.sidebarOpen =
6341 outerContainer.classList.contains('sidebarOpen');
6342 PDFViewerApplication.forceRendering();
6343 });
6344
6345 document.getElementById('viewThumbnail').addEventListener('click',
6346 function() {
6347 PDFViewerApplication.switchSidebarView('thumbs');
6348 });
6349
6350 document.getElementById('viewOutline').addEventListener('click',
6351 function() {
6352 PDFViewerApplication.switchSidebarView('outline');
6353 });
6354
6355 document.getElementById('viewAttachments').addEventListener('click',
6356 function() {
6357 PDFViewerApplication.switchSidebarView('attachments');
6358 });
6359
6360 document.getElementById('previous').addEventListener('click',
6361 function() {
6362 PDFViewerApplication.page--;
6363 });
6364
6365 document.getElementById('next').addEventListener('click',
6366 function() {
6367 PDFViewerApplication.page++;
6368 });
6369
6370 document.getElementById('zoomIn').addEventListener('click',
6371 function() {
6372 PDFViewerApplication.zoomIn();
6373 });
6374
6375 document.getElementById('zoomOut').addEventListener('click',
6376 function() {
6377 PDFViewerApplication.zoomOut();
6378 });
6379
6380 document.getElementById('pageNumber').addEventListener('click', function() {
6381 this.select();
6382 });
6383
6384 document.getElementById('pageNumber').addEventListener('change', function() {
6385 // Handle the user inputting a floating point number.
6386 PDFViewerApplication.page = (this.value | 0);
6387
6388 if (this.value !== (this.value | 0).toString()) {
6389 this.value = PDFViewerApplication.page;
6390 }
6391 });
6392
6393 document.getElementById('scaleSelect').addEventListener('change',
6394 function() {
6395 PDFViewerApplication.setScale(this.value, false);
6396 });
6397
6398 document.getElementById('presentationMode').addEventListener('click',
6399 SecondaryToolbar.presentationModeClick.bind(SecondaryToolbar));
6400
6401 document.getElementById('openFile').addEventListener('click',
6402 SecondaryToolbar.openFileClick.bind(SecondaryToolbar));
6403
6404 document.getElementById('print').addEventListener('click',
6405 SecondaryToolbar.printClick.bind(SecondaryToolbar));
6406
6407 document.getElementById('download').addEventListener('click',
6408 SecondaryToolbar.downloadClick.bind(SecondaryToolbar));
6409
6410
6411 if (file && file.lastIndexOf('file:', 0) === 0) {
6412 // file:-scheme. Load the contents in the main thread because QtWebKit
6413 // cannot load file:-URLs in a Web Worker. file:-URLs are usually loaded
6414 // very quickly, so there is no need to set up progress event listeners.
6415 PDFViewerApplication.setTitleUsingUrl(file);
6416 var xhr = new XMLHttpRequest();
6417 xhr.onload = function() {
6418 PDFViewerApplication.open(new Uint8Array(xhr.response), 0);
6419 };
6420 try {
6421 xhr.open('GET', file);
6422 xhr.responseType = 'arraybuffer';
6423 xhr.send();
6424 } catch (e) {
6425 PDFViewerApplication.error(mozL10n.get('loading_error', null,
6426 'An error occurred while loading the PDF.'), e);
6427 }
6428 return;
6429 }
6430
6431 if (file) {
6432 PDFViewerApplication.open(file, 0);
6433 }
6434}
6435
6436document.addEventListener('DOMContentLoaded', webViewerLoad, true);
6437
6438document.addEventListener('pagerendered', function (e) {
6439 var pageIndex = e.detail.pageNumber - 1;
6440 var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex);
6441 var thumbnailView = PDFViewerApplication.pdfThumbnailViewer.
6442 getThumbnail(pageIndex);
6443 thumbnailView.setImage(pageView.canvas);
6444
6445
6446 if (pageView.error) {
6447 PDFViewerApplication.error(mozL10n.get('rendering_error', null,
6448 'An error occurred while rendering the page.'), pageView.error);
6449 }
6450
6451
6452 // If the page is still visible when it has finished rendering,
6453 // ensure that the page number input loading indicator is hidden.
6454 if ((pageIndex + 1) === PDFViewerApplication.page) {
6455 var pageNumberInput = document.getElementById('pageNumber');
6456 pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
6457 }
6458}, true);
6459
6460window.addEventListener('presentationmodechanged', function (e) {
6461 var active = e.detail.active;
6462 var switchInProgress = e.detail.switchInProgress;
6463 PDFViewerApplication.pdfViewer.presentationModeState =
6464 switchInProgress ? PresentationModeState.CHANGING :
6465 active ? PresentationModeState.FULLSCREEN : PresentationModeState.NORMAL;
6466});
6467
6468function updateViewarea() {
6469 if (!PDFViewerApplication.initialized) {
6470 return;
6471 }
6472 PDFViewerApplication.pdfViewer.update();
6473}
6474
6475window.addEventListener('updateviewarea', function () {
6476 if (!PDFViewerApplication.initialized) {
6477 return;
6478 }
6479
6480 var location = PDFViewerApplication.pdfViewer.location;
6481
6482 PDFViewerApplication.store.initializedPromise.then(function() {
6483 PDFViewerApplication.store.setMultiple({
6484 'exists': true,
6485 'page': location.pageNumber,
6486 'zoom': location.scale,
6487 'scrollLeft': location.left,
6488 'scrollTop': location.top
6489 }).catch(function() {
6490 // unable to write to storage
6491 });
6492 });
6493 var href = PDFViewerApplication.getAnchorUrl(location.pdfOpenParams);
6494 document.getElementById('viewBookmark').href = href;
6495 document.getElementById('secondaryViewBookmark').href = href;
6496
6497 // Update the current bookmark in the browsing history.
6498 PDFHistory.updateCurrentBookmark(location.pdfOpenParams, location.pageNumber);
6499
6500 // Show/hide the loading indicator in the page number input element.
6501 var pageNumberInput = document.getElementById('pageNumber');
6502 var currentPage =
6503 PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1);
6504
6505 if (currentPage.renderingState === RenderingStates.FINISHED) {
6506 pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
6507 } else {
6508 pageNumberInput.classList.add(PAGE_NUMBER_LOADING_INDICATOR);
6509 }
6510}, true);
6511
6512window.addEventListener('resize', function webViewerResize(evt) {
6513 if (PDFViewerApplication.initialized &&
6514 (document.getElementById('pageWidthOption').selected ||
6515 document.getElementById('pageFitOption').selected ||
6516 document.getElementById('pageAutoOption').selected)) {
6517 var selectedScale = document.getElementById('scaleSelect').value;
6518 PDFViewerApplication.setScale(selectedScale, false);
6519 }
6520 updateViewarea();
6521
6522 // Set the 'max-height' CSS property of the secondary toolbar.
6523 SecondaryToolbar.setMaxHeight(document.getElementById('viewerContainer'));
6524});
6525
6526window.addEventListener('hashchange', function webViewerHashchange(evt) {
6527 if (PDFHistory.isHashChangeUnlocked) {
6528 PDFViewerApplication.setHash(document.location.hash.substring(1));
6529 }
6530});
6531
6532window.addEventListener('change', function webViewerChange(evt) {
6533 var files = evt.target.files;
6534 if (!files || files.length === 0) {
6535 return;
6536 }
6537 var file = files[0];
6538
6539 if (!PDFJS.disableCreateObjectURL &&
6540 typeof URL !== 'undefined' && URL.createObjectURL) {
6541 PDFViewerApplication.open(URL.createObjectURL(file), 0);
6542 } else {
6543 // Read the local file into a Uint8Array.
6544 var fileReader = new FileReader();
6545 fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
6546 var buffer = evt.target.result;
6547 var uint8Array = new Uint8Array(buffer);
6548 PDFViewerApplication.open(uint8Array, 0);
6549 };
6550 fileReader.readAsArrayBuffer(file);
6551 }
6552
6553 PDFViewerApplication.setTitleUsingUrl(file.name);
6554
6555 // URL does not reflect proper document location - hiding some icons.
6556 document.getElementById('viewBookmark').setAttribute('hidden', 'true');
6557 document.getElementById('secondaryViewBookmark').
6558 setAttribute('hidden', 'true');
6559 document.getElementById('download').setAttribute('hidden', 'true');
6560 document.getElementById('secondaryDownload').setAttribute('hidden', 'true');
6561}, true);
6562
6563function selectScaleOption(value) {
6564 var options = document.getElementById('scaleSelect').options;
6565 var predefinedValueFound = false;
6566 for (var i = 0; i < options.length; i++) {
6567 var option = options[i];
6568 if (option.value !== value) {
6569 option.selected = false;
6570 continue;
6571 }
6572 option.selected = true;
6573 predefinedValueFound = true;
6574 }
6575 return predefinedValueFound;
6576}
6577
6578window.addEventListener('localized', function localized(evt) {
6579 document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
6580
6581 PDFViewerApplication.animationStartedPromise.then(function() {
6582 // Adjust the width of the zoom box to fit the content.
6583 // Note: If the window is narrow enough that the zoom box is not visible,
6584 // we temporarily show it to be able to adjust its width.
6585 var container = document.getElementById('scaleSelectContainer');
6586 if (container.clientWidth === 0) {
6587 container.setAttribute('style', 'display: inherit;');
6588 }
6589 if (container.clientWidth > 0) {
6590 var select = document.getElementById('scaleSelect');
6591 select.setAttribute('style', 'min-width: inherit;');
6592 var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING;
6593 select.setAttribute('style', 'min-width: ' +
6594 (width + SCALE_SELECT_PADDING) + 'px;');
6595 container.setAttribute('style', 'min-width: ' + width + 'px; ' +
6596 'max-width: ' + width + 'px;');
6597 }
6598
6599 // Set the 'max-height' CSS property of the secondary toolbar.
6600 SecondaryToolbar.setMaxHeight(document.getElementById('viewerContainer'));
6601 });
6602}, true);
6603
6604window.addEventListener('scalechange', function scalechange(evt) {
6605 document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE);
6606 document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE);
6607
6608 var customScaleOption = document.getElementById('customScaleOption');
6609 customScaleOption.selected = false;
6610
6611 if (!PDFViewerApplication.updateScaleControls &&
6612 (document.getElementById('pageWidthOption').selected ||
6613 document.getElementById('pageFitOption').selected ||
6614 document.getElementById('pageAutoOption').selected)) {
6615 updateViewarea();
6616 return;
6617 }
6618
6619 if (evt.presetValue) {
6620 selectScaleOption(evt.presetValue);
6621 updateViewarea();
6622 return;
6623 }
6624
6625 var predefinedValueFound = selectScaleOption('' + evt.scale);
6626 if (!predefinedValueFound) {
6627 var customScale = Math.round(evt.scale * 10000) / 100;
6628 customScaleOption.textContent =
6629 mozL10n.get('page_scale_percent', { scale: customScale }, '{{scale}}%');
6630 customScaleOption.selected = true;
6631 }
6632 updateViewarea();
6633}, true);
6634
6635window.addEventListener('pagechange', function pagechange(evt) {
6636 var page = evt.pageNumber;
6637 if (evt.previousPageNumber !== page) {
6638 document.getElementById('pageNumber').value = page;
6639 PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
6640 }
6641 var numPages = PDFViewerApplication.pagesCount;
6642
6643 document.getElementById('previous').disabled = (page <= 1);
6644 document.getElementById('next').disabled = (page >= numPages);
6645
6646 document.getElementById('firstPage').disabled = (page <= 1);
6647 document.getElementById('lastPage').disabled = (page >= numPages);
6648
6649 // checking if the this.page was called from the updateViewarea function
6650 if (evt.updateInProgress) {
6651 return;
6652 }
6653 // Avoid scrolling the first page during loading
6654 if (this.loading && page === 1) {
6655 return;
6656 }
6657 PDFViewerApplication.pdfViewer.scrollPageIntoView(page);
6658}, true);
6659
6660function handleMouseWheel(evt) {
6661 var MOUSE_WHEEL_DELTA_FACTOR = 40;
6662 var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail :
6663 evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR;
6664 var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn';
6665
6666 if (PresentationMode.active) {
6667 evt.preventDefault();
6668 PDFViewerApplication.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR);
6669 } else if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer
6670 evt.preventDefault();
6671 PDFViewerApplication[direction](Math.abs(ticks));
6672 }
6673}
6674
6675window.addEventListener('DOMMouseScroll', handleMouseWheel);
6676window.addEventListener('mousewheel', handleMouseWheel);
6677
6678window.addEventListener('click', function click(evt) {
6679 if (!PresentationMode.active) {
6680 if (SecondaryToolbar.opened &&
6681 PDFViewerApplication.pdfViewer.containsElement(evt.target)) {
6682 SecondaryToolbar.close();
6683 }
6684 } else if (evt.button === 0) {
6685 // Necessary since preventDefault() in 'mousedown' won't stop
6686 // the event propagation in all circumstances in presentation mode.
6687 evt.preventDefault();
6688 }
6689}, false);
6690
6691window.addEventListener('keydown', function keydown(evt) {
6692 if (OverlayManager.active) {
6693 return;
6694 }
6695
6696 var handled = false;
6697 var cmd = (evt.ctrlKey ? 1 : 0) |
6698 (evt.altKey ? 2 : 0) |
6699 (evt.shiftKey ? 4 : 0) |
6700 (evt.metaKey ? 8 : 0);
6701
6702 // First, handle the key bindings that are independent whether an input
6703 // control is selected or not.
6704 if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
6705 // either CTRL or META key with optional SHIFT.
6706 var pdfViewer = PDFViewerApplication.pdfViewer;
6707 var inPresentationMode = pdfViewer &&
6708 (pdfViewer.presentationModeState === PresentationModeState.CHANGING ||
6709 pdfViewer.presentationModeState === PresentationModeState.FULLSCREEN);
6710
6711 switch (evt.keyCode) {
6712 case 70: // f
6713 if (!PDFViewerApplication.supportsIntegratedFind) {
6714 PDFViewerApplication.findBar.open();
6715 handled = true;
6716 }
6717 break;
6718 case 71: // g
6719 if (!PDFViewerApplication.supportsIntegratedFind) {
6720 PDFViewerApplication.findBar.dispatchEvent('again',
6721 cmd === 5 || cmd === 12);
6722 handled = true;
6723 }
6724 break;
6725 case 61: // FF/Mac '='
6726 case 107: // FF '+' and '='
6727 case 187: // Chrome '+'
6728 case 171: // FF with German keyboard
6729 if (!inPresentationMode) {
6730 PDFViewerApplication.zoomIn();
6731 }
6732 handled = true;
6733 break;
6734 case 173: // FF/Mac '-'
6735 case 109: // FF '-'
6736 case 189: // Chrome '-'
6737 if (!inPresentationMode) {
6738 PDFViewerApplication.zoomOut();
6739 }
6740 handled = true;
6741 break;
6742 case 48: // '0'
6743 case 96: // '0' on Numpad of Swedish keyboard
6744 if (!inPresentationMode) {
6745 // keeping it unhandled (to restore page zoom to 100%)
6746 setTimeout(function () {
6747 // ... and resetting the scale after browser adjusts its scale
6748 PDFViewerApplication.setScale(DEFAULT_SCALE, true);
6749 });
6750 handled = false;
6751 }
6752 break;
6753 }
6754 }
6755
6756 // CTRL or META without shift
6757 if (cmd === 1 || cmd === 8) {
6758 switch (evt.keyCode) {
6759 case 83: // s
6760 PDFViewerApplication.download();
6761 handled = true;
6762 break;
6763 }
6764 }
6765
6766 // CTRL+ALT or Option+Command
6767 if (cmd === 3 || cmd === 10) {
6768 switch (evt.keyCode) {
6769 case 80: // p
6770 SecondaryToolbar.presentationModeClick();
6771 handled = true;
6772 break;
6773 case 71: // g
6774 // focuses input#pageNumber field
6775 document.getElementById('pageNumber').select();
6776 handled = true;
6777 break;
6778 }
6779 }
6780
6781 if (handled) {
6782 evt.preventDefault();
6783 return;
6784 }
6785
6786 // Some shortcuts should not get handled if a control/input element
6787 // is selected.
6788 var curElement = document.activeElement || document.querySelector(':focus');
6789 var curElementTagName = curElement && curElement.tagName.toUpperCase();
6790 if (curElementTagName === 'INPUT' ||
6791 curElementTagName === 'TEXTAREA' ||
6792 curElementTagName === 'SELECT') {
6793 // Make sure that the secondary toolbar is closed when Escape is pressed.
6794 if (evt.keyCode !== 27) { // 'Esc'
6795 return;
6796 }
6797 }
6798
6799 if (cmd === 0) { // no control key pressed at all.
6800 switch (evt.keyCode) {
6801 case 38: // up arrow
6802 case 33: // pg up
6803 case 8: // backspace
6804 if (!PresentationMode.active &&
6805 PDFViewerApplication.currentScaleValue !== 'page-fit') {
6806 break;
6807 }
6808 /* in presentation mode */
6809 /* falls through */
6810 case 37: // left arrow
6811 // horizontal scrolling using arrow keys
6812 if (PDFViewerApplication.pdfViewer.isHorizontalScrollbarEnabled) {
6813 break;
6814 }
6815 /* falls through */
6816 case 75: // 'k'
6817 case 80: // 'p'
6818 PDFViewerApplication.page--;
6819 handled = true;
6820 break;
6821 case 27: // esc key
6822 if (SecondaryToolbar.opened) {
6823 SecondaryToolbar.close();
6824 handled = true;
6825 }
6826 if (!PDFViewerApplication.supportsIntegratedFind &&
6827 PDFViewerApplication.findBar.opened) {
6828 PDFViewerApplication.findBar.close();
6829 handled = true;
6830 }
6831 break;
6832 case 40: // down arrow
6833 case 34: // pg down
6834 case 32: // spacebar
6835 if (!PresentationMode.active &&
6836 PDFViewerApplication.currentScaleValue !== 'page-fit') {
6837 break;
6838 }
6839 /* falls through */
6840 case 39: // right arrow
6841 // horizontal scrolling using arrow keys
6842 if (PDFViewerApplication.pdfViewer.isHorizontalScrollbarEnabled) {
6843 break;
6844 }
6845 /* falls through */
6846 case 74: // 'j'
6847 case 78: // 'n'
6848 PDFViewerApplication.page++;
6849 handled = true;
6850 break;
6851
6852 case 36: // home
6853 if (PresentationMode.active || PDFViewerApplication.page > 1) {
6854 PDFViewerApplication.page = 1;
6855 handled = true;
6856 }
6857 break;
6858 case 35: // end
6859 if (PresentationMode.active || (PDFViewerApplication.pdfDocument &&
6860 PDFViewerApplication.page < PDFViewerApplication.pagesCount)) {
6861 PDFViewerApplication.page = PDFViewerApplication.pagesCount;
6862 handled = true;
6863 }
6864 break;
6865
6866 case 72: // 'h'
6867 if (!PresentationMode.active) {
6868 HandTool.toggle();
6869 }
6870 break;
6871 case 82: // 'r'
6872 PDFViewerApplication.rotatePages(90);
6873 break;
6874 }
6875 }
6876
6877 if (cmd === 4) { // shift-key
6878 switch (evt.keyCode) {
6879 case 32: // spacebar
6880 if (!PresentationMode.active &&
6881 PDFViewerApplication.currentScaleValue !== 'page-fit') {
6882 break;
6883 }
6884 PDFViewerApplication.page--;
6885 handled = true;
6886 break;
6887
6888 case 82: // 'r'
6889 PDFViewerApplication.rotatePages(-90);
6890 break;
6891 }
6892 }
6893
6894 if (!handled && !PresentationMode.active) {
6895 // 33=Page Up 34=Page Down 35=End 36=Home
6896 // 37=Left 38=Up 39=Right 40=Down
6897 if (evt.keyCode >= 33 && evt.keyCode <= 40 &&
6898 !PDFViewerApplication.pdfViewer.containsElement(curElement)) {
6899 // The page container is not focused, but a page navigation key has been
6900 // pressed. Change the focus to the viewer container to make sure that
6901 // navigation by keyboard works as expected.
6902 PDFViewerApplication.pdfViewer.focus();
6903 }
6904 // 32=Spacebar
6905 if (evt.keyCode === 32 && curElementTagName !== 'BUTTON') {
6906 if (!PDFViewerApplication.pdfViewer.containsElement(curElement)) {
6907 PDFViewerApplication.pdfViewer.focus();
6908 }
6909 }
6910 }
6911
6912 if (cmd === 2) { // alt-key
6913 switch (evt.keyCode) {
6914 case 37: // left arrow
6915 if (PresentationMode.active) {
6916 PDFHistory.back();
6917 handled = true;
6918 }
6919 break;
6920 case 39: // right arrow
6921 if (PresentationMode.active) {
6922 PDFHistory.forward();
6923 handled = true;
6924 }
6925 break;
6926 }
6927 }
6928
6929 if (handled) {
6930 evt.preventDefault();
6931 PDFViewerApplication.clearMouseScrollState();
6932 }
6933});
6934
6935window.addEventListener('beforeprint', function beforePrint(evt) {
6936 PDFViewerApplication.beforePrint();
6937});
6938
6939window.addEventListener('afterprint', function afterPrint(evt) {
6940 PDFViewerApplication.afterPrint();
6941});
6942
6943(function animationStartedClosure() {
6944 // The offsetParent is not set until the pdf.js iframe or object is visible.
6945 // Waiting for first animation.
6946 PDFViewerApplication.animationStartedPromise = new Promise(
6947 function (resolve) {
6948 window.requestAnimationFrame(resolve);
6949 });
6950})();