Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * Loads and resizes an image. | 6 * Loads and resizes an image. |
| 7 * @constructor | 7 * @constructor |
| 8 */ | 8 */ |
| 9 var ImageLoader = function() { | 9 var ImageLoader = function() { |
| 10 /** | 10 /** |
| 11 * Hash array of active requests. | 11 * Hash array of active requests. |
| 12 * @type {Object} | 12 * @type {Object} |
| 13 * @private | 13 * @private |
| 14 */ | 14 */ |
| 15 this.requests_ = {}; | 15 this.requests_ = {}; |
| 16 | 16 |
| 17 /** | 17 /** |
| 18 * Persistent cache object. | 18 * Persistent cache object. |
| 19 * @type {ImageLoader.Cache} | 19 * @type {ImageLoader.Cache} |
| 20 * @private | 20 * @private |
| 21 */ | 21 */ |
| 22 this.cache_ = new ImageLoader.Cache(); | 22 this.cache_ = new ImageLoader.Cache(); |
| 23 | 23 |
| 24 /** | |
| 25 * Manages pending requests and runs them in order of priorities. | |
| 26 * @type {ImageLoader.Worker} | |
| 27 * @private | |
| 28 */ | |
| 29 this.worker_ = new ImageLoader.Worker(); | |
| 30 | |
| 24 chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) { | 31 chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) { |
| 25 // TODO(mtomasz): Handle. | 32 // TODO(mtomasz): Handle. |
| 26 }); | 33 }); |
| 27 | 34 |
| 28 chrome.extension.onMessageExternal.addListener(function(request, | 35 chrome.extension.onMessageExternal.addListener(function(request, |
| 29 sender, | 36 sender, |
| 30 sendResponse) { | 37 sendResponse) { |
| 31 if (ImageLoader.ALLOWED_CLIENTS.indexOf(sender.id) !== -1) | 38 if (ImageLoader.ALLOWED_CLIENTS.indexOf(sender.id) !== -1) |
| 32 return this.onMessage_(sender.id, request, sendResponse); | 39 return this.onMessage_(sender.id, request, sendResponse); |
| 33 }.bind(this)); | 40 }.bind(this)); |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 56 ImageLoader.prototype.onMessage_ = function(senderId, request, callback) { | 63 ImageLoader.prototype.onMessage_ = function(senderId, request, callback) { |
| 57 var requestId = senderId + ':' + request.taskId; | 64 var requestId = senderId + ':' + request.taskId; |
| 58 if (request.cancel) { | 65 if (request.cancel) { |
| 59 // Cancel a task. | 66 // Cancel a task. |
| 60 if (requestId in this.requests_) { | 67 if (requestId in this.requests_) { |
| 61 this.requests_[requestId].cancel(); | 68 this.requests_[requestId].cancel(); |
| 62 delete this.requests_[requestId]; | 69 delete this.requests_[requestId]; |
| 63 } | 70 } |
| 64 return false; // No callback calls. | 71 return false; // No callback calls. |
| 65 } else { | 72 } else { |
| 66 // Start a task. | 73 // Create a request task and add it to the worker (queue). |
| 67 this.requests_[requestId] = | 74 var requestTask = new ImageLoader.Request(this.cache_, request, callback); |
| 68 new ImageLoader.Request(this.cache_, request, callback); | 75 this.requests_[requestId] = requestTask; |
| 76 this.worker_.add(requestTask); | |
| 69 return true; // Request will call the callback. | 77 return true; // Request will call the callback. |
| 70 } | 78 } |
| 71 }; | 79 }; |
| 72 | 80 |
| 73 /** | 81 /** |
| 74 * Returns the singleton instance. | 82 * Returns the singleton instance. |
| 75 * @return {ImageLoader} ImageLoader object. | 83 * @return {ImageLoader} ImageLoader object. |
| 76 */ | 84 */ |
| 77 ImageLoader.getInstance = function() { | 85 ImageLoader.getInstance = function() { |
| 78 if (!ImageLoader.instance_) | 86 if (!ImageLoader.instance_) |
| (...skipping 144 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 223 * @private | 231 * @private |
| 224 */ | 232 */ |
| 225 this.canvas_ = document.createElement('canvas'); | 233 this.canvas_ = document.createElement('canvas'); |
| 226 | 234 |
| 227 /** | 235 /** |
| 228 * @type {CanvasRenderingContext2D} | 236 * @type {CanvasRenderingContext2D} |
| 229 * @private | 237 * @private |
| 230 */ | 238 */ |
| 231 this.context_ = this.canvas_.getContext('2d'); | 239 this.context_ = this.canvas_.getContext('2d'); |
| 232 | 240 |
| 233 // Process the request. Try to load from cache. If it fails, then download. | 241 /** |
| 242 * Callback to be called once downloading is finished. | |
| 243 * @type {function()} | |
| 244 * @private | |
| 245 */ | |
| 246 this.downloadCallback_ = null; | |
| 247 }; | |
| 248 | |
| 249 /** | |
| 250 * Returns priority of the request. The higher priority, the faster it will | |
| 251 * be handled. The highest priority is 0. The default one is 2. | |
| 252 * | |
| 253 * @return {number} Priority. | |
| 254 */ | |
| 255 ImageLoader.Request.prototype.getPriority = function() { | |
| 256 return (this.request_.priority !== undefined) ? this.request_.priority : 2; | |
| 257 }; | |
| 258 | |
| 259 /** | |
| 260 * Tries to load the image from cache if exists and sends the response. | |
| 261 * | |
|
James Hawkins
2013/05/15 17:37:18
nit: Remove blank line.
mtomasz
2013/05/16 02:18:01
Why? We put an empty line after the description fo
| |
| 262 * @param {function()} onSuccess Success callback. | |
| 263 * @param {function()} onFailure Failure callback. | |
| 264 */ | |
| 265 ImageLoader.Request.prototype.loadFromCacheAndProcess = function( | |
| 266 onSuccess, onFailure) { | |
| 234 this.loadFromCache_( | 267 this.loadFromCache_( |
| 235 this.sendImageData_.bind(this), | 268 function(data) { // Found in cache. |
| 236 function() { // Failure, not in cache. | 269 this.sendImageData_(data); |
| 237 this.downloadOriginal_(this.onImageLoad_.bind(this), | 270 onSuccess(); |
| 238 this.onImageError_.bind(this)); | 271 }.bind(this), |
| 239 }.bind(this)); | 272 onFailure); // Not found in cache. |
| 273 }; | |
| 274 | |
| 275 /** | |
| 276 * Tries to download the image, resizes and sends the response. | |
| 277 * @param {function()} callback Completion callback. | |
| 278 */ | |
| 279 ImageLoader.Request.prototype.downloadAndProcess = function(callback) { | |
| 280 if (this.downloadCallback_) | |
| 281 throw new Error('Downloading already started.'); | |
| 282 | |
| 283 this.downloadCallback_ = callback; | |
| 284 this.downloadOriginal_(this.onImageLoad_.bind(this), | |
| 285 this.onImageError_.bind(this)); | |
| 240 }; | 286 }; |
| 241 | 287 |
| 242 /** | 288 /** |
| 243 * Fetches the image from the persistent cache. | 289 * Fetches the image from the persistent cache. |
| 290 * | |
|
James Hawkins
2013/05/15 17:37:18
nit: Please remove all of these added blank lines.
| |
| 244 * @param {function()} onSuccess Success callback. | 291 * @param {function()} onSuccess Success callback. |
| 245 * @param {function()} onFailure Failure callback. | 292 * @param {function()} onFailure Failure callback. |
| 246 * @private | 293 * @private |
| 247 */ | 294 */ |
| 248 ImageLoader.Request.prototype.loadFromCache_ = function(onSuccess, onFailure) { | 295 ImageLoader.Request.prototype.loadFromCache_ = function(onSuccess, onFailure) { |
| 249 var cacheKey = ImageLoader.Cache.createKey(this.request_); | 296 var cacheKey = ImageLoader.Cache.createKey(this.request_); |
| 250 | 297 |
| 251 if (!this.request_.cache) { | 298 if (!this.request_.cache) { |
| 252 // Cache is disabled for this request; therefore, remove it from cache | 299 // Cache is disabled for this request; therefore, remove it from cache |
| 253 // if existed. | 300 // if existed. |
| 254 this.cache_.removeImage(cacheKey); | 301 this.cache_.removeImage(cacheKey); |
| 255 onFailure(); | 302 onFailure(); |
| 256 return; | 303 return; |
| 257 } | 304 } |
| 258 | 305 |
| 259 if (!this.request_.timestamp) { | 306 if (!this.request_.timestamp) { |
| 260 // Persistent cache is available only when a timestamp is provided. | 307 // Persistent cache is available only when a timestamp is provided. |
| 261 onFailure(); | 308 onFailure(); |
| 262 return; | 309 return; |
| 263 } | 310 } |
| 264 | 311 |
| 265 this.cache_.loadImage(cacheKey, | 312 this.cache_.loadImage(cacheKey, |
| 266 this.request_.timestamp, | 313 this.request_.timestamp, |
| 267 onSuccess, | 314 onSuccess, |
| 268 onFailure); | 315 onFailure); |
| 269 }; | 316 }; |
| 270 | 317 |
| 271 /** | 318 /** |
| 272 * Saves the image to the persistent cache. | 319 * Saves the image to the persistent cache. |
| 320 * | |
| 273 * @param {string} data The image's data. | 321 * @param {string} data The image's data. |
| 274 * @private | 322 * @private |
| 275 */ | 323 */ |
| 276 ImageLoader.Request.prototype.saveToCache_ = function(data) { | 324 ImageLoader.Request.prototype.saveToCache_ = function(data) { |
| 277 if (!this.request_.cache || !this.request_.timestamp) { | 325 if (!this.request_.cache || !this.request_.timestamp) { |
| 278 // Persistent cache is available only when a timestamp is provided. | 326 // Persistent cache is available only when a timestamp is provided. |
| 279 return; | 327 return; |
| 280 } | 328 } |
| 281 | 329 |
| 282 var cacheKey = ImageLoader.Cache.createKey(this.request_); | 330 var cacheKey = ImageLoader.Cache.createKey(this.request_); |
| 283 this.cache_.saveImage(cacheKey, | 331 this.cache_.saveImage(cacheKey, |
| 284 data, | 332 data, |
| 285 this.request_.timestamp); | 333 this.request_.timestamp); |
| 286 }; | 334 }; |
| 287 | 335 |
| 288 /** | 336 /** |
| 289 * Downloads an image directly or for remote resources using the XmlHttpRequest. | 337 * Downloads an image directly or for remote resources using the XmlHttpRequest. |
| 338 * | |
| 290 * @param {function()} onSuccess Success callback. | 339 * @param {function()} onSuccess Success callback. |
| 291 * @param {function()} onFailure Failure callback. | 340 * @param {function()} onFailure Failure callback. |
| 292 * @private | 341 * @private |
| 293 */ | 342 */ |
| 294 ImageLoader.Request.prototype.downloadOriginal_ = function( | 343 ImageLoader.Request.prototype.downloadOriginal_ = function( |
| 295 onSuccess, onFailure) { | 344 onSuccess, onFailure) { |
| 296 this.image_.onload = onSuccess; | 345 this.image_.onload = onSuccess; |
| 297 this.image_.onerror = onFailure; | 346 this.image_.onerror = onFailure; |
| 298 | 347 |
| 299 if (window.harness || !this.request_.url.match(/^https?:/)) { | 348 if (!this.request_.url.match(/^https?:/)) { |
| 300 // Download directly. | 349 // Download directly. |
| 301 this.image_.src = this.request_.url; | 350 this.image_.src = this.request_.url; |
| 302 return; | 351 return; |
| 303 } | 352 } |
| 304 | 353 |
| 305 // Download using an xhr request. | 354 // Download using an xhr request. |
| 306 this.xhr_.responseType = 'blob'; | 355 this.xhr_.responseType = 'blob'; |
| 307 | 356 |
| 308 this.xhr_.onerror = this.image_.onerror; | 357 this.xhr_.onerror = this.image_.onerror; |
| 309 this.xhr_.onload = function() { | 358 this.xhr_.onload = function() { |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 356 ImageLoader.Request.prototype.sendImageData_ = function(data) { | 405 ImageLoader.Request.prototype.sendImageData_ = function(data) { |
| 357 this.sendResponse_({status: 'success', | 406 this.sendResponse_({status: 'success', |
| 358 data: data, | 407 data: data, |
| 359 taskId: this.request_.taskId}); | 408 taskId: this.request_.taskId}); |
| 360 }; | 409 }; |
| 361 | 410 |
| 362 /** | 411 /** |
| 363 * Handler, when contents are loaded into the image element. Performs resizing | 412 * Handler, when contents are loaded into the image element. Performs resizing |
| 364 * and finalizes the request process. | 413 * and finalizes the request process. |
| 365 * | 414 * |
| 415 * @param {function()} callback Completion callback. | |
| 366 * @private | 416 * @private |
| 367 */ | 417 */ |
| 368 ImageLoader.Request.prototype.onImageLoad_ = function() { | 418 ImageLoader.Request.prototype.onImageLoad_ = function(callback) { |
| 369 ImageLoader.resize(this.image_, this.canvas_, this.request_); | 419 ImageLoader.resize(this.image_, this.canvas_, this.request_); |
| 370 this.sendImage_(); | 420 this.sendImage_(); |
| 371 this.cleanup_(); | 421 this.cleanup_(); |
| 422 this.downloadCallback_(); | |
| 372 }; | 423 }; |
| 373 | 424 |
| 374 /** | 425 /** |
| 375 * Handler, when loading of the image fails. Sends a failure response and | 426 * Handler, when loading of the image fails. Sends a failure response and |
| 376 * finalizes the request process. | 427 * finalizes the request process. |
| 377 * | 428 * |
| 429 * @param {function()} callback Completion callback. | |
| 378 * @private | 430 * @private |
| 379 */ | 431 */ |
| 380 ImageLoader.Request.prototype.onImageError_ = function() { | 432 ImageLoader.Request.prototype.onImageError_ = function(callback) { |
| 381 this.sendResponse_({status: 'error', | 433 this.sendResponse_({status: 'error', |
| 382 taskId: this.request_.taskId}); | 434 taskId: this.request_.taskId}); |
| 383 this.cleanup_(); | 435 this.cleanup_(); |
| 436 this.downloadCallback_(); | |
| 384 }; | 437 }; |
| 385 | 438 |
| 386 /** | 439 /** |
| 387 * Cancels the request. | 440 * Cancels the request. |
| 388 */ | 441 */ |
| 389 ImageLoader.Request.prototype.cancel = function() { | 442 ImageLoader.Request.prototype.cancel = function() { |
| 390 this.cleanup_(); | 443 this.cleanup_(); |
| 444 | |
| 445 // If downloading has started, then call the callback. | |
| 446 if (this.downloadCallback_) | |
| 447 this.downloadCallback_(); | |
| 391 }; | 448 }; |
| 392 | 449 |
| 393 /** | 450 /** |
| 394 * Cleans up memory used by this request. | 451 * Cleans up memory used by this request. |
| 395 * @private | 452 * @private |
| 396 */ | 453 */ |
| 397 ImageLoader.Request.prototype.cleanup_ = function() { | 454 ImageLoader.Request.prototype.cleanup_ = function() { |
| 398 this.image_.onerror = function() {}; | 455 this.image_.onerror = function() {}; |
| 399 this.image_.onload = function() {}; | 456 this.image_.onload = function() {}; |
| 400 | 457 |
| 401 // Transparent 1x1 pixel gif, to force garbage collecting. | 458 // Transparent 1x1 pixel gif, to force garbage collecting. |
| 402 this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' + | 459 this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' + |
| 403 'ABAAEAAAICTAEAOw=='; | 460 'ABAAEAAAICTAEAOw=='; |
| 404 | 461 |
| 405 this.xhr_.onerror = function() {}; | 462 this.xhr_.onerror = function() {}; |
| 406 this.xhr_.onload = function() {}; | 463 this.xhr_.onload = function() {}; |
| 407 this.xhr_.abort(); | 464 this.xhr_.abort(); |
| 408 | 465 |
| 409 // Dispose memory allocated by Canvas. | 466 // Dispose memory allocated by Canvas. |
| 410 this.canvas_.width = 0; | 467 this.canvas_.width = 0; |
| 411 this.canvas_.height = 0; | 468 this.canvas_.height = 0; |
| 412 }; | 469 }; |
| 413 | 470 |
| 414 /** | 471 /** |
| 472 * Worker for requests. Fetches requests from a queue and processes them | |
| 473 * synchronously, taking into account priorities. The highest priority is 0. | |
| 474 */ | |
| 475 ImageLoader.Worker = function() { | |
| 476 /** | |
| 477 * List of pending requests. | |
| 478 * @type {ImageLoader.Request} | |
| 479 * @private | |
| 480 */ | |
| 481 this.pendingRequests_ = []; | |
| 482 | |
| 483 /** | |
| 484 * List of requests being processed. | |
| 485 * @type {ImageLoader.Request} | |
| 486 * @private | |
| 487 */ | |
| 488 this.activeRequests_ = []; | |
| 489 }; | |
| 490 | |
| 491 /** | |
| 492 * Maximum requests to be run in parallel. | |
| 493 * @type {number} | |
| 494 * @const | |
| 495 */ | |
| 496 ImageLoader.Worker.MAXIMUM_IN_PARALLEL = 5; | |
| 497 | |
| 498 /** | |
| 499 * Adds a request to the internal priority queue and executes it when requests | |
| 500 * with higher priorities are finished. If the result is cached, then it is | |
| 501 * processed immediately. | |
| 502 * | |
| 503 * @param {ImageLoader.Request} request Request object. | |
| 504 */ | |
| 505 ImageLoader.Worker.prototype.add = function(request) { | |
| 506 request.loadFromCacheAndProcess(function() { }, function() { | |
| 507 // Not found in cache, then add to pending requests. | |
| 508 this.pendingRequests_.push(request); | |
| 509 | |
| 510 // Sort requests by priorities. | |
| 511 this.pendingRequests_.sort(function(a, b) { | |
| 512 return a.getPriority() - b.getPriority(); | |
| 513 }); | |
| 514 | |
| 515 // Handle the most important requests (if possible). | |
| 516 this.continue_(); | |
| 517 }.bind(this)); | |
| 518 }; | |
| 519 | |
| 520 /** | |
| 521 * Processes pending requests from the queue. There is no guarantee that | |
| 522 * all of the tasks will be processed at once. | |
| 523 * | |
| 524 * @private | |
| 525 */ | |
| 526 ImageLoader.Worker.prototype.continue_ = function() { | |
| 527 for (var index = 0; index < this.pendingRequests_.length; index++) { | |
| 528 var request = this.pendingRequests_[index]; | |
| 529 | |
| 530 // Run only up to MAXIMUM_IN_PARALLEL in the same time. | |
| 531 if (Object.keys(this.activeRequests_).length == | |
| 532 ImageLoader.Worker.MAXIMUM_IN_PARALLEL) { | |
| 533 return; | |
| 534 } | |
| 535 | |
| 536 delete this.pendingRequests_.splice(index, 1); | |
| 537 this.activeRequests_.push(request); | |
| 538 | |
| 539 request.downloadAndProcess(this.finish_.bind(this, request)); | |
| 540 } | |
| 541 }; | |
| 542 | |
| 543 /** | |
| 544 * Handles finished requests. | |
| 545 * | |
| 546 * @param {ImageLoader.Request} request Finished request. | |
| 547 * @private | |
| 548 */ | |
| 549 ImageLoader.Worker.prototype.finish_ = function(request) { | |
| 550 var index = this.activeRequests_.indexOf(request); | |
| 551 if (index < 0) | |
| 552 console.warn('Request not found.'); | |
| 553 delete this.activeRequests_.splice(index, 1); | |
| 554 | |
| 555 // Handle the most important requests (if possible). | |
| 556 this.continue_(); | |
| 557 }; | |
| 558 | |
| 559 /** | |
| 415 * Persistent cache storing images in an indexed database on the hard disk. | 560 * Persistent cache storing images in an indexed database on the hard disk. |
| 416 * @constructor | 561 * @constructor |
| 417 */ | 562 */ |
| 418 ImageLoader.Cache = function() { | 563 ImageLoader.Cache = function() { |
| 419 /** | 564 /** |
| 420 * IndexedDB database handle. | 565 * IndexedDB database handle. |
| 421 * @type {IDBDatabase} | 566 * @type {IDBDatabase} |
| 422 * @private | 567 * @private |
| 423 */ | 568 */ |
| 424 this.db_ = null; | 569 this.db_ = null; |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 453 * @type {string} | 598 * @type {string} |
| 454 * @const | 599 * @const |
| 455 */ | 600 */ |
| 456 ImageLoader.Cache.DB_NAME = 'image-loader'; | 601 ImageLoader.Cache.DB_NAME = 'image-loader'; |
| 457 | 602 |
| 458 /** | 603 /** |
| 459 * Cache database version. | 604 * Cache database version. |
| 460 * @type {number} | 605 * @type {number} |
| 461 * @const | 606 * @const |
| 462 */ | 607 */ |
| 463 ImageLoader.Cache.DB_VERSION = 8; | 608 ImageLoader.Cache.DB_VERSION = 11; |
| 464 | 609 |
| 465 /** | 610 /** |
| 466 * Memory limit for images data in bytes. | 611 * Memory limit for images data in bytes. |
| 467 * | 612 * |
| 468 * @const | 613 * @const |
| 469 * @type {number} | 614 * @type {number} |
| 470 */ | 615 */ |
| 471 ImageLoader.Cache.MEMORY_LIMIT = 250 * 1024 * 1024; // 250 MB. | 616 ImageLoader.Cache.MEMORY_LIMIT = 250 * 1024 * 1024; // 250 MB. |
| 472 | 617 |
| 473 /** | 618 /** |
| (...skipping 329 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 803 | 948 |
| 804 metadataRequest.onerror = function() { | 949 metadataRequest.onerror = function() { |
| 805 console.error('Failed to remove an image.'); | 950 console.error('Failed to remove an image.'); |
| 806 metadataReceived = true; | 951 metadataReceived = true; |
| 807 onPartialSuccess(); | 952 onPartialSuccess(); |
| 808 }; | 953 }; |
| 809 }; | 954 }; |
| 810 | 955 |
| 811 // Load the extension. | 956 // Load the extension. |
| 812 ImageLoader.getInstance(); | 957 ImageLoader.getInstance(); |
| OLD | NEW |