blob: 4b847204c9d81ef00daf664f37e752733fdf1a4d [file] [log] [blame]
// 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.
'use strict';
/**
* Creates and starts downloading and then resizing of the image. Finally,
* returns the image using the callback.
*
* @param {string} id Request ID.
* @param {Cache} cache Cache object.
* @param {Object} request Request message as a hash array.
* @param {function} callback Callback used to send the response.
* @constructor
*/
function Request(id, cache, request, callback) {
/**
* @type {string}
* @private
*/
this.id_ = id;
/**
* @type {Cache}
* @private
*/
this.cache_ = cache;
/**
* @type {Object}
* @private
*/
this.request_ = request;
/**
* @type {function}
* @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 {XMLHttpRequest}
* @private
*/
this.xhr_ = new XMLHttpRequest();
/**
* Temporary canvas used to resize and compress the image.
* @type {HTMLCanvasElement}
* @private
*/
this.canvas_ = document.createElement('canvas');
/**
* @type {CanvasRenderingContext2D}
* @private
*/
this.context_ = 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.
*/
Request.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.
*/
Request.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.
*/
Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
this.loadFromCache_(
function(data) { // Found in cache.
this.sendImageData_(data);
onSuccess();
}.bind(this),
onFailure); // Not found in cache.
};
/**
* Tries to download the image, resizes and sends the response.
* @param {function()} callback Completion callback.
*/
Request.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()} onSuccess Success callback.
* @param {function()} onFailure Failure callback.
* @private
*/
Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
var cacheKey = Cache.createKey(this.request_);
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.
* @private
*/
Request.prototype.saveToCache_ = function(data) {
if (!this.request_.cache || !this.request_.timestamp) {
// Persistent cache is available only when a timestamp is provided.
return;
}
var cacheKey = Cache.createKey(this.request_);
this.cache_.saveImage(cacheKey,
data,
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
*/
Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
this.image_.onload = onSuccess;
this.image_.onerror = onFailure;
// 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;
}
// Download using an xhr request.
this.xhr_.responseType = 'blob';
this.xhr_.onerror = this.image_.onerror;
this.xhr_.onload = function() {
if (this.xhr_.status != 200) {
this.image_.onerror();
return;
}
// Process returned data, including the mime type.
this.contentType_ = this.xhr_.getResponseHeader('Content-Type');
var reader = new FileReader();
reader.onerror = this.image_.onerror;
reader.onload = function(e) {
this.image_.src = e.target.result;
}.bind(this);
// Load the data to the image as a data url.
reader.readAsDataURL(this.xhr_.response);
}.bind(this);
// Perform a xhr request.
try {
this.xhr_.open('GET', this.request_.url, true);
this.xhr_.send();
} catch (e) {
this.image_.onerror();
}
};
/**
* 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
*/
Request.prototype.sendImage_ = function(imageChanged) {
var imageData;
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;
} else {
// The image has been resized or rotated, therefore the canvas has to be
// encoded to get the correct compressed image data.
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);
this.saveToCache_(imageData);
};
/**
* Sends the resized image via the callback.
* @param {string} data Compressed image data.
* @private
*/
Request.prototype.sendImageData_ = function(data) {
this.sendResponse_({status: 'success',
data: data,
taskId: this.request_.taskId});
};
/**
* Handler, when contents are loaded into the image element. Performs resizing
* and finalizes the request process.
*
* @param {function()} callback Completion callback.
* @private
*/
Request.prototype.onImageLoad_ = function(callback) {
// 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.resize(this.image_, this.canvas_, this.request_);
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.
*
* @param {function()} callback Completion callback.
* @private
*/
Request.prototype.onImageError_ = function(callback) {
this.sendResponse_({status: 'error',
taskId: this.request_.taskId});
this.cleanup_();
this.downloadCallback_();
};
/**
* Cancels the request.
*/
Request.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
*/
Request.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_.onerror = function() {};
this.xhr_.onload = function() {};
this.xhr_.abort();
// Dispose memory allocated by Canvas.
this.canvas_.width = 0;
this.canvas_.height = 0;
};