|  | // Copyright 2013 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | /** | 
|  | * @typedef {{ | 
|  | *   cache: (boolean|undefined), | 
|  | *   priority: (number|undefined), | 
|  | *   taskId: number, | 
|  | *   timestamp: (number|undefined), | 
|  | *   url: string, | 
|  | *   orientation: !ImageOrientation, | 
|  | *   colorSpace: ?ColorSpace | 
|  | * }} | 
|  | */ | 
|  | var LoadImageRequest; | 
|  |  | 
|  | /** | 
|  | * Creates and starts downloading and then resizing of the image. Finally, | 
|  | * returns the image using the callback. | 
|  | * | 
|  | * @param {string} id Request ID. | 
|  | * @param {ImageCache} cache Cache object. | 
|  | * @param {!PiexLoader} piexLoader Piex loader for RAW file. | 
|  | * @param {LoadImageRequest} request Request message as a hash array. | 
|  | * @param {function(Object)} callback Callback used to send the response. | 
|  | * @constructor | 
|  | */ | 
|  | function ImageRequest(id, cache, piexLoader, request, callback) { | 
|  | /** | 
|  | * @type {string} | 
|  | * @private | 
|  | */ | 
|  | this.id_ = id; | 
|  |  | 
|  | /** | 
|  | * @type {ImageCache} | 
|  | * @private | 
|  | */ | 
|  | this.cache_ = cache; | 
|  |  | 
|  | /** | 
|  | * @type {!PiexLoader} | 
|  | * @private | 
|  | */ | 
|  | this.piexLoader_ = piexLoader; | 
|  |  | 
|  | /** | 
|  | * @type {LoadImageRequest} | 
|  | * @private | 
|  | */ | 
|  | this.request_ = request; | 
|  |  | 
|  | /** | 
|  | * @type {function(Object)} | 
|  | * @private | 
|  | */ | 
|  | this.sendResponse_ = callback; | 
|  |  | 
|  | /** | 
|  | * Temporary image used to download images. | 
|  | * @type {Image} | 
|  | * @private | 
|  | */ | 
|  | this.image_ = new Image(); | 
|  |  | 
|  | /** | 
|  | * MIME type of the fetched image. | 
|  | * @type {?string} | 
|  | * @private | 
|  | */ | 
|  | this.contentType_ = null; | 
|  |  | 
|  | /** | 
|  | * Used to download remote images using http:// or https:// protocols. | 
|  | * @type {AuthorizedXHR} | 
|  | * @private | 
|  | */ | 
|  | this.xhr_ = new AuthorizedXHR(); | 
|  |  | 
|  | /** | 
|  | * Temporary canvas used to resize and compress the image. | 
|  | * @type {HTMLCanvasElement} | 
|  | * @private | 
|  | */ | 
|  | this.canvas_ = | 
|  | /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); | 
|  |  | 
|  | /** | 
|  | * @type {CanvasRenderingContext2D} | 
|  | * @private | 
|  | */ | 
|  | this.context_ = | 
|  | /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d')); | 
|  |  | 
|  | /** | 
|  | * Callback to be called once downloading is finished. | 
|  | * @type {?function()} | 
|  | * @private | 
|  | */ | 
|  | this.downloadCallback_ = null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns ID of the request. | 
|  | * @return {string} Request ID. | 
|  | */ | 
|  | ImageRequest.prototype.getId = function() { | 
|  | return this.id_; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Returns priority of the request. The higher priority, the faster it will | 
|  | * be handled. The highest priority is 0. The default one is 2. | 
|  | * | 
|  | * @return {number} Priority. | 
|  | */ | 
|  | ImageRequest.prototype.getPriority = function() { | 
|  | return (this.request_.priority !== undefined) ? this.request_.priority : 2; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Tries to load the image from cache if exists and sends the response. | 
|  | * | 
|  | * @param {function()} onSuccess Success callback. | 
|  | * @param {function()} onFailure Failure callback. | 
|  | */ | 
|  | ImageRequest.prototype.loadFromCacheAndProcess = function( | 
|  | onSuccess, onFailure) { | 
|  | this.loadFromCache_( | 
|  | function(data, width, height) {  // Found in cache. | 
|  | this.sendImageData_(data, width, height); | 
|  | onSuccess(); | 
|  | }.bind(this), | 
|  | onFailure);  // Not found in cache. | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Tries to download the image, resizes and sends the response. | 
|  | * @param {function()} callback Completion callback. | 
|  | */ | 
|  | ImageRequest.prototype.downloadAndProcess = function(callback) { | 
|  | if (this.downloadCallback_) | 
|  | throw new Error('Downloading already started.'); | 
|  |  | 
|  | this.downloadCallback_ = callback; | 
|  | this.downloadOriginal_(this.onImageLoad_.bind(this), | 
|  | this.onImageError_.bind(this)); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Fetches the image from the persistent cache. | 
|  | * | 
|  | * @param {function(string, number, number)} onSuccess Success callback. | 
|  | * @param {function()} onFailure Failure callback. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.loadFromCache_ = function(onSuccess, onFailure) { | 
|  | var cacheKey = ImageCache.createKey(this.request_); | 
|  |  | 
|  | if (!cacheKey) { | 
|  | // Cache key is not provided for the request. | 
|  | onFailure(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!this.request_.cache) { | 
|  | // Cache is disabled for this request; therefore, remove it from cache | 
|  | // if existed. | 
|  | this.cache_.removeImage(cacheKey); | 
|  | onFailure(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!this.request_.timestamp) { | 
|  | // Persistent cache is available only when a timestamp is provided. | 
|  | onFailure(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | this.cache_.loadImage(cacheKey, | 
|  | this.request_.timestamp, | 
|  | onSuccess, | 
|  | onFailure); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Saves the image to the persistent cache. | 
|  | * | 
|  | * @param {string} data The image's data. | 
|  | * @param {number} width Image width. | 
|  | * @param {number} height Image height. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.saveToCache_ = function(data, width, height) { | 
|  | if (!this.request_.cache || !this.request_.timestamp) { | 
|  | // Persistent cache is available only when a timestamp is provided. | 
|  | return; | 
|  | } | 
|  |  | 
|  | var cacheKey = ImageCache.createKey(this.request_); | 
|  | if (!cacheKey) { | 
|  | // Cache key is not provided for the request. | 
|  | return; | 
|  | } | 
|  |  | 
|  | this.cache_.saveImage(cacheKey, | 
|  | data, | 
|  | width, | 
|  | height, | 
|  | this.request_.timestamp); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Downloads an image directly or for remote resources using the XmlHttpRequest. | 
|  | * | 
|  | * @param {function()} onSuccess Success callback. | 
|  | * @param {function()} onFailure Failure callback. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.downloadOriginal_ = function(onSuccess, onFailure) { | 
|  | this.image_.onload = function() { | 
|  | URL.revokeObjectURL(this.image_.src); | 
|  | onSuccess(); | 
|  | }.bind(this); | 
|  | this.image_.onerror = function() { | 
|  | URL.revokeObjectURL(this.image_.src); | 
|  | onFailure(); | 
|  | }.bind(this); | 
|  |  | 
|  | // Download data urls directly since they are not supported by XmlHttpRequest. | 
|  | var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/); | 
|  | if (dataUrlMatches) { | 
|  | this.image_.src = this.request_.url; | 
|  | this.contentType_ = dataUrlMatches[1]; | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Load RAW images by using Piex loader instead of XHR. | 
|  | var fileType = FileType.getTypeForName(this.request_.url); | 
|  | if (fileType.type === 'raw') { | 
|  | var timer = metrics.getTracker().startTiming( | 
|  | metrics.Categories.INTERNALS, | 
|  | metrics.timing.Variables.EXTRACT_THUMBNAIL_FROM_RAW, | 
|  | fileType.subtype); | 
|  | this.piexLoader_.load(this.request_.url).then(function(data) { | 
|  | timer.send(); | 
|  | var blob = new Blob([data.thumbnail], {type: 'image/jpeg'}); | 
|  | var url = URL.createObjectURL(blob); | 
|  | this.image_.src = url; | 
|  | this.request_.orientation = data.orientation; | 
|  | this.request_.colorSpace = data.colorSpace; | 
|  | }.bind(this), function(error) { | 
|  | console.error('PiexLoaderError: ', error); | 
|  | onFailure(); | 
|  | }); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Fetch the image via authorized XHR and parse it. | 
|  | var parseImage = function(contentType, blob) { | 
|  | if (contentType) | 
|  | this.contentType_ = contentType; | 
|  | this.image_.src = URL.createObjectURL(blob); | 
|  | }.bind(this); | 
|  |  | 
|  | // Request raw data via XHR. | 
|  | this.xhr_.load(this.request_.url, parseImage, onFailure); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers. | 
|  | * @constructor | 
|  | */ | 
|  | function AuthorizedXHR() { | 
|  | this.xhr_ = null; | 
|  | this.aborted_ = false; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * A map which is used to estimate content type from extension. | 
|  | * @enum {string} | 
|  | */ | 
|  | AuthorizedXHR.ExtensionContentTypeMap = { | 
|  | gif: 'image/gif', | 
|  | png: 'image/png', | 
|  | svg: 'image/svg', | 
|  | bmp: 'image/bmp', | 
|  | jpg: 'image/jpeg', | 
|  | jpeg: 'image/jpeg' | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Aborts the current request (if running). | 
|  | */ | 
|  | AuthorizedXHR.prototype.abort = function() { | 
|  | this.aborted_ = true; | 
|  | if (this.xhr_) | 
|  | this.xhr_.abort(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Loads an image using a OAuth2 token. If it fails, then tries to retry with | 
|  | * a refreshed OAuth2 token. | 
|  | * | 
|  | * @param {string} url URL to the resource to be fetched. | 
|  | * @param {function(string, Blob)} onSuccess Success callback with the content | 
|  | *     type and the fetched data. | 
|  | * @param {function()} onFailure Failure callback. | 
|  | */ | 
|  | AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) { | 
|  | this.aborted_ = false; | 
|  |  | 
|  | // Do not call any callbacks when aborting. | 
|  | var onMaybeSuccess = /** @type {function(string, Blob)} */ ( | 
|  | function(contentType, response) { | 
|  | // When content type is not available, try to estimate it from url. | 
|  | if (!contentType) { | 
|  | contentType = AuthorizedXHR.ExtensionContentTypeMap[ | 
|  | this.extractExtension_(url)]; | 
|  | } | 
|  |  | 
|  | if (!this.aborted_) | 
|  | onSuccess(contentType, response); | 
|  | }.bind(this)); | 
|  |  | 
|  | var onMaybeFailure = /** @type {function(number=)} */ ( | 
|  | function(opt_code) { | 
|  | if (!this.aborted_) | 
|  | onFailure(); | 
|  | }.bind(this)); | 
|  |  | 
|  | // Fetches the access token and makes an authorized call. If refresh is true, | 
|  | // then forces refreshing the access token. | 
|  | var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) { | 
|  | chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) { | 
|  | if (this.aborted_) | 
|  | return; | 
|  | if (!token) { | 
|  | onInnerFailure(); | 
|  | return; | 
|  | } | 
|  | this.xhr_ = AuthorizedXHR.load_( | 
|  | token, url, onInnerSuccess, onInnerFailure); | 
|  | }.bind(this)); | 
|  | }.bind(this); | 
|  |  | 
|  | // Refreshes the access token and retries the request. | 
|  | var maybeRetryCall = function(code) { | 
|  | if (this.aborted_) | 
|  | return; | 
|  | requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure); | 
|  | }.bind(this); | 
|  |  | 
|  | // Do not request a token for local resources, since it is not necessary. | 
|  | if (/^filesystem:/.test(url)) { | 
|  | // The query parameter is workaround for | 
|  | // crbug.com/379678, which force to obtain the latest contents of the image. | 
|  | var noCacheUrl = url + '?nocache=' + Date.now(); | 
|  | this.xhr_ = AuthorizedXHR.load_( | 
|  | null, | 
|  | noCacheUrl, | 
|  | onMaybeSuccess, | 
|  | onMaybeFailure); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Make the request with reusing the current token. If it fails, then retry. | 
|  | requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Extracts extension from url. | 
|  | * @param {string} url Url. | 
|  | * @return {string} Extracted extensiion, e.g. png. | 
|  | */ | 
|  | AuthorizedXHR.prototype.extractExtension_ = function(url) { | 
|  | var result = (/\.([a-zA-Z]+)$/i).exec(url); | 
|  | return result ? result[1] : ''; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token. | 
|  | * If the token is invalid, the request will fail. | 
|  | * | 
|  | * @param {?string} token OAuth2 token to be injected to the request. Null for | 
|  | *     no token. | 
|  | * @param {string} url URL to the resource to be fetched. | 
|  | * @param {function(string, Blob)} onSuccess Success callback with the content | 
|  | *     type and the fetched data. | 
|  | * @param {function(number=)} onFailure Failure callback with the error code | 
|  | *     if available. | 
|  | * @return {XMLHttpRequest} XHR instance. | 
|  | * @private | 
|  | */ | 
|  | AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) { | 
|  | var xhr = new XMLHttpRequest(); | 
|  | xhr.responseType = 'blob'; | 
|  |  | 
|  | xhr.onreadystatechange = function() { | 
|  | if (xhr.readyState != 4) | 
|  | return; | 
|  | if (xhr.status != 200) { | 
|  | onFailure(xhr.status); | 
|  | return; | 
|  | } | 
|  | var contentType = xhr.getResponseHeader('Content-Type'); | 
|  | onSuccess(contentType, /** @type {Blob} */ (xhr.response)); | 
|  | }.bind(this); | 
|  |  | 
|  | // Perform a xhr request. | 
|  | try { | 
|  | xhr.open('GET', url, true); | 
|  | if (token) | 
|  | xhr.setRequestHeader('Authorization', 'Bearer ' + token); | 
|  | xhr.send(); | 
|  | } catch (e) { | 
|  | onFailure(); | 
|  | } | 
|  |  | 
|  | return xhr; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Sends the resized image via the callback. If the image has been changed, | 
|  | * then packs the canvas contents, otherwise sends the raw image data. | 
|  | * | 
|  | * @param {boolean} imageChanged Whether the image has been changed. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.sendImage_ = function(imageChanged) { | 
|  | var imageData; | 
|  | var width; | 
|  | var height; | 
|  | if (!imageChanged) { | 
|  | // The image hasn't been processed, so the raw data can be directly | 
|  | // forwarded for speed (no need to encode the image again). | 
|  | imageData = this.image_.src; | 
|  | width = this.image_.width; | 
|  | height = this.image_.height; | 
|  | } else { | 
|  | // The image has been resized or rotated, therefore the canvas has to be | 
|  | // encoded to get the correct compressed image data. | 
|  | width = this.canvas_.width; | 
|  | height = this.canvas_.height; | 
|  |  | 
|  | switch (this.contentType_) { | 
|  | case 'image/gif': | 
|  | case 'image/png': | 
|  | case 'image/svg': | 
|  | case 'image/bmp': | 
|  | imageData = this.canvas_.toDataURL('image/png'); | 
|  | break; | 
|  | case 'image/jpeg': | 
|  | default: | 
|  | imageData = this.canvas_.toDataURL('image/jpeg', 0.9); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Send and store in the persistent cache. | 
|  | this.sendImageData_(imageData, width, height); | 
|  | this.saveToCache_(imageData, width, height); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Sends the resized image via the callback. | 
|  | * @param {string} data Compressed image data. | 
|  | * @param {number} width Width. | 
|  | * @param {number} height Height. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.sendImageData_ = function(data, width, height) { | 
|  | this.sendResponse_({ | 
|  | status: 'success', data: data, width: width, height: height, | 
|  | taskId: this.request_.taskId | 
|  | }); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Handler, when contents are loaded into the image element. Performs resizing | 
|  | * and finalizes the request process. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.onImageLoad_ = function() { | 
|  | // Perform processing if the url is not a data url, or if there are some | 
|  | // operations requested. | 
|  | if (!this.request_.url.match(/^data/) || | 
|  | ImageLoader.shouldProcess(this.image_.width, | 
|  | this.image_.height, | 
|  | this.request_)) { | 
|  | ImageLoader.resizeAndCrop(this.image_, this.canvas_, this.request_); | 
|  | ImageLoader.convertColorSpace( | 
|  | this.canvas_, this.request_.colorSpace || ColorSpace.SRGB); | 
|  | this.sendImage_(true);  // Image changed. | 
|  | } else { | 
|  | this.sendImage_(false);  // Image not changed. | 
|  | } | 
|  | this.cleanup_(); | 
|  | this.downloadCallback_(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Handler, when loading of the image fails. Sends a failure response and | 
|  | * finalizes the request process. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.onImageError_ = function() { | 
|  | this.sendResponse_( | 
|  | {status: 'error', taskId: this.request_.taskId}); | 
|  | this.cleanup_(); | 
|  | this.downloadCallback_(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Cancels the request. | 
|  | */ | 
|  | ImageRequest.prototype.cancel = function() { | 
|  | this.cleanup_(); | 
|  |  | 
|  | // If downloading has started, then call the callback. | 
|  | if (this.downloadCallback_) | 
|  | this.downloadCallback_(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Cleans up memory used by this request. | 
|  | * @private | 
|  | */ | 
|  | ImageRequest.prototype.cleanup_ = function() { | 
|  | this.image_.onerror = function() {}; | 
|  | this.image_.onload = function() {}; | 
|  |  | 
|  | // Transparent 1x1 pixel gif, to force garbage collecting. | 
|  | this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' + | 
|  | 'ABAAEAAAICTAEAOw=='; | 
|  |  | 
|  | this.xhr_.onload = function() {}; | 
|  | this.xhr_.abort(); | 
|  |  | 
|  | // Dispose memory allocated by Canvas. | 
|  | this.canvas_.width = 0; | 
|  | this.canvas_.height = 0; | 
|  | }; |