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),
17 * Creates and starts downloading and then resizing of the image. Finally,
18 * returns the image using the callback.
20 * @param {string} id Request ID.
21 * @param {Cache} cache Cache object.
22 * @param {LoadImageRequest} request Request message as a hash array.
23 * @param {function(Object)} callback Callback used to send the response.
26 function Request(id, cache, request, callback) {
40 * @type {LoadImageRequest}
43 this.request_ = request;
46 * @type {function(Object)}
49 this.sendResponse_ = callback;
52 * Temporary image used to download images.
56 this.image_ = new Image();
59 * MIME type of the fetched image.
63 this.contentType_ = null;
66 * Used to download remote images using http:// or https:// protocols.
67 * @type {AuthorizedXHR}
70 this.xhr_ = new AuthorizedXHR();
73 * Temporary canvas used to resize and compress the image.
74 * @type {HTMLCanvasElement}
78 /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
81 * @type {CanvasRenderingContext2D}
85 /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d'));
88 * Callback to be called once downloading is finished.
92 this.downloadCallback_ = null;
96 * Returns ID of the request.
97 * @return {string} Request ID.
99 Request.prototype.getId = function() {
104 * Returns priority of the request. The higher priority, the faster it will
105 * be handled. The highest priority is 0. The default one is 2.
107 * @return {number} Priority.
109 Request.prototype.getPriority = function() {
110 return (this.request_.priority !== undefined) ? this.request_.priority : 2;
114 * Tries to load the image from cache if exists and sends the response.
116 * @param {function()} onSuccess Success callback.
117 * @param {function()} onFailure Failure callback.
119 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
121 function(data) { // Found in cache.
122 this.sendImageData_(data);
125 onFailure); // Not found in cache.
129 * Tries to download the image, resizes and sends the response.
130 * @param {function()} callback Completion callback.
132 Request.prototype.downloadAndProcess = function(callback) {
133 if (this.downloadCallback_)
134 throw new Error('Downloading already started.');
136 this.downloadCallback_ = callback;
137 this.downloadOriginal_(this.onImageLoad_.bind(this),
138 this.onImageError_.bind(this));
142 * Fetches the image from the persistent cache.
144 * @param {function(string)} onSuccess Success callback.
145 * @param {function()} onFailure Failure callback.
148 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
149 var cacheKey = Cache.createKey(this.request_);
151 if (!this.request_.cache) {
152 // Cache is disabled for this request; therefore, remove it from cache
154 this.cache_.removeImage(cacheKey);
159 if (!this.request_.timestamp) {
160 // Persistent cache is available only when a timestamp is provided.
165 this.cache_.loadImage(cacheKey,
166 this.request_.timestamp,
172 * Saves the image to the persistent cache.
174 * @param {string} data The image's data.
177 Request.prototype.saveToCache_ = function(data) {
178 if (!this.request_.cache || !this.request_.timestamp) {
179 // Persistent cache is available only when a timestamp is provided.
183 var cacheKey = Cache.createKey(this.request_);
184 this.cache_.saveImage(cacheKey,
186 this.request_.timestamp);
190 * Downloads an image directly or for remote resources using the XmlHttpRequest.
192 * @param {function()} onSuccess Success callback.
193 * @param {function()} onFailure Failure callback.
196 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
197 this.image_.onload = function() {
198 URL.revokeObjectURL(this.image_.src);
201 this.image_.onerror = function() {
202 URL.revokeObjectURL(this.image_.src);
206 // Download data urls directly since they are not supported by XmlHttpRequest.
207 var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
208 if (dataUrlMatches) {
209 this.image_.src = this.request_.url;
210 this.contentType_ = dataUrlMatches[1];
214 // Fetch the image via authorized XHR and parse it.
215 var parseImage = function(contentType, blob) {
216 this.image_.src = URL.createObjectURL(blob);
219 // Request raw data via XHR.
220 this.xhr_.load(this.request_.url, parseImage, onFailure);
224 * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
227 function AuthorizedXHR() {
229 this.aborted_ = false;
233 * Aborts the current request (if running).
235 AuthorizedXHR.prototype.abort = function() {
236 this.aborted_ = true;
242 * Loads an image using a OAuth2 token. If it fails, then tries to retry with
243 * a refreshed OAuth2 token.
245 * @param {string} url URL to the resource to be fetched.
246 * @param {function(string, Blob)} onSuccess Success callback with the content
247 * type and the fetched data.
248 * @param {function()} onFailure Failure callback.
250 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
251 this.aborted_ = false;
253 // Do not call any callbacks when aborting.
254 var onMaybeSuccess = /** @type {function(string, Blob)} */ (
255 function(contentType, response) {
257 onSuccess(contentType, response);
260 var onMaybeFailure = /** @type {function(number=)} */ (
266 // Fetches the access token and makes an authorized call. If refresh is true,
267 // then forces refreshing the access token.
268 var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
269 chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
276 this.xhr_ = AuthorizedXHR.load_(
277 token, url, onInnerSuccess, onInnerFailure);
281 // Refreshes the access token and retries the request.
282 var maybeRetryCall = function(code) {
285 requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
288 // Do not request a token for local resources, since it is not necessary.
289 if (/^filesystem:/.test(url)) {
290 // The query parameter is workaround for
291 // crbug.com/379678, which force to obtain the latest contents of the image.
292 var noCacheUrl = url + '?nocache=' + Date.now();
293 this.xhr_ = AuthorizedXHR.load_(
301 // Make the request with reusing the current token. If it fails, then retry.
302 requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
306 * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
307 * If the token is invalid, the request will fail.
309 * @param {?string} token OAuth2 token to be injected to the request. Null for
311 * @param {string} url URL to the resource to be fetched.
312 * @param {function(string, Blob)} onSuccess Success callback with the content
313 * type and the fetched data.
314 * @param {function(number=)} onFailure Failure callback with the error code
316 * @return {XMLHttpRequest} XHR instance.
319 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
320 var xhr = new XMLHttpRequest();
321 xhr.responseType = 'blob';
323 xhr.onreadystatechange = function() {
324 if (xhr.readyState != 4)
326 if (xhr.status != 200) {
327 onFailure(xhr.status);
330 var contentType = xhr.getResponseHeader('Content-Type');
331 onSuccess(contentType, /** @type {Blob} */ (xhr.response));
334 // Perform a xhr request.
336 xhr.open('GET', url, true);
338 xhr.setRequestHeader('Authorization', 'Bearer ' + token);
348 * Sends the resized image via the callback. If the image has been changed,
349 * then packs the canvas contents, otherwise sends the raw image data.
351 * @param {boolean} imageChanged Whether the image has been changed.
354 Request.prototype.sendImage_ = function(imageChanged) {
357 // The image hasn't been processed, so the raw data can be directly
358 // forwarded for speed (no need to encode the image again).
359 imageData = this.image_.src;
361 // The image has been resized or rotated, therefore the canvas has to be
362 // encoded to get the correct compressed image data.
363 switch (this.contentType_) {
368 imageData = this.canvas_.toDataURL('image/png');
372 imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
376 // Send and store in the persistent cache.
377 this.sendImageData_(imageData);
378 this.saveToCache_(imageData);
382 * Sends the resized image via the callback.
383 * @param {string} data Compressed image data.
386 Request.prototype.sendImageData_ = function(data) {
388 {status: 'success', data: data, taskId: this.request_.taskId});
392 * Handler, when contents are loaded into the image element. Performs resizing
393 * and finalizes the request process.
396 Request.prototype.onImageLoad_ = function() {
397 // Perform processing if the url is not a data url, or if there are some
398 // operations requested.
399 if (!this.request_.url.match(/^data/) ||
400 ImageLoader.shouldProcess(this.image_.width,
403 ImageLoader.resize(this.image_, this.canvas_, this.request_);
404 this.sendImage_(true); // Image changed.
406 this.sendImage_(false); // Image not changed.
409 this.downloadCallback_();
413 * Handler, when loading of the image fails. Sends a failure response and
414 * finalizes the request process.
417 Request.prototype.onImageError_ = function() {
419 {status: 'error', taskId: this.request_.taskId});
421 this.downloadCallback_();
425 * Cancels the request.
427 Request.prototype.cancel = function() {
430 // If downloading has started, then call the callback.
431 if (this.downloadCallback_)
432 this.downloadCallback_();
436 * Cleans up memory used by this request.
439 Request.prototype.cleanup_ = function() {
440 this.image_.onerror = function() {};
441 this.image_.onload = function() {};
443 // Transparent 1x1 pixel gif, to force garbage collecting.
444 this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
445 'ABAAEAAAICTAEAOw==';
447 this.xhr_.onload = function() {};
450 // Dispose memory allocated by Canvas.
451 this.canvas_.width = 0;
452 this.canvas_.height = 0;