OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 var ImageLoader = ImageLoader || {}; |
| 6 |
| 7 /** |
| 8 * Image loader's extension id. |
| 9 * @const |
| 10 * @type {string} |
| 11 */ |
| 12 ImageLoader.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; |
| 13 |
| 14 /** |
| 15 * Client used to connect to the remote ImageLoader extension. Client class runs |
| 16 * in the extension, where the client.js is included (eg. Files.app). |
| 17 * It sends remote requests using IPC to the ImageLoader class and forwards |
| 18 * its responses. |
| 19 * |
| 20 * Implements cache, which is stored in the calling extension. |
| 21 * |
| 22 * @constructor |
| 23 */ |
| 24 ImageLoader.Client = function() { |
| 25 /** |
| 26 * @type {Port} |
| 27 * @private |
| 28 */ |
| 29 this.port_ = chrome.extension.connect(ImageLoader.EXTENSION_ID); |
| 30 this.port_.onMessage.addListener(this.handleMessage_.bind(this)); |
| 31 |
| 32 /** |
| 33 * Hash array with active tasks. |
| 34 * @type {Object} |
| 35 * @private |
| 36 */ |
| 37 this.tasks_ = {}; |
| 38 |
| 39 /** |
| 40 * @type {number} |
| 41 * @private |
| 42 */ |
| 43 this.lastTaskId_ = 0; |
| 44 |
| 45 /** |
| 46 * LRU cache for images. |
| 47 * @type {ImageLoader.Client.Cache} |
| 48 * @private |
| 49 */ |
| 50 this.cache_ = new ImageLoader.Client.Cache(); |
| 51 }; |
| 52 |
| 53 /** |
| 54 * Returns a singleton instance. |
| 55 * @return {ImageLoader.Client} ImageLoader.Client instance. |
| 56 */ |
| 57 ImageLoader.Client.getInstance = function() { |
| 58 if (!ImageLoader.Client.instance_) |
| 59 ImageLoader.Client.instance_ = new ImageLoader.Client(); |
| 60 return ImageLoader.Client.instance_; |
| 61 }; |
| 62 |
| 63 /** |
| 64 * Handles a message from the remote image loader and calls the registered |
| 65 * callback to pass the response back to the requester. |
| 66 * |
| 67 * @param {Object} message Response message as a hash array. |
| 68 * @private |
| 69 */ |
| 70 ImageLoader.Client.prototype.handleMessage_ = function(message) { |
| 71 if (!(message.taskId in this.tasks_)) { |
| 72 // This task has been canceled, but was already fetched, so it's result |
| 73 // should be discarded anyway. |
| 74 return; |
| 75 } |
| 76 |
| 77 var task = this.tasks_[message.taskId]; |
| 78 |
| 79 // Check if the task is still valid. |
| 80 if (task.isValid()) |
| 81 task.accept(message); |
| 82 |
| 83 delete this.tasks_[message.taskId]; |
| 84 }; |
| 85 |
| 86 /** |
| 87 * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| 88 * which are not valid anymore, which will reduce cpu consumption. |
| 89 * |
| 90 * @param {string} url Url of the requested image. |
| 91 * @param {function} callback Callback used to return response. |
| 92 * @param {Object=} opt_options Loader options, such as: scale, maxHeight, |
| 93 * width, height and/or cache. |
| 94 * @param {function=} opt_isValid Function returning false in case |
| 95 * a request is not valid anymore, eg. parent node has been detached. |
| 96 * @return {?number} Remote task id or null if loaded from cache. |
| 97 */ |
| 98 ImageLoader.Client.prototype.load = function( |
| 99 url, callback, opt_options, opt_isValid) { |
| 100 opt_options = opt_options || {}; |
| 101 opt_isValid = opt_isValid || function() { return true; }; |
| 102 |
| 103 // Cancel old, invalid tasks. |
| 104 var taskKeys = Object.keys(this.tasks_); |
| 105 for (var index = 0; index < taskKeys.length; index++) { |
| 106 var taskKey = taskKeys[index]; |
| 107 var task = this.tasks_[taskKey]; |
| 108 if (!task.isValid()) { |
| 109 // Cancel this task since it is not valid anymore. |
| 110 this.cancel(taskKey); |
| 111 delete this.tasks_[taskKey]; |
| 112 } |
| 113 } |
| 114 |
| 115 // Replace the extension id. |
| 116 var sourceId = chrome.i18n.getMessage('@@extension_id'); |
| 117 var targetId = ImageLoader.EXTENSION_ID; |
| 118 |
| 119 url = url.replace('filesystem:chrome-extension://' + sourceId, |
| 120 'filesystem:chrome-extension://' + targetId); |
| 121 |
| 122 // Try to load from cache, if available. |
| 123 var cacheKey = ImageLoader.Client.Cache.createKey(url, opt_options); |
| 124 if (opt_options.cache) { |
| 125 // Load from cache. |
| 126 // TODO(mtomasz): Add cache invalidating if the file has changed. |
| 127 var cachedData = this.cache_.loadImage(cacheKey); |
| 128 if (cachedData) { |
| 129 callback({ status: 'success', data: cachedData }); |
| 130 return null; |
| 131 } |
| 132 } else { |
| 133 // Remove from cache. |
| 134 this.cache_.removeImage(cacheKey); |
| 135 } |
| 136 |
| 137 // Not available in cache, performing a request to a remote extension. |
| 138 request = opt_options; |
| 139 this.lastTaskId_++; |
| 140 var task = { isValid: opt_isValid, accept: function(result) { |
| 141 // Save to cache. |
| 142 if (result.status == 'success' && opt_options.cache) |
| 143 this.cache_.saveImage(cacheKey, result.data); |
| 144 callback(result); |
| 145 }.bind(this) }; |
| 146 this.tasks_[this.lastTaskId_] = task; |
| 147 |
| 148 request.url = url; |
| 149 request.taskId = this.lastTaskId_; |
| 150 |
| 151 this.port_.postMessage(request); |
| 152 return request.taskId; |
| 153 }; |
| 154 |
| 155 /** |
| 156 * Cancels the request. |
| 157 * @param {number} taskId Task id returned by ImageLoader.Client.load(). |
| 158 */ |
| 159 ImageLoader.Client.prototype.cancel = function(taskId) { |
| 160 this.port_.postMessage({ taskId: taskId, cancel: true }); |
| 161 }; |
| 162 |
| 163 /** |
| 164 * Prints the cache usage statistics. |
| 165 */ |
| 166 ImageLoader.Client.prototype.stat = function() { |
| 167 this.cache_.stat(); |
| 168 }; |
| 169 |
| 170 /** |
| 171 * Least Recently Used (LRU) cache implementation to be used by |
| 172 * ImageLoader.Client class. It has memory constraints, so it will never |
| 173 * exceed specified memory limit defined in MEMORY_LIMIT. |
| 174 * |
| 175 * @constructor |
| 176 */ |
| 177 ImageLoader.Client.Cache = function() { |
| 178 this.images_ = []; |
| 179 this.size_ = 0; |
| 180 }; |
| 181 |
| 182 /** |
| 183 * Memory limit for images data in bytes. |
| 184 * |
| 185 * @const |
| 186 * @type {number} |
| 187 */ |
| 188 ImageLoader.Client.Cache.MEMORY_LIMIT = 100 * 1024 * 1024; // 100 MB. |
| 189 |
| 190 /** |
| 191 * Creates a cache key. |
| 192 * |
| 193 * @param {string} url Image url. |
| 194 * @param {Object=} opt_options Loader options as a hash array. |
| 195 * @return {string} Cache key. |
| 196 */ |
| 197 ImageLoader.Client.Cache.createKey = function(url, opt_options) { |
| 198 var array = opt_options || {}; |
| 199 array.url = url; |
| 200 return JSON.stringify(array); |
| 201 }; |
| 202 |
| 203 /** |
| 204 * Evicts the least used elements in cache to make space for a new image. |
| 205 * |
| 206 * @param {number} size Requested size. |
| 207 * @private |
| 208 */ |
| 209 ImageLoader.Client.Cache.prototype.evictCache_ = function(size) { |
| 210 // Sort from the most recent to the oldest. |
| 211 this.images_.sort(function(a, b) { |
| 212 return b.timestamp - a.timestamp; |
| 213 }); |
| 214 |
| 215 while (this.images_.length > 0 && |
| 216 (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ < size)) { |
| 217 var entry = this.images_.pop(); |
| 218 this.size_ -= entry.data.length; |
| 219 } |
| 220 }; |
| 221 |
| 222 /** |
| 223 * Saves an image in the cache. |
| 224 * |
| 225 * @param {string} key Cache key. |
| 226 * @param {string} data Image data. |
| 227 */ |
| 228 ImageLoader.Client.Cache.prototype.saveImage = function(key, data) { |
| 229 this.evictCache_(data.length); |
| 230 if (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ >= data.length) { |
| 231 this.images_[key] = { timestamp: Date.now(), data: data }; |
| 232 this.size_ += data.length; |
| 233 } |
| 234 }; |
| 235 |
| 236 /** |
| 237 * Loads an image from the cache (if available) or returns null. |
| 238 * |
| 239 * @param {string} key Cache key. |
| 240 * @return {?string} Data of the loaded image or null. |
| 241 */ |
| 242 ImageLoader.Client.Cache.prototype.loadImage = function(key) { |
| 243 if (!(key in this.images_)) |
| 244 return null; |
| 245 |
| 246 var entry = this.images_[key]; |
| 247 entry.timestamp = Date.now(); |
| 248 return entry.data; |
| 249 }; |
| 250 |
| 251 /** |
| 252 * Prints the cache usage stats. |
| 253 */ |
| 254 ImageLoader.Client.Cache.prototype.stat = function() { |
| 255 console.log('Cache entries: ' + Object.keys(this.images_).length); |
| 256 console.log('Usage: ' + Math.round(this.size_ / |
| 257 ImageLoader.Client.Cache.MEMORY_LIMIT * 100.0) + '%'); |
| 258 }; |
| 259 |
| 260 /** |
| 261 * Removes the image from the cache. |
| 262 * @param {string} key Cache key. |
| 263 */ |
| 264 ImageLoader.Client.Cache.prototype.removeImage = function(key) { |
| 265 if (!(key in this.images_)) |
| 266 return; |
| 267 |
| 268 var entry = this.images_[key]; |
| 269 this.size_ -= entry.data.length; |
| 270 delete this.images_[key]; |
| 271 }; |
| 272 |
| 273 // Helper functions. |
| 274 |
| 275 /** |
| 276 * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| 277 * which are not valid anymore, which will reduce cpu consumption. |
| 278 * |
| 279 * @param {string} url Url of the requested image. |
| 280 * @param {Image} image Image node to load the requested picture into. |
| 281 * @param {Object} options Loader options, such as: scale, maxHeight, width, |
| 282 * height and/or cache. |
| 283 * @param {function=} onSuccess Callback for success. |
| 284 * @param {function=} onError Callback for failure. |
| 285 * @param {function=} opt_isValid Function returning false in case |
| 286 * a request is not valid anymore, eg. parent node has been detached. |
| 287 * @return {?number} Remote task id or null if loaded from cache. |
| 288 */ |
| 289 ImageLoader.Client.loadToImage = function(url, image, options, onSuccess, |
| 290 onError, opt_isValid) { |
| 291 var callback = function(result) { |
| 292 if (result.status == 'error') { |
| 293 onError(); |
| 294 return; |
| 295 } |
| 296 image.src = result.data; |
| 297 onSuccess(); |
| 298 }; |
| 299 |
| 300 return ImageLoader.Client.getInstance().load( |
| 301 url, callback, options, opt_isValid); |
| 302 }; |
OLD | NEW |