Roll src/third_party/WebKit a4fcdf5:1a119ff (svn 197351:197359)
[chromium-blink-merge.git] / ui / file_manager / image_loader / request.js
blob0b23aec9a4955378eb17396038b2036662003b16
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.
5 /**
6  * @typedef {{
7  *   cache: (boolean|undefined),
8  *   priority: (number|undefined),
9  *   taskId: number,
10  *   timestamp: (number|undefined),
11  *   url: string,
12  *   orientation: ImageOrientation,
13  *   colorSpace: ?ColorSpace
14  * }}
15  */
16 var LoadImageRequest;
18 /**
19  * Creates and starts downloading and then resizing of the image. Finally,
20  * returns the image using the callback.
21  *
22  * @param {string} id Request ID.
23  * @param {ImageCache} cache Cache object.
24  * @param {!PiexLoader} piexLoader Piex loader for RAW file.
25  * @param {LoadImageRequest} request Request message as a hash array.
26  * @param {function(Object)} callback Callback used to send the response.
27  * @constructor
28  */
29 function ImageRequest(id, cache, piexLoader, request, callback) {
30   /**
31    * @type {string}
32    * @private
33    */
34   this.id_ = id;
36   /**
37    * @type {ImageCache}
38    * @private
39    */
40   this.cache_ = cache;
42   /**
43    * @type {!PiexLoader}
44    * @private
45    */
46   this.piexLoader_ = piexLoader;
48   /**
49    * @type {LoadImageRequest}
50    * @private
51    */
52   this.request_ = request;
54   /**
55    * @type {function(Object)}
56    * @private
57    */
58   this.sendResponse_ = callback;
60   /**
61    * Temporary image used to download images.
62    * @type {Image}
63    * @private
64    */
65   this.image_ = new Image();
67   /**
68    * MIME type of the fetched image.
69    * @type {?string}
70    * @private
71    */
72   this.contentType_ = null;
74   /**
75    * Used to download remote images using http:// or https:// protocols.
76    * @type {AuthorizedXHR}
77    * @private
78    */
79   this.xhr_ = new AuthorizedXHR();
81   /**
82    * Temporary canvas used to resize and compress the image.
83    * @type {HTMLCanvasElement}
84    * @private
85    */
86   this.canvas_ =
87       /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
89   /**
90    * @type {CanvasRenderingContext2D}
91    * @private
92    */
93   this.context_ =
94       /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d'));
96   /**
97    * Callback to be called once downloading is finished.
98    * @type {?function()}
99    * @private
100    */
101   this.downloadCallback_ = null;
105  * Returns ID of the request.
106  * @return {string} Request ID.
107  */
108 ImageRequest.prototype.getId = function() {
109   return this.id_;
113  * Returns priority of the request. The higher priority, the faster it will
114  * be handled. The highest priority is 0. The default one is 2.
116  * @return {number} Priority.
117  */
118 ImageRequest.prototype.getPriority = function() {
119   return (this.request_.priority !== undefined) ? this.request_.priority : 2;
123  * Tries to load the image from cache if exists and sends the response.
125  * @param {function()} onSuccess Success callback.
126  * @param {function()} onFailure Failure callback.
127  */
128 ImageRequest.prototype.loadFromCacheAndProcess = function(
129     onSuccess, onFailure) {
130   this.loadFromCache_(
131       function(data, width, height) {  // Found in cache.
132         this.sendImageData_(data, width, height);
133         onSuccess();
134       }.bind(this),
135       onFailure);  // Not found in cache.
139  * Tries to download the image, resizes and sends the response.
140  * @param {function()} callback Completion callback.
141  */
142 ImageRequest.prototype.downloadAndProcess = function(callback) {
143   if (this.downloadCallback_)
144     throw new Error('Downloading already started.');
146   this.downloadCallback_ = callback;
147   this.downloadOriginal_(this.onImageLoad_.bind(this),
148                          this.onImageError_.bind(this));
152  * Fetches the image from the persistent cache.
154  * @param {function(string, number, number)} onSuccess Success callback.
155  * @param {function()} onFailure Failure callback.
156  * @private
157  */
158 ImageRequest.prototype.loadFromCache_ = function(onSuccess, onFailure) {
159   var cacheKey = ImageCache.createKey(this.request_);
161   if (!cacheKey) {
162     // Cache key is not provided for the request.
163     onFailure();
164     return;
165   }
167   if (!this.request_.cache) {
168     // Cache is disabled for this request; therefore, remove it from cache
169     // if existed.
170     this.cache_.removeImage(cacheKey);
171     onFailure();
172     return;
173   }
175   if (!this.request_.timestamp) {
176     // Persistent cache is available only when a timestamp is provided.
177     onFailure();
178     return;
179   }
181   this.cache_.loadImage(cacheKey,
182                         this.request_.timestamp,
183                         onSuccess,
184                         onFailure);
188  * Saves the image to the persistent cache.
190  * @param {string} data The image's data.
191  * @param {number} width Image width.
192  * @param {number} height Image height.
193  * @private
194  */
195 ImageRequest.prototype.saveToCache_ = function(data, width, height) {
196   if (!this.request_.cache || !this.request_.timestamp) {
197     // Persistent cache is available only when a timestamp is provided.
198     return;
199   }
201   var cacheKey = ImageCache.createKey(this.request_);
202   if (!cacheKey) {
203     // Cache key is not provided for the request.
204     return;
205   }
207   this.cache_.saveImage(cacheKey,
208                         data,
209                         width,
210                         height,
211                         this.request_.timestamp);
215  * Downloads an image directly or for remote resources using the XmlHttpRequest.
217  * @param {function()} onSuccess Success callback.
218  * @param {function()} onFailure Failure callback.
219  * @private
220  */
221 ImageRequest.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
222   this.image_.onload = function() {
223     URL.revokeObjectURL(this.image_.src);
224     onSuccess();
225   }.bind(this);
226   this.image_.onerror = function() {
227     URL.revokeObjectURL(this.image_.src);
228     onFailure();
229   }.bind(this);
231   // Download data urls directly since they are not supported by XmlHttpRequest.
232   var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
233   if (dataUrlMatches) {
234     this.image_.src = this.request_.url;
235     this.contentType_ = dataUrlMatches[1];
236     return;
237   }
239   // Load RAW images by using Piex loader instead of XHR.
240   var fileType = FileType.getTypeForName(this.request_.url);
241   if (fileType.type === 'raw') {
242     var timer = metrics.getTracker().startTiming(
243         metrics.Categories.INTERNALS,
244         metrics.timing.Variables.EXTRACT_THUMBNAIL_FROM_RAW,
245         fileType.subtype);
246     this.piexLoader_.load(this.request_.url).then(function(data) {
247       timer.send();
248       var blob = new Blob([data.thumbnail], {type: 'image/jpeg'});
249       var url = URL.createObjectURL(blob);
250       this.image_.src = url;
251       this.request_.orientation = data.orientation;
252       this.request_.colorSpace = data.colorSpace;
253     }.bind(this), function(error) {
254       console.error('PiexLoaderError: ', error);
255       onFailure();
256     });
257     return;
258   }
260   // Fetch the image via authorized XHR and parse it.
261   var parseImage = function(contentType, blob) {
262     if (contentType)
263       this.contentType_ = contentType;
264     this.image_.src = URL.createObjectURL(blob);
265   }.bind(this);
267   // Request raw data via XHR.
268   this.xhr_.load(this.request_.url, parseImage, onFailure);
272  * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
273  * @constructor
274  */
275 function AuthorizedXHR() {
276   this.xhr_ = null;
277   this.aborted_ = false;
281  * A map which is used to estimate content type from extension.
282  * @enum {string}
283  */
284 AuthorizedXHR.ExtensionContentTypeMap = {
285   gif: 'image/gif',
286   png: 'image/png',
287   svg: 'image/svg',
288   bmp: 'image/bmp',
289   jpg: 'image/jpeg',
290   jpeg: 'image/jpeg'
294  * Aborts the current request (if running).
295  */
296 AuthorizedXHR.prototype.abort = function() {
297   this.aborted_ = true;
298   if (this.xhr_)
299     this.xhr_.abort();
303  * Loads an image using a OAuth2 token. If it fails, then tries to retry with
304  * a refreshed OAuth2 token.
306  * @param {string} url URL to the resource to be fetched.
307  * @param {function(string, Blob)} onSuccess Success callback with the content
308  *     type and the fetched data.
309  * @param {function()} onFailure Failure callback.
310  */
311 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
312   this.aborted_ = false;
314   // Do not call any callbacks when aborting.
315   var onMaybeSuccess = /** @type {function(string, Blob)} */ (
316       function(contentType, response) {
317         // When content type is not available, try to estimate it from url.
318         if (!contentType) {
319           contentType = AuthorizedXHR.ExtensionContentTypeMap[
320               this.extractExtension_(url)];
321         }
323         if (!this.aborted_)
324           onSuccess(contentType, response);
325       }.bind(this));
327   var onMaybeFailure = /** @type {function(number=)} */ (
328       function(opt_code) {
329         if (!this.aborted_)
330           onFailure();
331       }.bind(this));
333   // Fetches the access token and makes an authorized call. If refresh is true,
334   // then forces refreshing the access token.
335   var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
336     chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
337       if (this.aborted_)
338         return;
339       if (!token) {
340         onInnerFailure();
341         return;
342       }
343       this.xhr_ = AuthorizedXHR.load_(
344           token, url, onInnerSuccess, onInnerFailure);
345     }.bind(this));
346   }.bind(this);
348   // Refreshes the access token and retries the request.
349   var maybeRetryCall = function(code) {
350     if (this.aborted_)
351       return;
352     requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
353   }.bind(this);
355   // Do not request a token for local resources, since it is not necessary.
356   if (/^filesystem:/.test(url)) {
357     // The query parameter is workaround for
358     // crbug.com/379678, which force to obtain the latest contents of the image.
359     var noCacheUrl = url + '?nocache=' + Date.now();
360     this.xhr_ = AuthorizedXHR.load_(
361         null,
362         noCacheUrl,
363         onMaybeSuccess,
364         onMaybeFailure);
365     return;
366   }
368   // Make the request with reusing the current token. If it fails, then retry.
369   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
373  * Extracts extension from url.
374  * @param {string} url Url.
375  * @return {string} Extracted extensiion, e.g. png.
376  */
377 AuthorizedXHR.prototype.extractExtension_ = function(url) {
378   var result = (/\.([a-zA-Z]+)$/i).exec(url);
379   return result ? result[1] : '';
383  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
384  * If the token is invalid, the request will fail.
386  * @param {?string} token OAuth2 token to be injected to the request. Null for
387  *     no token.
388  * @param {string} url URL to the resource to be fetched.
389  * @param {function(string, Blob)} onSuccess Success callback with the content
390  *     type and the fetched data.
391  * @param {function(number=)} onFailure Failure callback with the error code
392  *     if available.
393  * @return {XMLHttpRequest} XHR instance.
394  * @private
395  */
396 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
397   var xhr = new XMLHttpRequest();
398   xhr.responseType = 'blob';
400   xhr.onreadystatechange = function() {
401     if (xhr.readyState != 4)
402       return;
403     if (xhr.status != 200) {
404       onFailure(xhr.status);
405       return;
406     }
407     var contentType = xhr.getResponseHeader('Content-Type');
408     onSuccess(contentType, /** @type {Blob} */ (xhr.response));
409   }.bind(this);
411   // Perform a xhr request.
412   try {
413     xhr.open('GET', url, true);
414     if (token)
415       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
416     xhr.send();
417   } catch (e) {
418     onFailure();
419   }
421   return xhr;
425  * Sends the resized image via the callback. If the image has been changed,
426  * then packs the canvas contents, otherwise sends the raw image data.
428  * @param {boolean} imageChanged Whether the image has been changed.
429  * @private
430  */
431 ImageRequest.prototype.sendImage_ = function(imageChanged) {
432   var imageData;
433   var width;
434   var height;
435   if (!imageChanged) {
436     // The image hasn't been processed, so the raw data can be directly
437     // forwarded for speed (no need to encode the image again).
438     imageData = this.image_.src;
439     width = this.image_.width;
440     height = this.image_.height;
441   } else {
442     // The image has been resized or rotated, therefore the canvas has to be
443     // encoded to get the correct compressed image data.
444     width = this.canvas_.width;
445     height = this.canvas_.height;
447     switch (this.contentType_) {
448       case 'image/gif':
449       case 'image/png':
450       case 'image/svg':
451       case 'image/bmp':
452         imageData = this.canvas_.toDataURL('image/png');
453         break;
454       case 'image/jpeg':
455       default:
456         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
457     }
458   }
460   // Send and store in the persistent cache.
461   this.sendImageData_(imageData, width, height);
462   this.saveToCache_(imageData, width, height);
466  * Sends the resized image via the callback.
467  * @param {string} data Compressed image data.
468  * @param {number} width Width.
469  * @param {number} height Height.
470  * @private
471  */
472 ImageRequest.prototype.sendImageData_ = function(data, width, height) {
473   this.sendResponse_({
474     status: 'success', data: data, width: width, height: height,
475     taskId: this.request_.taskId
476   });
480  * Handler, when contents are loaded into the image element. Performs resizing
481  * and finalizes the request process.
482  * @private
483  */
484 ImageRequest.prototype.onImageLoad_ = function() {
485   // Perform processing if the url is not a data url, or if there are some
486   // operations requested.
487   if (!this.request_.url.match(/^data/) ||
488       ImageLoader.shouldProcess(this.image_.width,
489                                 this.image_.height,
490                                 this.request_)) {
491     ImageLoader.resizeAndCrop(this.image_, this.canvas_, this.request_);
492     ImageLoader.convertColorSpace(
493         this.canvas_, this.request_.colorSpace || ColorSpace.SRGB);
494     this.sendImage_(true);  // Image changed.
495   } else {
496     this.sendImage_(false);  // Image not changed.
497   }
498   this.cleanup_();
499   this.downloadCallback_();
503  * Handler, when loading of the image fails. Sends a failure response and
504  * finalizes the request process.
505  * @private
506  */
507 ImageRequest.prototype.onImageError_ = function() {
508   this.sendResponse_(
509       {status: 'error', taskId: this.request_.taskId});
510   this.cleanup_();
511   this.downloadCallback_();
515  * Cancels the request.
516  */
517 ImageRequest.prototype.cancel = function() {
518   this.cleanup_();
520   // If downloading has started, then call the callback.
521   if (this.downloadCallback_)
522     this.downloadCallback_();
526  * Cleans up memory used by this request.
527  * @private
528  */
529 ImageRequest.prototype.cleanup_ = function() {
530   this.image_.onerror = function() {};
531   this.image_.onload = function() {};
533   // Transparent 1x1 pixel gif, to force garbage collecting.
534   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
535       'ABAAEAAAICTAEAOw==';
537   this.xhr_.onload = function() {};
538   this.xhr_.abort();
540   // Dispose memory allocated by Canvas.
541   this.canvas_.width = 0;
542   this.canvas_.height = 0;