1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * @return {number} Width of a scrollbar in pixels
10 function getScrollbarWidth() {
11 var div
= document
.createElement('div');
12 div
.style
.visibility
= 'hidden';
13 div
.style
.overflow
= 'scroll';
14 div
.style
.width
= '50px';
15 div
.style
.height
= '50px';
16 div
.style
.position
= 'absolute';
17 document
.body
.appendChild(div
);
18 var result
= div
.offsetWidth
- div
.clientWidth
;
19 div
.parentNode
.removeChild(div
);
24 * Return the filename component of a URL.
25 * @param {string} url The URL to get the filename from.
26 * @return {string} The filename component.
28 function getFilenameFromURL(url
) {
29 var components
= url
.split(/\/|\\/);
30 return components
[components
.length
- 1];
34 * Called when navigation happens in the current tab.
35 * @param {string} url The url to be opened in the current tab.
37 function onNavigateInCurrentTab(url
) {
38 window
.location
.href
= url
;
42 * Called when navigation happens in the new tab.
43 * @param {string} url The url to be opened in the new tab.
45 function onNavigateInNewTab(url
) {
46 // Prefer the tabs API because it guarantees we can just open a new tab.
47 // window.open doesn't have this guarantee.
49 chrome
.tabs
.create({ url
: url
});
55 * The minimum number of pixels to offset the toolbar by from the bottom and
56 * right side of the screen.
58 PDFViewer
.MIN_TOOLBAR_OFFSET
= 15;
61 * Creates a new PDFViewer. There should only be one of these objects per
64 * @param {Object} streamDetails The stream object which points to the data
65 * contained in the PDF.
67 function PDFViewer(streamDetails
) {
68 this.streamDetails_
= streamDetails
;
70 this.parentWindow_
= null;
72 this.delayedScriptingMessages_
= [];
74 this.isPrintPreview_
=
75 this.streamDetails_
.originalUrl
.indexOf('chrome://print') == 0;
76 this.isMaterial_
= location
.pathname
.substring(1) === 'index-material.html';
78 // The sizer element is placed behind the plugin element to cause scrollbars
79 // to be displayed in the window. It is sized according to the document size
80 // of the pdf and zoom level.
81 this.sizer_
= $('sizer');
82 this.toolbar_
= $('toolbar');
83 this.pageIndicator_
= $('page-indicator');
84 this.progressBar_
= $('progress-bar');
85 this.passwordScreen_
= $('password-screen');
86 this.passwordScreen_
.addEventListener('password-submitted',
87 this.onPasswordSubmitted_
.bind(this));
88 this.errorScreen_
= $('error-screen');
90 // Create the viewport.
91 this.viewport_
= new Viewport(window
,
93 this.viewportChanged_
.bind(this),
94 this.beforeZoom_
.bind(this),
95 this.afterZoom_
.bind(this),
98 // Create the plugin object dynamically so we can set its src. The plugin
99 // element is sized to fill the entire window and is set to be fixed
100 // positioning, acting as a viewport. The plugin renders into this viewport
101 // according to the scroll position of the window.
102 this.plugin_
= document
.createElement('embed');
103 // NOTE: The plugin's 'id' field must be set to 'plugin' since
104 // chrome/renderer/printing/print_web_view_helper.cc actually references it.
105 this.plugin_
.id
= 'plugin';
106 this.plugin_
.type
= 'application/x-google-chrome-pdf';
107 this.plugin_
.addEventListener('message', this.handlePluginMessage_
.bind(this),
110 // Handle scripting messages from outside the extension that wish to interact
111 // with it. We also send a message indicating that extension has loaded and
112 // is ready to receive messages.
113 window
.addEventListener('message', this.handleScriptingMessage
.bind(this),
116 document
.title
= getFilenameFromURL(this.streamDetails_
.originalUrl
);
117 this.plugin_
.setAttribute('src', this.streamDetails_
.originalUrl
);
118 this.plugin_
.setAttribute('stream-url', this.streamDetails_
.streamUrl
);
120 for (var header
in this.streamDetails_
.responseHeaders
) {
121 headers
+= header
+ ': ' +
122 this.streamDetails_
.responseHeaders
[header
] + '\n';
124 this.plugin_
.setAttribute('headers', headers
);
126 if (this.isMaterial_
)
127 this.plugin_
.setAttribute('is-material', '');
129 if (!this.streamDetails_
.embedded
)
130 this.plugin_
.setAttribute('full-frame', '');
131 document
.body
.appendChild(this.plugin_
);
133 // Setup the button event listeners.
134 if (!this.isMaterial_
) {
135 $('fit-to-width-button').addEventListener('click',
136 this.viewport_
.fitToWidth
.bind(this.viewport_
));
137 $('fit-to-page-button').addEventListener('click',
138 this.viewport_
.fitToPage
.bind(this.viewport_
));
139 $('zoom-in-button').addEventListener('click',
140 this.viewport_
.zoomIn
.bind(this.viewport_
));
141 $('zoom-out-button').addEventListener('click',
142 this.viewport_
.zoomOut
.bind(this.viewport_
));
143 $('save-button').addEventListener('click', this.save_
.bind(this));
144 $('print-button').addEventListener('click', this.print_
.bind(this));
147 if (this.isMaterial_
) {
148 this.bookmarksPane_
= $('bookmarks-pane');
150 this.zoomSelector_
= $('zoom-selector');
151 this.zoomSelector_
.zoomMin
= Viewport
.ZOOM_FACTOR_RANGE
.min
* 100;
152 this.zoomSelector_
.zoomMax
= Viewport
.ZOOM_FACTOR_RANGE
.max
* 100;
153 this.zoomSelector_
.addEventListener('zoom', function(e
) {
154 this.viewport_
.setZoom(e
.detail
.zoom
);
156 this.zoomSelector_
.addEventListener('fit-to-width',
157 this.viewport_
.fitToWidth
.bind(this.viewport_
));
158 this.zoomSelector_
.addEventListener('fit-to-page',
159 this.viewport_
.fitToPage
.bind(this.viewport_
));
161 this.materialToolbar_
= $('material-toolbar');
162 this.materialToolbar_
.docTitle
= document
.title
;
163 this.materialToolbar_
.addEventListener('save', this.save_
.bind(this));
164 this.materialToolbar_
.addEventListener('print', this.print_
.bind(this));
165 this.materialToolbar_
.addEventListener('rotate-right',
166 this.rotateClockwise_
.bind(this));
167 this.materialToolbar_
.addEventListener('toggle-bookmarks', function() {
168 this.bookmarksPane_
.buttonToggle();
171 document
.body
.addEventListener('change-page', function(e
) {
172 this.viewport_
.goToPage(e
.detail
.page
);
175 this.uiManager_
= new UiManager(window
, this.materialToolbar_
,
176 [this.bookmarksPane_
]);
179 // Set up the zoom API.
180 if (this.shouldManageZoom_()) {
181 chrome
.tabs
.setZoomSettings(this.streamDetails_
.tabId
,
182 {mode
: 'manual', scope
: 'per-tab'}, function() {
184 new ZoomManager(this.viewport_
, this.setZoom_
.bind(this));
185 chrome
.tabs
.onZoomChange
.addListener(function(zoomChangeInfo
) {
186 if (zoomChangeInfo
.tabId
!= this.streamDetails_
.tabId
)
188 this.zoomManager_
.onBrowserZoomChange(zoomChangeInfo
.newZoomFactor
);
193 // Setup the keyboard event listener.
194 document
.onkeydown
= this.handleKeyEvent_
.bind(this);
196 // Parse open pdf parameters.
198 new OpenPDFParamsParser(this.getNamedDestination_
.bind(this));
199 this.navigator_
= new Navigator(this.streamDetails_
.originalUrl
,
200 this.viewport_
, this.paramsParser_
,
201 onNavigateInCurrentTab
, onNavigateInNewTab
);
202 this.viewportScroller_
=
203 new ViewportScroller(this.viewport_
, this.plugin_
, window
);
206 PDFViewer
.prototype = {
209 * Handle key events. These may come from the user directly or via the
211 * @param {KeyboardEvent} e the event to handle.
213 handleKeyEvent_: function(e
) {
214 var position
= this.viewport_
.position
;
215 // Certain scroll events may be sent from outside of the extension.
216 var fromScriptingAPI
= e
.fromScriptingAPI
;
218 var pageUpHandler = function() {
219 // Go to the previous page if we are fit-to-page.
220 if (this.viewport_
.fittingType
== Viewport
.FittingType
.FIT_TO_PAGE
) {
221 this.viewport_
.goToPage(this.viewport_
.getMostVisiblePage() - 1);
222 // Since we do the movement of the page.
224 } else if (fromScriptingAPI
) {
225 position
.y
-= this.viewport
.size
.height
;
226 this.viewport
.position
= position
;
229 var pageDownHandler = function() {
230 // Go to the next page if we are fit-to-page.
231 if (this.viewport_
.fittingType
== Viewport
.FittingType
.FIT_TO_PAGE
) {
232 this.viewport_
.goToPage(this.viewport_
.getMostVisiblePage() + 1);
233 // Since we do the movement of the page.
235 } else if (fromScriptingAPI
) {
236 position
.y
+= this.viewport
.size
.height
;
237 this.viewport
.position
= position
;
242 case 32: // Space key.
248 case 33: // Page up key.
251 case 34: // Page down key.
254 case 37: // Left arrow key.
255 if (!(e
.altKey
|| e
.ctrlKey
|| e
.metaKey
|| e
.shiftKey
)) {
256 // Go to the previous page if there are no horizontal scrollbars.
257 if (!this.viewport_
.documentHasScrollbars().horizontal
) {
258 this.viewport_
.goToPage(this.viewport_
.getMostVisiblePage() - 1);
259 // Since we do the movement of the page.
261 } else if (fromScriptingAPI
) {
262 position
.x
-= Viewport
.SCROLL_INCREMENT
;
263 this.viewport
.position
= position
;
267 case 38: // Up arrow key.
268 if (fromScriptingAPI
) {
269 position
.y
-= Viewport
.SCROLL_INCREMENT
;
270 this.viewport
.position
= position
;
273 case 39: // Right arrow key.
274 if (!(e
.altKey
|| e
.ctrlKey
|| e
.metaKey
|| e
.shiftKey
)) {
275 // Go to the next page if there are no horizontal scrollbars.
276 if (!this.viewport_
.documentHasScrollbars().horizontal
) {
277 this.viewport_
.goToPage(this.viewport_
.getMostVisiblePage() + 1);
278 // Since we do the movement of the page.
280 } else if (fromScriptingAPI
) {
281 position
.x
+= Viewport
.SCROLL_INCREMENT
;
282 this.viewport
.position
= position
;
286 case 40: // Down arrow key.
287 if (fromScriptingAPI
) {
288 position
.y
+= Viewport
.SCROLL_INCREMENT
;
289 this.viewport
.position
= position
;
293 if (e
.ctrlKey
|| e
.metaKey
) {
294 this.plugin_
.postMessage({
297 // Since we do selection ourselves.
302 if (this.isMaterial_
&& (e
.ctrlKey
|| e
.metaKey
)) {
303 this.materialToolbar_
.selectPageNumber();
304 // To prevent the default "find text" behaviour in Chrome.
308 case 219: // left bracket.
310 this.rotateCounterClockwise_();
312 case 221: // right bracket.
314 this.rotateClockwise_();
318 // Give print preview a chance to handle the key event.
319 if (!fromScriptingAPI
&& this.isPrintPreview_
) {
320 this.sendScriptingMessage_({
321 type
: 'sendKeyEvent',
322 keyEvent
: SerializeKeyEvent(e
)
329 * Rotate the plugin clockwise.
331 rotateClockwise_: function() {
332 this.plugin_
.postMessage({
333 type
: 'rotateClockwise'
339 * Rotate the plugin counter-clockwise.
341 rotateCounterClockwise_: function() {
342 this.plugin_
.postMessage({
343 type
: 'rotateCounterclockwise'
349 * Notify the plugin to print.
352 this.plugin_
.postMessage({
359 * Notify the plugin to save.
362 this.plugin_
.postMessage({
368 * Fetches the page number corresponding to the given named destination from
370 * @param {string} name The namedDestination to fetch page number from plugin.
372 getNamedDestination_: function(name
) {
373 this.plugin_
.postMessage({
374 type
: 'getNamedDestination',
375 namedDestination
: name
381 * Handle open pdf parameters. This function updates the viewport as per
382 * the parameters mentioned in the url while opening pdf. The order is
383 * important as later actions can override the effects of previous actions.
384 * @param {Object} viewportPosition The initial position of the viewport to be
387 handleURLParams_: function(viewportPosition
) {
388 if (viewportPosition
.page
!= undefined)
389 this.viewport_
.goToPage(viewportPosition
.page
);
390 if (viewportPosition
.position
) {
391 // Make sure we don't cancel effect of page parameter.
392 this.viewport_
.position
= {
393 x
: this.viewport_
.position
.x
+ viewportPosition
.position
.x
,
394 y
: this.viewport_
.position
.y
+ viewportPosition
.position
.y
397 if (viewportPosition
.zoom
)
398 this.viewport_
.setZoom(viewportPosition
.zoom
);
403 * Update the loading progress of the document in response to a progress
404 * message being received from the plugin.
405 * @param {number} progress the progress as a percentage.
407 updateProgress_: function(progress
) {
408 if (this.isMaterial_
)
409 this.materialToolbar_
.loadProgress
= progress
;
411 this.progressBar_
.progress
= progress
;
413 if (progress
== -1) {
414 // Document load failed.
415 this.errorScreen_
.style
.visibility
= 'visible';
416 this.sizer_
.style
.display
= 'none';
417 if (!this.isMaterial_
)
418 this.toolbar_
.style
.visibility
= 'hidden';
419 if (this.passwordScreen_
.active
) {
420 this.passwordScreen_
.deny();
421 this.passwordScreen_
.active
= false;
423 } else if (progress
== 100) {
424 // Document load complete.
425 if (this.lastViewportPosition_
)
426 this.viewport_
.position
= this.lastViewportPosition_
;
427 this.paramsParser_
.getViewportFromUrlParams(
428 this.streamDetails_
.originalUrl
, this.handleURLParams_
.bind(this));
430 this.sendScriptingMessage_({
431 type
: 'documentLoaded'
433 while (this.delayedScriptingMessages_
.length
> 0)
434 this.handleScriptingMessage(this.delayedScriptingMessages_
.shift());
436 if (this.isMaterial_
)
437 this.uiManager_
.hideUiAfterTimeout();
443 * An event handler for handling password-submitted events. These are fired
444 * when an event is entered into the password screen.
445 * @param {Object} event a password-submitted event.
447 onPasswordSubmitted_: function(event
) {
448 this.plugin_
.postMessage({
449 type
: 'getPasswordComplete',
450 password
: event
.detail
.password
456 * An event handler for handling message events received from the plugin.
457 * @param {MessageObject} message a message event.
459 handlePluginMessage_: function(message
) {
460 switch (message
.data
.type
.toString()) {
461 case 'documentDimensions':
462 this.documentDimensions_
= message
.data
;
463 this.viewport_
.setDocumentDimensions(this.documentDimensions_
);
464 // If we received the document dimensions, the password was good so we
465 // can dismiss the password screen.
466 if (this.passwordScreen_
.active
)
467 this.passwordScreen_
.accept();
469 if (this.isMaterial_
) {
470 this.materialToolbar_
.docLength
=
471 this.documentDimensions_
.pageDimensions
.length
;
473 this.pageIndicator_
.initialFadeIn();
474 this.toolbar_
.initialFadeIn();
478 var href
= 'mailto:' + message
.data
.to
+ '?cc=' + message
.data
.cc
+
479 '&bcc=' + message
.data
.bcc
+ '&subject=' + message
.data
.subject
+
480 '&body=' + message
.data
.body
;
481 window
.location
.href
= href
;
483 case 'getAccessibilityJSONReply':
484 this.sendScriptingMessage_(message
.data
);
487 // If the password screen isn't up, put it up. Otherwise we're
488 // responding to an incorrect password so deny it.
489 if (!this.passwordScreen_
.active
)
490 this.passwordScreen_
.active
= true;
492 this.passwordScreen_
.deny();
494 case 'getSelectedTextReply':
495 this.sendScriptingMessage_(message
.data
);
498 this.viewport_
.goToPage(message
.data
.page
);
501 this.updateProgress_(message
.data
.progress
);
504 // If in print preview, always open a new tab.
505 if (this.isPrintPreview_
)
506 this.navigator_
.navigate(message
.data
.url
, true);
508 this.navigator_
.navigate(message
.data
.url
, message
.data
.newTab
);
510 case 'setScrollPosition':
511 var position
= this.viewport_
.position
;
512 if (message
.data
.x
!== undefined)
513 position
.x
= message
.data
.x
;
514 if (message
.data
.y
!== undefined)
515 position
.y
= message
.data
.y
;
516 this.viewport_
.position
= position
;
518 case 'setTranslatedStrings':
519 this.passwordScreen_
.text
= message
.data
.getPasswordString
;
520 if (!this.isMaterial_
) {
521 this.progressBar_
.text
= message
.data
.loadingString
;
522 if (!this.isPrintPreview_
)
523 this.progressBar_
.style
.visibility
= 'visible';
525 this.errorScreen_
.text
= message
.data
.loadFailedString
;
527 case 'cancelStreamUrl':
528 chrome
.mimeHandlerPrivate
.abortStream();
531 this.bookmarks_
= message
.data
.bookmarks
;
532 if (this.isMaterial_
&& this.bookmarks_
.length
!== 0) {
533 $('bookmarks-container').bookmarks
= this.bookmarks
;
534 this.materialToolbar_
.hasBookmarks
= true;
537 case 'setIsSelecting':
538 this.viewportScroller_
.setEnableScrolling(message
.data
.isSelecting
);
540 case 'getNamedDestinationReply':
541 this.paramsParser_
.onNamedDestinationReceived(
542 message
.data
.pageNumber
);
549 * A callback that's called before the zoom changes. Notify the plugin to stop
550 * reacting to scroll events while zoom is taking place to avoid flickering.
552 beforeZoom_: function() {
553 this.plugin_
.postMessage({
554 type
: 'stopScrolling'
560 * A callback that's called after the zoom changes. Notify the plugin of the
561 * zoom change and to continue reacting to scroll events.
563 afterZoom_: function() {
564 var position
= this.viewport_
.position
;
565 var zoom
= this.viewport_
.zoom
;
566 if (this.isMaterial_
)
567 this.zoomSelector_
.zoomValue
= 100 * zoom
;
568 this.plugin_
.postMessage({
574 if (this.zoomManager_
)
575 this.zoomManager_
.onPdfZoomChange();
578 setZoom_: function(zoom
) {
579 return new Promise(function(resolve
, reject
) {
580 chrome
.tabs
.setZoom(this.streamDetails_
.tabId
, zoom
, resolve
);
586 * A callback that's called after the viewport changes.
588 viewportChanged_: function() {
589 if (!this.documentDimensions_
)
592 // Update the buttons selected.
593 if (!this.isMaterial_
) {
594 $('fit-to-page-button').classList
.remove('polymer-selected');
595 $('fit-to-width-button').classList
.remove('polymer-selected');
596 if (this.viewport_
.fittingType
== Viewport
.FittingType
.FIT_TO_PAGE
) {
597 $('fit-to-page-button').classList
.add('polymer-selected');
598 } else if (this.viewport_
.fittingType
==
599 Viewport
.FittingType
.FIT_TO_WIDTH
) {
600 $('fit-to-width-button').classList
.add('polymer-selected');
604 // Offset the toolbar position so that it doesn't move if scrollbars appear.
605 var hasScrollbars
= this.viewport_
.documentHasScrollbars();
606 var scrollbarWidth
= this.viewport_
.scrollbarWidth
;
607 var verticalScrollbarWidth
= hasScrollbars
.vertical
? scrollbarWidth
: 0;
608 var horizontalScrollbarWidth
=
609 hasScrollbars
.horizontal
? scrollbarWidth
: 0;
610 var toolbarRight
= Math
.max(PDFViewer
.MIN_TOOLBAR_OFFSET
, scrollbarWidth
);
611 var toolbarBottom
= Math
.max(PDFViewer
.MIN_TOOLBAR_OFFSET
, scrollbarWidth
);
612 toolbarRight
-= verticalScrollbarWidth
;
613 toolbarBottom
-= horizontalScrollbarWidth
;
614 if (!this.isMaterial_
) {
615 this.toolbar_
.style
.right
= toolbarRight
+ 'px';
616 this.toolbar_
.style
.bottom
= toolbarBottom
+ 'px';
617 // Hide the toolbar if it doesn't fit in the viewport.
618 if (this.toolbar_
.offsetLeft
< 0 || this.toolbar_
.offsetTop
< 0)
619 this.toolbar_
.style
.visibility
= 'hidden';
621 this.toolbar_
.style
.visibility
= 'visible';
624 // Update the page indicator.
625 var visiblePage
= this.viewport_
.getMostVisiblePage();
626 if (this.isMaterial_
) {
627 this.materialToolbar_
.pageIndex
= visiblePage
;
629 this.pageIndicator_
.index
= visiblePage
;
630 if (this.documentDimensions_
.pageDimensions
.length
> 1 &&
631 hasScrollbars
.vertical
) {
632 this.pageIndicator_
.style
.visibility
= 'visible';
634 this.pageIndicator_
.style
.visibility
= 'hidden';
638 var visiblePageDimensions
= this.viewport_
.getPageScreenRect(visiblePage
);
639 var size
= this.viewport_
.size
;
640 this.sendScriptingMessage_({
642 pageX
: visiblePageDimensions
.x
,
643 pageY
: visiblePageDimensions
.y
,
644 pageWidth
: visiblePageDimensions
.width
,
645 viewportWidth
: size
.width
,
646 viewportHeight
: size
.height
651 * Handle a scripting message from outside the extension (typically sent by
652 * PDFScriptingAPI in a page containing the extension) to interact with the
654 * @param {MessageObject} message the message to handle.
656 handleScriptingMessage: function(message
) {
657 if (this.parentWindow_
!= message
.source
) {
658 this.parentWindow_
= message
.source
;
659 // Ensure that we notify the embedder if the document is loaded.
661 this.sendScriptingMessage_({
662 type
: 'documentLoaded'
667 if (this.handlePrintPreviewScriptingMessage_(message
))
670 // Delay scripting messages from users of the scripting API until the
671 // document is loaded. This simplifies use of the APIs.
673 this.delayedScriptingMessages_
.push(message
);
677 switch (message
.data
.type
.toString()) {
678 case 'getAccessibilityJSON':
679 case 'getSelectedText':
682 this.plugin_
.postMessage(message
.data
);
689 * Handle scripting messages specific to print preview.
690 * @param {MessageObject} message the message to handle.
691 * @return {boolean} true if the message was handled, false otherwise.
693 handlePrintPreviewScriptingMessage_: function(message
) {
694 if (!this.isPrintPreview_
)
697 switch (message
.data
.type
.toString()) {
698 case 'loadPreviewPage':
699 this.plugin_
.postMessage(message
.data
);
701 case 'resetPrintPreviewMode':
702 this.loaded_
= false;
703 if (!this.inPrintPreviewMode_
) {
704 this.inPrintPreviewMode_
= true;
705 this.viewport_
.fitToPage();
708 // Stash the scroll location so that it can be restored when the new
709 // document is loaded.
710 this.lastViewportPosition_
= this.viewport_
.position
;
712 // TODO(raymes): Disable these properly in the plugin.
713 var printButton
= $('print-button');
715 printButton
.parentNode
.removeChild(printButton
);
716 var saveButton
= $('save-button');
718 saveButton
.parentNode
.removeChild(saveButton
);
720 if (!this.isMaterial_
)
721 this.pageIndicator_
.pageLabels
= message
.data
.pageNumbers
;
723 this.plugin_
.postMessage({
724 type
: 'resetPrintPreviewMode',
725 url
: message
.data
.url
,
726 grayscale
: message
.data
.grayscale
,
727 // If the PDF isn't modifiable we send 0 as the page count so that no
728 // blank placeholder pages get appended to the PDF.
729 pageCount
: (message
.data
.modifiable
?
730 message
.data
.pageNumbers
.length
: 0)
734 this.handleKeyEvent_(DeserializeKeyEvent(message
.data
.keyEvent
));
743 * Send a scripting message outside the extension (typically to
744 * PDFScriptingAPI in a page containing the extension).
745 * @param {Object} message the message to send.
747 sendScriptingMessage_: function(message
) {
748 if (this.parentWindow_
)
749 this.parentWindow_
.postMessage(message
, '*');
754 * Return whether this PDFViewer should manage zoom for its containing page.
755 * @return {boolean} Whether this PDFViewer should manage zoom for its
758 shouldManageZoom_: function() {
759 return !!(chrome
.tabs
&& !this.streamDetails_
.embedded
&&
760 this.streamDetails_
.tabId
!= -1);
764 * @type {Viewport} the viewport of the PDF viewer.
767 return this.viewport_
;
771 * Each bookmark is an Object containing a:
774 * - array of children (themselves bookmarks)
775 * @type {Array} the top-level bookmarks of the PDF.
778 return this.bookmarks_
;