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 {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.
29 function Request(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 Request.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 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.
128 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
130 function(data, width, height) { // Found in cache.
131 this.sendImageData_(data, width, height);
134 onFailure); // Not found in cache.
138 * Tries to download the image, resizes and sends the response.
139 * @param {function()} callback Completion callback.
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.
157 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
158 var cacheKey = Cache.createKey(this.request_);
161 // Cache key is not provided for the request.
166 if (!this.request_.cache) {
167 // Cache is disabled for this request; therefore, remove it from cache
169 this.cache_.removeImage(cacheKey);
174 if (!this.request_.timestamp) {
175 // Persistent cache is available only when a timestamp is provided.
180 this.cache_.loadImage(cacheKey,
181 this.request_.timestamp,
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.
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.
200 var cacheKey = Cache.createKey(this.request_);
202 // Cache key is not provided for the request.
206 this.cache_.saveImage(cacheKey,
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.
220 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
221 this.image_.onload = function() {
222 URL.revokeObjectURL(this.image_.src);
225 this.image_.onerror = function() {
226 URL.revokeObjectURL(this.image_.src);
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];
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);
253 // Fetch the image via authorized XHR and parse it.
254 var parseImage = function(contentType, blob) {
256 this.contentType_ = contentType;
257 this.image_.src = URL.createObjectURL(blob);
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.
268 function AuthorizedXHR() {
270 this.aborted_ = false;
274 * A map which is used to estimate content type from extension.
277 AuthorizedXHR.ExtensionContentTypeMap = {
287 * Aborts the current request (if running).
289 AuthorizedXHR.prototype.abort = function() {
290 this.aborted_ = true;
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.
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.
312 contentType = AuthorizedXHR.ExtensionContentTypeMap[
313 this.extractExtension_(url)];
317 onSuccess(contentType, response);
320 var onMaybeFailure = /** @type {function(number=)} */ (
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) {
336 this.xhr_ = AuthorizedXHR.load_(
337 token, url, onInnerSuccess, onInnerFailure);
341 // Refreshes the access token and retries the request.
342 var maybeRetryCall = function(code) {
345 requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
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_(
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.
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
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
386 * @return {XMLHttpRequest} XHR instance.
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)
396 if (xhr.status != 200) {
397 onFailure(xhr.status);
400 var contentType = xhr.getResponseHeader('Content-Type');
401 onSuccess(contentType, /** @type {Blob} */ (xhr.response));
404 // Perform a xhr request.
406 xhr.open('GET', url, true);
408 xhr.setRequestHeader('Authorization', 'Bearer ' + token);
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.
424 Request.prototype.sendImage_ = function(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;
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_) {
445 imageData = this.canvas_.toDataURL('image/png');
449 imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
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.
465 Request.prototype.sendImageData_ = function(data, width, height) {
467 status: 'success', data: data, width: width, height: height,
468 taskId: this.request_.taskId
473 * Handler, when contents are loaded into the image element. Performs resizing
474 * and finalizes the request process.
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,
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.
489 this.sendImage_(false); // Image not changed.
492 this.downloadCallback_();
496 * Handler, when loading of the image fails. Sends a failure response and
497 * finalizes the request process.
500 Request.prototype.onImageError_ = function() {
502 {status: 'error', taskId: this.request_.taskId});
504 this.downloadCallback_();
508 * Cancels the request.
510 Request.prototype.cancel = function() {
513 // If downloading has started, then call the callback.
514 if (this.downloadCallback_)
515 this.downloadCallback_();
519 * Cleans up memory used by this request.
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() {};
533 // Dispose memory allocated by Canvas.
534 this.canvas_.width = 0;
535 this.canvas_.height = 0;