Home | History | Annotate | Download | only in pdf
      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.
      4 
      5 'use strict';
      6 
      7 <include src="../../../../ui/webui/resources/js/util.js">
      8 <include src="pdf_scripting_api.js">
      9 <include src="viewport.js">
     10 
     11 /**
     12  * @return {number} Width of a scrollbar in pixels
     13  */
     14 function getScrollbarWidth() {
     15   var div = document.createElement('div');
     16   div.style.visibility = 'hidden';
     17   div.style.overflow = 'scroll';
     18   div.style.width = '50px';
     19   div.style.height = '50px';
     20   div.style.position = 'absolute';
     21   document.body.appendChild(div);
     22   var result = div.offsetWidth - div.clientWidth;
     23   div.parentNode.removeChild(div);
     24   return result;
     25 }
     26 
     27 /**
     28  * The minimum number of pixels to offset the toolbar by from the bottom and
     29  * right side of the screen.
     30  */
     31 PDFViewer.MIN_TOOLBAR_OFFSET = 15;
     32 
     33 /**
     34  * Creates a new PDFViewer. There should only be one of these objects per
     35  * document.
     36  */
     37 function PDFViewer() {
     38   this.loaded = false;
     39 
     40   // The sizer element is placed behind the plugin element to cause scrollbars
     41   // to be displayed in the window. It is sized according to the document size
     42   // of the pdf and zoom level.
     43   this.sizer_ = $('sizer');
     44   this.toolbar_ = $('toolbar');
     45   this.pageIndicator_ = $('page-indicator');
     46   this.progressBar_ = $('progress-bar');
     47   this.passwordScreen_ = $('password-screen');
     48   this.passwordScreen_.addEventListener('password-submitted',
     49                                         this.onPasswordSubmitted_.bind(this));
     50   this.errorScreen_ = $('error-screen');
     51 
     52   // Create the viewport.
     53   this.viewport_ = new Viewport(window,
     54                                 this.sizer_,
     55                                 this.viewportChangedCallback_.bind(this),
     56                                 getScrollbarWidth());
     57 
     58   // Create the plugin object dynamically so we can set its src. The plugin
     59   // element is sized to fill the entire window and is set to be fixed
     60   // positioning, acting as a viewport. The plugin renders into this viewport
     61   // according to the scroll position of the window.
     62   this.plugin_ = document.createElement('object');
     63   // NOTE: The plugin's 'id' field must be set to 'plugin' since
     64   // chrome/renderer/printing/print_web_view_helper.cc actually references it.
     65   this.plugin_.id = 'plugin';
     66   this.plugin_.type = 'application/x-google-chrome-pdf';
     67   this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
     68                                 false);
     69 
     70   // Handle scripting messages from outside the extension that wish to interact
     71   // with it. We also send a message indicating that extension has loaded and
     72   // is ready to receive messages.
     73   window.addEventListener('message', this.handleScriptingMessage_.bind(this),
     74                           false);
     75   this.sendScriptingMessage_({type: 'readyToReceive'});
     76 
     77   // If the viewer is started from a MIME type request, there will be a
     78   // background page and stream details object with the details of the request.
     79   // Otherwise, we take the query string of the URL to indicate the URL of the
     80   // PDF to load. This is used for print preview in particular.
     81   if (chrome.extension.getBackgroundPage &&
     82       chrome.extension.getBackgroundPage()) {
     83     this.streamDetails =
     84         chrome.extension.getBackgroundPage().popStreamDetails();
     85   }
     86 
     87   if (!this.streamDetails) {
     88     // The URL of this page will be of the form
     89     // "chrome-extension://<extension id>?<pdf url>". We pull out the <pdf url>
     90     // part here.
     91     var url = window.location.search.substring(1);
     92     this.streamDetails = {
     93       streamUrl: url,
     94       originalUrl: url,
     95       responseHeaders: ''
     96     };
     97   }
     98 
     99   this.plugin_.setAttribute('src', this.streamDetails.originalUrl);
    100   this.plugin_.setAttribute('stream-url', this.streamDetails.streamUrl);
    101   var headers = '';
    102   for (var header in this.streamDetails.responseHeaders) {
    103     headers += header + ': ' +
    104         this.streamDetails.responseHeaders[header] + '\n';
    105   }
    106   this.plugin_.setAttribute('headers', headers);
    107 
    108   if (window.top == window)
    109     this.plugin_.setAttribute('full-frame', '');
    110   document.body.appendChild(this.plugin_);
    111 
    112   // Setup the button event listeners.
    113   $('fit-to-width-button').addEventListener('click',
    114       this.viewport_.fitToWidth.bind(this.viewport_));
    115   $('fit-to-page-button').addEventListener('click',
    116       this.viewport_.fitToPage.bind(this.viewport_));
    117   $('zoom-in-button').addEventListener('click',
    118       this.viewport_.zoomIn.bind(this.viewport_));
    119   $('zoom-out-button').addEventListener('click',
    120       this.viewport_.zoomOut.bind(this.viewport_));
    121   $('save-button-link').href = this.streamDetails.originalUrl;
    122   $('print-button').addEventListener('click', this.print_.bind(this));
    123 
    124   // Setup the keyboard event listener.
    125   document.onkeydown = this.handleKeyEvent_.bind(this);
    126 }
    127 
    128 PDFViewer.prototype = {
    129   /**
    130    * @private
    131    * Handle key events. These may come from the user directly or via the
    132    * scripting API.
    133    * @param {KeyboardEvent} e the event to handle.
    134    */
    135   handleKeyEvent_: function(e) {
    136     var position = this.viewport_.position;
    137     // Certain scroll events may be sent from outside of the extension.
    138     var fromScriptingAPI = e.type == 'scriptingKeypress';
    139 
    140     switch (e.keyCode) {
    141       case 33:  // Page up key.
    142         // Go to the previous page if we are fit-to-page.
    143         if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
    144           this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
    145           // Since we do the movement of the page.
    146           e.preventDefault();
    147         } else if (fromScriptingAPI) {
    148           position.y -= this.viewport.size.height;
    149           this.viewport.position = position;
    150         }
    151         return;
    152       case 34:  // Page down key.
    153         // Go to the next page if we are fit-to-page.
    154         if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
    155           this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
    156           // Since we do the movement of the page.
    157           e.preventDefault();
    158         } else if (fromScriptingAPI) {
    159           position.y += this.viewport.size.height;
    160           this.viewport.position = position;
    161         }
    162         return;
    163       case 37:  // Left arrow key.
    164         // Go to the previous page if there are no horizontal scrollbars.
    165         if (!this.viewport_.documentHasScrollbars().x) {
    166           this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
    167           // Since we do the movement of the page.
    168           e.preventDefault();
    169         } else if (fromScriptingAPI) {
    170           position.x -= Viewport.SCROLL_INCREMENT;
    171           this.viewport.position = position;
    172         }
    173         return;
    174       case 38:  // Up arrow key.
    175         if (fromScriptingAPI) {
    176           position.y -= Viewport.SCROLL_INCREMENT;
    177           this.viewport.position = position;
    178         }
    179         return;
    180       case 39:  // Right arrow key.
    181         // Go to the next page if there are no horizontal scrollbars.
    182         if (!this.viewport_.documentHasScrollbars().x) {
    183           this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
    184           // Since we do the movement of the page.
    185           e.preventDefault();
    186         } else if (fromScriptingAPI) {
    187           position.x += Viewport.SCROLL_INCREMENT;
    188           this.viewport.position = position;
    189         }
    190         return;
    191       case 40:  // Down arrow key.
    192         if (fromScriptingAPI) {
    193           position.y += Viewport.SCROLL_INCREMENT;
    194           this.viewport.position = position;
    195         }
    196         return;
    197       case 187:  // +/= key.
    198       case 107:  // Numpad + key.
    199         if (e.ctrlKey || e.metaKey) {
    200           this.viewport_.zoomIn();
    201           // Since we do the zooming of the page.
    202           e.preventDefault();
    203         }
    204         return;
    205       case 189:  // -/_ key.
    206       case 109:  // Numpad - key.
    207         if (e.ctrlKey || e.metaKey) {
    208           this.viewport_.zoomOut();
    209           // Since we do the zooming of the page.
    210           e.preventDefault();
    211         }
    212         return;
    213       case 83:  // s key.
    214         if (e.ctrlKey || e.metaKey) {
    215           // Simulate a click on the button so that the <a download ...>
    216           // attribute is used.
    217           $('save-button-link').click();
    218           // Since we do the saving of the page.
    219           e.preventDefault();
    220         }
    221         return;
    222       case 80:  // p key.
    223         if (e.ctrlKey || e.metaKey) {
    224           this.print_();
    225           // Since we do the printing of the page.
    226           e.preventDefault();
    227         }
    228         return;
    229     }
    230   },
    231 
    232   /**
    233    * @private
    234    * Notify the plugin to print.
    235    */
    236   print_: function() {
    237     this.plugin_.postMessage({
    238       type: 'print',
    239     });
    240   },
    241 
    242   /**
    243    * @private
    244    * Update the loading progress of the document in response to a progress
    245    * message being received from the plugin.
    246    * @param {number} progress the progress as a percentage.
    247    */
    248   updateProgress_: function(progress) {
    249     this.progressBar_.progress = progress;
    250     if (progress == -1) {
    251       // Document load failed.
    252       this.errorScreen_.style.visibility = 'visible';
    253       this.sizer_.style.display = 'none';
    254       this.toolbar_.style.visibility = 'hidden';
    255       if (this.passwordScreen_.active) {
    256         this.passwordScreen_.deny();
    257         this.passwordScreen_.active = false;
    258       }
    259     } else if (progress == 100) {
    260       // Document load complete.
    261       this.loaded = true;
    262       var loadEvent = new Event('pdfload');
    263       window.dispatchEvent(loadEvent);
    264       this.sendScriptingMessage_({
    265         type: 'documentLoaded'
    266       });
    267       if (this.lastViewportPosition_)
    268         this.viewport_.position = this.lastViewportPosition_;
    269     }
    270   },
    271 
    272   /**
    273    * @private
    274    * An event handler for handling password-submitted events. These are fired
    275    * when an event is entered into the password screen.
    276    * @param {Object} event a password-submitted event.
    277    */
    278   onPasswordSubmitted_: function(event) {
    279     this.plugin_.postMessage({
    280       type: 'getPasswordComplete',
    281       password: event.detail.password
    282     });
    283   },
    284 
    285   /**
    286    * @private
    287    * An event handler for handling message events received from the plugin.
    288    * @param {MessageObject} message a message event.
    289    */
    290   handlePluginMessage_: function(message) {
    291     switch (message.data.type.toString()) {
    292       case 'documentDimensions':
    293         this.documentDimensions_ = message.data;
    294         this.viewport_.setDocumentDimensions(this.documentDimensions_);
    295         this.toolbar_.style.visibility = 'visible';
    296         // If we received the document dimensions, the password was good so we
    297         // can dismiss the password screen.
    298         if (this.passwordScreen_.active)
    299           this.passwordScreen_.accept();
    300 
    301         this.pageIndicator_.initialFadeIn();
    302         this.toolbar_.initialFadeIn();
    303         break;
    304       case 'email':
    305         var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
    306             '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
    307             '&body=' + message.data.body;
    308         var w = window.open(href, '_blank', 'width=1,height=1');
    309         if (w)
    310           w.close();
    311         break;
    312       case 'getAccessibilityJSONReply':
    313         this.sendScriptingMessage_(message.data);
    314         break;
    315       case 'getPassword':
    316         // If the password screen isn't up, put it up. Otherwise we're
    317         // responding to an incorrect password so deny it.
    318         if (!this.passwordScreen_.active)
    319           this.passwordScreen_.active = true;
    320         else
    321           this.passwordScreen_.deny();
    322         break;
    323       case 'goToPage':
    324         this.viewport_.goToPage(message.data.page);
    325         break;
    326       case 'loadProgress':
    327         this.updateProgress_(message.data.progress);
    328         break;
    329       case 'navigate':
    330         if (message.data.newTab)
    331           window.open(message.data.url);
    332         else
    333           window.location.href = message.data.url;
    334         break;
    335       case 'setScrollPosition':
    336         var position = this.viewport_.position;
    337         if (message.data.x != undefined)
    338           position.x = message.data.x;
    339         if (message.data.y != undefined)
    340           position.y = message.data.y;
    341         this.viewport_.position = position;
    342         break;
    343       case 'setTranslatedStrings':
    344         this.passwordScreen_.text = message.data.getPasswordString;
    345         this.progressBar_.text = message.data.loadingString;
    346         this.errorScreen_.text = message.data.loadFailedString;
    347         break;
    348       case 'cancelStreamUrl':
    349         chrome.streamsPrivate.abort(this.streamDetails.streamUrl);
    350         break;
    351     }
    352   },
    353 
    354   /**
    355    * @private
    356    * A callback that's called when the viewport changes.
    357    */
    358   viewportChangedCallback_: function() {
    359     if (!this.documentDimensions_)
    360       return;
    361 
    362     // Update the buttons selected.
    363     $('fit-to-page-button').classList.remove('polymer-selected');
    364     $('fit-to-width-button').classList.remove('polymer-selected');
    365     if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
    366       $('fit-to-page-button').classList.add('polymer-selected');
    367     } else if (this.viewport_.fittingType ==
    368                Viewport.FittingType.FIT_TO_WIDTH) {
    369       $('fit-to-width-button').classList.add('polymer-selected');
    370     }
    371 
    372     var hasScrollbars = this.viewport_.documentHasScrollbars();
    373     var scrollbarWidth = this.viewport_.scrollbarWidth;
    374     // Offset the toolbar position so that it doesn't move if scrollbars appear.
    375     var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
    376     var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
    377     if (hasScrollbars.vertical)
    378       toolbarRight -= scrollbarWidth;
    379     if (hasScrollbars.horizontal)
    380       toolbarBottom -= scrollbarWidth;
    381     this.toolbar_.style.right = toolbarRight + 'px';
    382     this.toolbar_.style.bottom = toolbarBottom + 'px';
    383 
    384     // Update the page indicator.
    385     var visiblePage = this.viewport_.getMostVisiblePage();
    386     this.pageIndicator_.index = visiblePage;
    387     if (this.documentDimensions_.pageDimensions.length > 1 &&
    388         hasScrollbars.vertical) {
    389       this.pageIndicator_.style.visibility = 'visible';
    390     } else {
    391       this.pageIndicator_.style.visibility = 'hidden';
    392     }
    393 
    394     var position = this.viewport_.position;
    395     var zoom = this.viewport_.zoom;
    396     // Notify the plugin of the viewport change.
    397     this.plugin_.postMessage({
    398       type: 'viewport',
    399       zoom: zoom,
    400       xOffset: position.x,
    401       yOffset: position.y
    402     });
    403 
    404     var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
    405     var size = this.viewport_.size;
    406     this.sendScriptingMessage_({
    407       type: 'viewport',
    408       pageX: visiblePageDimensions.x,
    409       pageY: visiblePageDimensions.y,
    410       pageWidth: visiblePageDimensions.width,
    411       viewportWidth: size.width,
    412       viewportHeight: size.height,
    413     });
    414   },
    415 
    416   /**
    417    * @private
    418    * Handle a scripting message from outside the extension (typically sent by
    419    * PDFScriptingAPI in a page containing the extension) to interact with the
    420    * plugin.
    421    * @param {MessageObject} message the message to handle.
    422    */
    423   handleScriptingMessage_: function(message) {
    424     switch (message.data.type.toString()) {
    425       case 'getAccessibilityJSON':
    426       case 'loadPreviewPage':
    427         this.plugin_.postMessage(message.data);
    428         break;
    429       case 'resetPrintPreviewMode':
    430         if (!this.inPrintPreviewMode_) {
    431           this.inPrintPreviewMode_ = true;
    432           this.viewport_.fitToPage();
    433         }
    434 
    435         // Stash the scroll location so that it can be restored when the new
    436         // document is loaded.
    437         this.lastViewportPosition_ = this.viewport_.position;
    438 
    439         // TODO(raymes): Disable these properly in the plugin.
    440         var printButton = $('print-button');
    441         if (printButton)
    442           printButton.parentNode.removeChild(printButton);
    443         var saveButton = $('save-button');
    444         if (saveButton)
    445           saveButton.parentNode.removeChild(saveButton);
    446 
    447         this.pageIndicator_.pageLabels = message.data.pageNumbers;
    448 
    449         this.plugin_.postMessage({
    450           type: 'resetPrintPreviewMode',
    451           url: message.data.url,
    452           grayscale: message.data.grayscale,
    453           // If the PDF isn't modifiable we send 0 as the page count so that no
    454           // blank placeholder pages get appended to the PDF.
    455           pageCount: (message.data.modifiable ?
    456                       message.data.pageNumbers.length : 0)
    457         });
    458         break;
    459       case 'sendKeyEvent':
    460         var e = document.createEvent('Event');
    461         e.initEvent('scriptingKeypress');
    462         e.keyCode = message.data.keyCode;
    463         this.handleKeyEvent_(e);
    464         break;
    465     }
    466 
    467   },
    468 
    469   /**
    470    * @private
    471    * Send a scripting message outside the extension (typically to
    472    * PDFScriptingAPI in a page containing the extension).
    473    * @param {Object} message the message to send.
    474    */
    475   sendScriptingMessage_: function(message) {
    476     window.parent.postMessage(message, '*');
    477   },
    478 
    479   /**
    480    * @type {Viewport} the viewport of the PDF viewer.
    481    */
    482   get viewport() {
    483     return this.viewport_;
    484   }
    485 };
    486 
    487 var viewer = new PDFViewer();
    488