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.
7 * cache: (boolean|undefined),
8 * priority: (number|undefined),
10 * timestamp: (number|undefined),
12 * orientation: ImageOrientation,
13 * colorSpace: ?ColorSpace
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.
29 function ImageRequest(id
, cache
, piexLoader
, request
, callback
) {
46 this.piexLoader_
= piexLoader
;
49 * @type {LoadImageRequest}
52 this.request_
= request
;
55 * @type {function(Object)}
58 this.sendResponse_
= callback
;
61 * Temporary image used to download images.
65 this.image_
= new Image();
68 * MIME type of the fetched image.
72 this.contentType_
= null;
75 * Used to download remote images using http:// or https:// protocols.
76 * @type {AuthorizedXHR}
79 this.xhr_
= new AuthorizedXHR();
82 * Temporary canvas used to resize and compress the image.
83 * @type {HTMLCanvasElement}
87 /** @type {HTMLCanvasElement} */ (document
.createElement('canvas'));
90 * @type {CanvasRenderingContext2D}
94 /** @type {CanvasRenderingContext2D} */ (this.canvas_
.getContext('2d'));
97 * Callback to be called once downloading is finished.
101 this.downloadCallback_
= null;
105 * Returns ID of the request.
106 * @return {string} Request ID.
108 ImageRequest
.prototype.getId = function() {
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
) {
131 function(data
, width
, height
) { // Found in cache.
132 this.sendImageData_(data
, width
, height
);
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.
158 ImageRequest
.prototype.loadFromCache_ = function(onSuccess
, onFailure
) {
159 var cacheKey
= ImageCache
.createKey(this.request_
);
162 // Cache key is not provided for the request.
167 if (!this.request_
.cache
) {
168 // Cache is disabled for this request; therefore, remove it from cache
170 this.cache_
.removeImage(cacheKey
);
175 if (!this.request_
.timestamp
) {
176 // Persistent cache is available only when a timestamp is provided.
181 this.cache_
.loadImage(cacheKey
,
182 this.request_
.timestamp
,
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.
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.
201 var cacheKey
= ImageCache
.createKey(this.request_
);
203 // Cache key is not provided for the request.
207 this.cache_
.saveImage(cacheKey
,
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.
221 ImageRequest
.prototype.downloadOriginal_ = function(onSuccess
, onFailure
) {
222 this.image_
.onload = function() {
223 URL
.revokeObjectURL(this.image_
.src
);
226 this.image_
.onerror = function() {
227 URL
.revokeObjectURL(this.image_
.src
);
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];
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
,
246 this.piexLoader_
.load(this.request_
.url
).then(function(data
) {
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
);
260 // Fetch the image via authorized XHR and parse it.
261 var parseImage = function(contentType
, blob
) {
263 this.contentType_
= contentType
;
264 this.image_
.src
= URL
.createObjectURL(blob
);
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.
275 function AuthorizedXHR() {
277 this.aborted_
= false;
281 * A map which is used to estimate content type from extension.
284 AuthorizedXHR
.ExtensionContentTypeMap
= {
294 * Aborts the current request (if running).
296 AuthorizedXHR
.prototype.abort = function() {
297 this.aborted_
= true;
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.
319 contentType
= AuthorizedXHR
.ExtensionContentTypeMap
[
320 this.extractExtension_(url
)];
324 onSuccess(contentType
, response
);
327 var onMaybeFailure
= /** @type {function(number=)} */ (
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
) {
343 this.xhr_
= AuthorizedXHR
.load_(
344 token
, url
, onInnerSuccess
, onInnerFailure
);
348 // Refreshes the access token and retries the request.
349 var maybeRetryCall = function(code
) {
352 requestTokenAndCall(true, onMaybeSuccess
, onMaybeFailure
);
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_(
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
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
393 * @return {XMLHttpRequest} XHR instance.
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)
403 if (xhr
.status
!= 200) {
404 onFailure(xhr
.status
);
407 var contentType
= xhr
.getResponseHeader('Content-Type');
408 onSuccess(contentType
, /** @type {Blob} */ (xhr
.response
));
411 // Perform a xhr request.
413 xhr
.open('GET', url
, true);
415 xhr
.setRequestHeader('Authorization', 'Bearer ' + token
);
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.
431 ImageRequest
.prototype.sendImage_ = function(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
;
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_
) {
452 imageData
= this.canvas_
.toDataURL('image/png');
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.
472 ImageRequest
.prototype.sendImageData_ = function(data
, width
, height
) {
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.
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
,
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.
496 this.sendImage_(false); // Image not changed.
499 this.downloadCallback_();
503 * Handler, when loading of the image fails. Sends a failure response and
504 * finalizes the request process.
507 ImageRequest
.prototype.onImageError_ = function() {
509 {status
: 'error', taskId
: this.request_
.taskId
});
511 this.downloadCallback_();
515 * Cancels the request.
517 ImageRequest
.prototype.cancel = function() {
520 // If downloading has started, then call the callback.
521 if (this.downloadCallback_
)
522 this.downloadCallback_();
526 * Cleans up memory used by this request.
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() {};
540 // Dispose memory allocated by Canvas.
541 this.canvas_
.width
= 0;
542 this.canvas_
.height
= 0;