Snap pinch zoom gestures near the screen edge.
[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 * }}
16 var LoadImageRequest;
18 /**
19 * Creates and starts downloading and then resizing of the image. Finally,
20 * returns the image using the callback.
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
29 function ImageRequest(id, cache, piexLoader, request, callback) {
30 /**
31 * @type {string}
32 * @private
34 this.id_ = id;
36 /**
37 * @type {ImageCache}
38 * @private
40 this.cache_ = cache;
42 /**
43 * @type {!PiexLoader}
44 * @private
46 this.piexLoader_ = piexLoader;
48 /**
49 * @type {LoadImageRequest}
50 * @private
52 this.request_ = request;
54 /**
55 * @type {function(Object)}
56 * @private
58 this.sendResponse_ = callback;
60 /**
61 * Temporary image used to download images.
62 * @type {Image}
63 * @private
65 this.image_ = new Image();
67 /**
68 * MIME type of the fetched image.
69 * @type {?string}
70 * @private
72 this.contentType_ = null;
74 /**
75 * Used to download remote images using http:// or https:// protocols.
76 * @type {AuthorizedXHR}
77 * @private
79 this.xhr_ = new AuthorizedXHR();
81 /**
82 * Temporary canvas used to resize and compress the image.
83 * @type {HTMLCanvasElement}
84 * @private
86 this.canvas_ =
87 /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
89 /**
90 * @type {CanvasRenderingContext2D}
91 * @private
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
101 this.downloadCallback_ = null;
105 * Returns ID of the request.
106 * @return {string} Request ID.
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.
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.
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.
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
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;
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;
175 if (!this.request_.timestamp) {
176 // Persistent cache is available only when a timestamp is provided.
177 onFailure();
178 return;
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
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;
201 var cacheKey = ImageCache.createKey(this.request_);
202 if (!cacheKey) {
203 // Cache key is not provided for the request.
204 return;
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
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;
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();
257 return;
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
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}
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).
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.
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)];
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;
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;
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.
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
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;
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();
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
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);
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
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
480 * Handler, when contents are loaded into the image element. Performs resizing
481 * and finalizes the request process.
482 * @private
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.
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
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.
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
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;