Home | History | Annotate | Download | only in image_loader
      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 /**
      8  * Creates and starts downloading and then resizing of the image. Finally,
      9  * returns the image using the callback.
     10  *
     11  * @param {string} id Request ID.
     12  * @param {Cache} cache Cache object.
     13  * @param {Object} request Request message as a hash array.
     14  * @param {function} callback Callback used to send the response.
     15  * @constructor
     16  */
     17 function Request(id, cache, request, callback) {
     18   /**
     19    * @type {string}
     20    * @private
     21    */
     22   this.id_ = id;
     23 
     24   /**
     25    * @type {Cache}
     26    * @private
     27    */
     28   this.cache_ = cache;
     29 
     30   /**
     31    * @type {Object}
     32    * @private
     33    */
     34   this.request_ = request;
     35 
     36   /**
     37    * @type {function}
     38    * @private
     39    */
     40   this.sendResponse_ = callback;
     41 
     42   /**
     43    * Temporary image used to download images.
     44    * @type {Image}
     45    * @private
     46    */
     47   this.image_ = new Image();
     48 
     49   /**
     50    * MIME type of the fetched image.
     51    * @type {string}
     52    * @private
     53    */
     54   this.contentType_ = null;
     55 
     56   /**
     57    * Used to download remote images using http:// or https:// protocols.
     58    * @type {AuthorizedXHR}
     59    * @private
     60    */
     61   this.xhr_ = new AuthorizedXHR();
     62 
     63   /**
     64    * Temporary canvas used to resize and compress the image.
     65    * @type {HTMLCanvasElement}
     66    * @private
     67    */
     68   this.canvas_ = document.createElement('canvas');
     69 
     70   /**
     71    * @type {CanvasRenderingContext2D}
     72    * @private
     73    */
     74   this.context_ = this.canvas_.getContext('2d');
     75 
     76   /**
     77    * Callback to be called once downloading is finished.
     78    * @type {function()}
     79    * @private
     80    */
     81   this.downloadCallback_ = null;
     82 }
     83 
     84 /**
     85  * Returns ID of the request.
     86  * @return {string} Request ID.
     87  */
     88 Request.prototype.getId = function() {
     89   return this.id_;
     90 };
     91 
     92 /**
     93  * Returns priority of the request. The higher priority, the faster it will
     94  * be handled. The highest priority is 0. The default one is 2.
     95  *
     96  * @return {number} Priority.
     97  */
     98 Request.prototype.getPriority = function() {
     99   return (this.request_.priority !== undefined) ? this.request_.priority : 2;
    100 };
    101 
    102 /**
    103  * Tries to load the image from cache if exists and sends the response.
    104  *
    105  * @param {function()} onSuccess Success callback.
    106  * @param {function()} onFailure Failure callback.
    107  */
    108 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
    109   this.loadFromCache_(
    110       function(data) {  // Found in cache.
    111         this.sendImageData_(data);
    112         onSuccess();
    113       }.bind(this),
    114       onFailure);  // Not found in cache.
    115 };
    116 
    117 /**
    118  * Tries to download the image, resizes and sends the response.
    119  * @param {function()} callback Completion callback.
    120  */
    121 Request.prototype.downloadAndProcess = function(callback) {
    122   if (this.downloadCallback_)
    123     throw new Error('Downloading already started.');
    124 
    125   this.downloadCallback_ = callback;
    126   this.downloadOriginal_(this.onImageLoad_.bind(this),
    127                          this.onImageError_.bind(this));
    128 };
    129 
    130 /**
    131  * Fetches the image from the persistent cache.
    132  *
    133  * @param {function()} onSuccess Success callback.
    134  * @param {function()} onFailure Failure callback.
    135  * @private
    136  */
    137 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
    138   var cacheKey = Cache.createKey(this.request_);
    139 
    140   if (!this.request_.cache) {
    141     // Cache is disabled for this request; therefore, remove it from cache
    142     // if existed.
    143     this.cache_.removeImage(cacheKey);
    144     onFailure();
    145     return;
    146   }
    147 
    148   if (!this.request_.timestamp) {
    149     // Persistent cache is available only when a timestamp is provided.
    150     onFailure();
    151     return;
    152   }
    153 
    154   this.cache_.loadImage(cacheKey,
    155                         this.request_.timestamp,
    156                         onSuccess,
    157                         onFailure);
    158 };
    159 
    160 /**
    161  * Saves the image to the persistent cache.
    162  *
    163  * @param {string} data The image's data.
    164  * @private
    165  */
    166 Request.prototype.saveToCache_ = function(data) {
    167   if (!this.request_.cache || !this.request_.timestamp) {
    168     // Persistent cache is available only when a timestamp is provided.
    169     return;
    170   }
    171 
    172   var cacheKey = Cache.createKey(this.request_);
    173   this.cache_.saveImage(cacheKey,
    174                         data,
    175                         this.request_.timestamp);
    176 };
    177 
    178 /**
    179  * Downloads an image directly or for remote resources using the XmlHttpRequest.
    180  *
    181  * @param {function()} onSuccess Success callback.
    182  * @param {function()} onFailure Failure callback.
    183  * @private
    184  */
    185 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
    186   this.image_.onload = onSuccess;
    187   this.image_.onerror = onFailure;
    188 
    189   // Download data urls directly since they are not supported by XmlHttpRequest.
    190   var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
    191   if (dataUrlMatches) {
    192     this.image_.src = this.request_.url;
    193     this.contentType_ = dataUrlMatches[1];
    194     return;
    195   }
    196 
    197   // Fetch the image via authorized XHR and parse it.
    198   var parseImage = function(contentType, blob) {
    199     var reader = new FileReader();
    200     reader.onerror = onFailure;
    201     reader.onload = function(e) {
    202       this.image_.src = e.target.result;
    203     }.bind(this);
    204 
    205     // Load the data to the image as a data url.
    206     reader.readAsDataURL(blob);
    207   }.bind(this);
    208 
    209   // Request raw data via XHR.
    210   this.xhr_.load(this.request_.url, parseImage, onFailure);
    211 };
    212 
    213 /**
    214  * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
    215  * @constructor
    216  */
    217 function AuthorizedXHR() {
    218   this.xhr_ = null;
    219   this.aborted_ = false;
    220 }
    221 
    222 /**
    223  * Aborts the current request (if running).
    224  */
    225 AuthorizedXHR.prototype.abort = function() {
    226   this.aborted_ = true;
    227   if (this.xhr_)
    228     this.xhr_.abort();
    229 };
    230 
    231 /**
    232  * Loads an image using a OAuth2 token. If it fails, then tries to retry with
    233  * a refreshed OAuth2 token.
    234  *
    235  * @param {string} url URL to the resource to be fetched.
    236  * @param {function(string, Blob}) onSuccess Success callback with the content
    237  *     type and the fetched data.
    238  * @param {function()} onFailure Failure callback.
    239  */
    240 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
    241   this.aborted_ = false;
    242 
    243   // Do not call any callbacks when aborting.
    244   var onMaybeSuccess = function(contentType, response) {
    245     if (!this.aborted_)
    246       onSuccess(contentType, response);
    247   }.bind(this);
    248   var onMaybeFailure = function(opt_code) {
    249     if (!this.aborted_)
    250       onFailure();
    251   }.bind(this);
    252 
    253   // Fetches the access token and makes an authorized call. If refresh is true,
    254   // then forces refreshing the access token.
    255   var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
    256     chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
    257       if (this.aborted_)
    258         return;
    259       if (!token) {
    260         onInnerFailure();
    261         return;
    262       }
    263       this.xhr_ = AuthorizedXHR.load_(
    264           token, url, onInnerSuccess, onInnerFailure);
    265     }.bind(this));
    266   }.bind(this);
    267 
    268   // Refreshes the access token and retries the request.
    269   var maybeRetryCall = function(code) {
    270     if (this.aborted_)
    271       return;
    272     requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
    273   }.bind(this);
    274 
    275   // Do not request a token for local resources, since it is not necessary.
    276   if (/^filesystem:/.test(url)) {
    277     // The query parameter is workaround for
    278     // crbug.com/379678, which force to obtain the latest contents of the image.
    279     var noCacheUrl = url + '?nocache=' + Date.now();
    280     this.xhr_ = AuthorizedXHR.load_(
    281         null,
    282         noCacheUrl,
    283         onMaybeSuccess,
    284         onMaybeFailure);
    285     return;
    286   }
    287 
    288   // Make the request with reusing the current token. If it fails, then retry.
    289   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
    290 };
    291 
    292 /**
    293  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
    294  * If the token is invalid, the request will fail.
    295  *
    296  * @param {?string} token OAuth2 token to be injected to the request. Null for
    297  *     no token.
    298  * @param {string} url URL to the resource to be fetched.
    299  * @param {function(string, Blob}) onSuccess Success callback with the content
    300  *     type and the fetched data.
    301  * @param {function(number=)} onFailure Failure callback with the error code
    302  *     if available.
    303  * @return {AuthorizedXHR} XHR instance.
    304  * @private
    305  */
    306 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
    307   var xhr = new XMLHttpRequest();
    308   xhr.responseType = 'blob';
    309 
    310   xhr.onreadystatechange = function() {
    311     if (xhr.readyState != 4)
    312       return;
    313     if (xhr.status != 200) {
    314       onFailure(xhr.status);
    315       return;
    316     }
    317     var contentType = xhr.getResponseHeader('Content-Type');
    318     onSuccess(contentType, xhr.response);
    319   }.bind(this);
    320 
    321   // Perform a xhr request.
    322   try {
    323     xhr.open('GET', url, true);
    324     if (token)
    325       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
    326     xhr.send();
    327   } catch (e) {
    328     onFailure();
    329   }
    330 
    331   return xhr;
    332 };
    333 
    334 /**
    335  * Sends the resized image via the callback. If the image has been changed,
    336  * then packs the canvas contents, otherwise sends the raw image data.
    337  *
    338  * @param {boolean} imageChanged Whether the image has been changed.
    339  * @private
    340  */
    341 Request.prototype.sendImage_ = function(imageChanged) {
    342   var imageData;
    343   if (!imageChanged) {
    344     // The image hasn't been processed, so the raw data can be directly
    345     // forwarded for speed (no need to encode the image again).
    346     imageData = this.image_.src;
    347   } else {
    348     // The image has been resized or rotated, therefore the canvas has to be
    349     // encoded to get the correct compressed image data.
    350     switch (this.contentType_) {
    351       case 'image/gif':
    352       case 'image/png':
    353       case 'image/svg':
    354       case 'image/bmp':
    355         imageData = this.canvas_.toDataURL('image/png');
    356         break;
    357       case 'image/jpeg':
    358       default:
    359         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
    360     }
    361   }
    362 
    363   // Send and store in the persistent cache.
    364   this.sendImageData_(imageData);
    365   this.saveToCache_(imageData);
    366 };
    367 
    368 /**
    369  * Sends the resized image via the callback.
    370  * @param {string} data Compressed image data.
    371  * @private
    372  */
    373 Request.prototype.sendImageData_ = function(data) {
    374   this.sendResponse_(
    375       {status: 'success', data: data, taskId: this.request_.taskId});
    376 };
    377 
    378 /**
    379  * Handler, when contents are loaded into the image element. Performs resizing
    380  * and finalizes the request process.
    381  *
    382  * @param {function()} callback Completion callback.
    383  * @private
    384  */
    385 Request.prototype.onImageLoad_ = function(callback) {
    386   // Perform processing if the url is not a data url, or if there are some
    387   // operations requested.
    388   if (!this.request_.url.match(/^data/) ||
    389       ImageLoader.shouldProcess(this.image_.width,
    390                                 this.image_.height,
    391                                 this.request_)) {
    392     ImageLoader.resize(this.image_, this.canvas_, this.request_);
    393     this.sendImage_(true);  // Image changed.
    394   } else {
    395     this.sendImage_(false);  // Image not changed.
    396   }
    397   this.cleanup_();
    398   this.downloadCallback_();
    399 };
    400 
    401 /**
    402  * Handler, when loading of the image fails. Sends a failure response and
    403  * finalizes the request process.
    404  *
    405  * @param {function()} callback Completion callback.
    406  * @private
    407  */
    408 Request.prototype.onImageError_ = function(callback) {
    409   this.sendResponse_(
    410       {status: 'error', taskId: this.request_.taskId});
    411   this.cleanup_();
    412   this.downloadCallback_();
    413 };
    414 
    415 /**
    416  * Cancels the request.
    417  */
    418 Request.prototype.cancel = function() {
    419   this.cleanup_();
    420 
    421   // If downloading has started, then call the callback.
    422   if (this.downloadCallback_)
    423     this.downloadCallback_();
    424 };
    425 
    426 /**
    427  * Cleans up memory used by this request.
    428  * @private
    429  */
    430 Request.prototype.cleanup_ = function() {
    431   this.image_.onerror = function() {};
    432   this.image_.onload = function() {};
    433 
    434   // Transparent 1x1 pixel gif, to force garbage collecting.
    435   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
    436       'ABAAEAAAICTAEAOw==';
    437 
    438   this.xhr_.onload = function() {};
    439   this.xhr_.abort();
    440 
    441   // Dispose memory allocated by Canvas.
    442   this.canvas_.width = 0;
    443   this.canvas_.height = 0;
    444 };
    445