Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / file_manager / image_loader / request.js
blobe8cd3a5e742470fd98033873bfa7e9332162b674
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 {Cache} 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 Request(id, cache, piexLoader, request, callback) {
30   /**
31    * @type {string}
32    * @private
33    */
34   this.id_ = id;
36   /**
37    * @type {Cache}
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 Request.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 Request.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 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
129   this.loadFromCache_(
130       function(data, width, height) {  // Found in cache.
131         this.sendImageData_(data, width, height);
132         onSuccess();
133       }.bind(this),
134       onFailure);  // Not found in cache.
138  * Tries to download the image, resizes and sends the response.
139  * @param {function()} callback Completion callback.
140  */
141 Request.prototype.downloadAndProcess = function(callback) {
142   if (this.downloadCallback_)
143     throw new Error('Downloading already started.');
145   this.downloadCallback_ = callback;
146   this.downloadOriginal_(this.onImageLoad_.bind(this),
147                          this.onImageError_.bind(this));
151  * Fetches the image from the persistent cache.
153  * @param {function(string, number, number)} onSuccess Success callback.
154  * @param {function()} onFailure Failure callback.
155  * @private
156  */
157 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
158   var cacheKey = Cache.createKey(this.request_);
160   if (!cacheKey) {
161     // Cache key is not provided for the request.
162     onFailure();
163     return;
164   }
166   if (!this.request_.cache) {
167     // Cache is disabled for this request; therefore, remove it from cache
168     // if existed.
169     this.cache_.removeImage(cacheKey);
170     onFailure();
171     return;
172   }
174   if (!this.request_.timestamp) {
175     // Persistent cache is available only when a timestamp is provided.
176     onFailure();
177     return;
178   }
180   this.cache_.loadImage(cacheKey,
181                         this.request_.timestamp,
182                         onSuccess,
183                         onFailure);
187  * Saves the image to the persistent cache.
189  * @param {string} data The image's data.
190  * @param {number} width Image width.
191  * @param {number} height Image height.
192  * @private
193  */
194 Request.prototype.saveToCache_ = function(data, width, height) {
195   if (!this.request_.cache || !this.request_.timestamp) {
196     // Persistent cache is available only when a timestamp is provided.
197     return;
198   }
200   var cacheKey = Cache.createKey(this.request_);
201   if (!cacheKey) {
202     // Cache key is not provided for the request.
203     return;
204   }
206   this.cache_.saveImage(cacheKey,
207                         data,
208                         width,
209                         height,
210                         this.request_.timestamp);
214  * Downloads an image directly or for remote resources using the XmlHttpRequest.
216  * @param {function()} onSuccess Success callback.
217  * @param {function()} onFailure Failure callback.
218  * @private
219  */
220 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
221   this.image_.onload = function() {
222     URL.revokeObjectURL(this.image_.src);
223     onSuccess();
224   }.bind(this);
225   this.image_.onerror = function() {
226     URL.revokeObjectURL(this.image_.src);
227     onFailure();
228   }.bind(this);
230   // Download data urls directly since they are not supported by XmlHttpRequest.
231   var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
232   if (dataUrlMatches) {
233     this.image_.src = this.request_.url;
234     this.contentType_ = dataUrlMatches[1];
235     return;
236   }
238   // Load RAW images by using Piex loader instead of XHR.
239   if (FileType.getTypeForName(this.request_.url).type === 'raw') {
240     this.piexLoader_.load(this.request_.url).then(function(data) {
241       var blob = new Blob([data.thumbnail], {type: 'image/jpeg'});
242       var url = URL.createObjectURL(blob);
243       this.image_.src = url;
244       this.request_.orientation = data.orientation;
245       this.request_.colorSpace = data.colorSpace;
246     }.bind(this), function(error) {
247       console.error('PiexLoaderError: ', error);
248       onFailure();
249     });
250     return;
251   }
253   // Fetch the image via authorized XHR and parse it.
254   var parseImage = function(contentType, blob) {
255     if (contentType)
256       this.contentType_ = contentType;
257     this.image_.src = URL.createObjectURL(blob);
258   }.bind(this);
260   // Request raw data via XHR.
261   this.xhr_.load(this.request_.url, parseImage, onFailure);
265  * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
266  * @constructor
267  */
268 function AuthorizedXHR() {
269   this.xhr_ = null;
270   this.aborted_ = false;
274  * A map which is used to estimate content type from extension.
275  * @enum {string}
276  */
277 AuthorizedXHR.ExtensionContentTypeMap = {
278   gif: 'image/gif',
279   png: 'image/png',
280   svg: 'image/svg',
281   bmp: 'image/bmp',
282   jpg: 'image/jpeg',
283   jpeg: 'image/jpeg'
287  * Aborts the current request (if running).
288  */
289 AuthorizedXHR.prototype.abort = function() {
290   this.aborted_ = true;
291   if (this.xhr_)
292     this.xhr_.abort();
296  * Loads an image using a OAuth2 token. If it fails, then tries to retry with
297  * a refreshed OAuth2 token.
299  * @param {string} url URL to the resource to be fetched.
300  * @param {function(string, Blob)} onSuccess Success callback with the content
301  *     type and the fetched data.
302  * @param {function()} onFailure Failure callback.
303  */
304 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
305   this.aborted_ = false;
307   // Do not call any callbacks when aborting.
308   var onMaybeSuccess = /** @type {function(string, Blob)} */ (
309       function(contentType, response) {
310         // When content type is not available, try to estimate it from url.
311         if (!contentType) {
312           contentType = AuthorizedXHR.ExtensionContentTypeMap[
313               this.extractExtension_(url)];
314         }
316         if (!this.aborted_)
317           onSuccess(contentType, response);
318       }.bind(this));
320   var onMaybeFailure = /** @type {function(number=)} */ (
321       function(opt_code) {
322         if (!this.aborted_)
323           onFailure();
324       }.bind(this));
326   // Fetches the access token and makes an authorized call. If refresh is true,
327   // then forces refreshing the access token.
328   var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
329     chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
330       if (this.aborted_)
331         return;
332       if (!token) {
333         onInnerFailure();
334         return;
335       }
336       this.xhr_ = AuthorizedXHR.load_(
337           token, url, onInnerSuccess, onInnerFailure);
338     }.bind(this));
339   }.bind(this);
341   // Refreshes the access token and retries the request.
342   var maybeRetryCall = function(code) {
343     if (this.aborted_)
344       return;
345     requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
346   }.bind(this);
348   // Do not request a token for local resources, since it is not necessary.
349   if (/^filesystem:/.test(url)) {
350     // The query parameter is workaround for
351     // crbug.com/379678, which force to obtain the latest contents of the image.
352     var noCacheUrl = url + '?nocache=' + Date.now();
353     this.xhr_ = AuthorizedXHR.load_(
354         null,
355         noCacheUrl,
356         onMaybeSuccess,
357         onMaybeFailure);
358     return;
359   }
361   // Make the request with reusing the current token. If it fails, then retry.
362   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
366  * Extracts extension from url.
367  * @param {string} url Url.
368  * @return {string} Extracted extensiion, e.g. png.
369  */
370 AuthorizedXHR.prototype.extractExtension_ = function(url) {
371   var result = (/\.([a-zA-Z]+)$/i).exec(url);
372   return result ? result[1] : '';
376  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
377  * If the token is invalid, the request will fail.
379  * @param {?string} token OAuth2 token to be injected to the request. Null for
380  *     no token.
381  * @param {string} url URL to the resource to be fetched.
382  * @param {function(string, Blob)} onSuccess Success callback with the content
383  *     type and the fetched data.
384  * @param {function(number=)} onFailure Failure callback with the error code
385  *     if available.
386  * @return {XMLHttpRequest} XHR instance.
387  * @private
388  */
389 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
390   var xhr = new XMLHttpRequest();
391   xhr.responseType = 'blob';
393   xhr.onreadystatechange = function() {
394     if (xhr.readyState != 4)
395       return;
396     if (xhr.status != 200) {
397       onFailure(xhr.status);
398       return;
399     }
400     var contentType = xhr.getResponseHeader('Content-Type');
401     onSuccess(contentType, /** @type {Blob} */ (xhr.response));
402   }.bind(this);
404   // Perform a xhr request.
405   try {
406     xhr.open('GET', url, true);
407     if (token)
408       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
409     xhr.send();
410   } catch (e) {
411     onFailure();
412   }
414   return xhr;
418  * Sends the resized image via the callback. If the image has been changed,
419  * then packs the canvas contents, otherwise sends the raw image data.
421  * @param {boolean} imageChanged Whether the image has been changed.
422  * @private
423  */
424 Request.prototype.sendImage_ = function(imageChanged) {
425   var imageData;
426   var width;
427   var height;
428   if (!imageChanged) {
429     // The image hasn't been processed, so the raw data can be directly
430     // forwarded for speed (no need to encode the image again).
431     imageData = this.image_.src;
432     width = this.image_.width;
433     height = this.image_.height;
434   } else {
435     // The image has been resized or rotated, therefore the canvas has to be
436     // encoded to get the correct compressed image data.
437     width = this.canvas_.width;
438     height = this.canvas_.height;
440     switch (this.contentType_) {
441       case 'image/gif':
442       case 'image/png':
443       case 'image/svg':
444       case 'image/bmp':
445         imageData = this.canvas_.toDataURL('image/png');
446         break;
447       case 'image/jpeg':
448       default:
449         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
450     }
451   }
453   // Send and store in the persistent cache.
454   this.sendImageData_(imageData, width, height);
455   this.saveToCache_(imageData, width, height);
459  * Sends the resized image via the callback.
460  * @param {string} data Compressed image data.
461  * @param {number} width Width.
462  * @param {number} height Height.
463  * @private
464  */
465 Request.prototype.sendImageData_ = function(data, width, height) {
466   this.sendResponse_({
467     status: 'success', data: data, width: width, height: height,
468     taskId: this.request_.taskId
469   });
473  * Handler, when contents are loaded into the image element. Performs resizing
474  * and finalizes the request process.
475  * @private
476  */
477 Request.prototype.onImageLoad_ = function() {
478   // Perform processing if the url is not a data url, or if there are some
479   // operations requested.
480   if (!this.request_.url.match(/^data/) ||
481       ImageLoader.shouldProcess(this.image_.width,
482                                 this.image_.height,
483                                 this.request_)) {
484     ImageLoader.resize(this.image_, this.canvas_, this.request_);
485     ImageLoader.convertColorSpace(
486         this.canvas_, this.request_.colorSpace || ColorSpace.SRGB);
487     this.sendImage_(true);  // Image changed.
488   } else {
489     this.sendImage_(false);  // Image not changed.
490   }
491   this.cleanup_();
492   this.downloadCallback_();
496  * Handler, when loading of the image fails. Sends a failure response and
497  * finalizes the request process.
498  * @private
499  */
500 Request.prototype.onImageError_ = function() {
501   this.sendResponse_(
502       {status: 'error', taskId: this.request_.taskId});
503   this.cleanup_();
504   this.downloadCallback_();
508  * Cancels the request.
509  */
510 Request.prototype.cancel = function() {
511   this.cleanup_();
513   // If downloading has started, then call the callback.
514   if (this.downloadCallback_)
515     this.downloadCallback_();
519  * Cleans up memory used by this request.
520  * @private
521  */
522 Request.prototype.cleanup_ = function() {
523   this.image_.onerror = function() {};
524   this.image_.onload = function() {};
526   // Transparent 1x1 pixel gif, to force garbage collecting.
527   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
528       'ABAAEAAAICTAEAOw==';
530   this.xhr_.onload = function() {};
531   this.xhr_.abort();
533   // Dispose memory allocated by Canvas.
534   this.canvas_.width = 0;
535   this.canvas_.height = 0;