// 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; };