| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 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 'use strict'; | 5 'use strict'; |
| 6 | 6 |
| 7 /** | 7 /** |
| 8 * The overlay displaying the image. | 8 * The overlay displaying the image. |
| 9 * | 9 * |
| 10 * @param {HTMLElement} container The container element. | 10 * @param {HTMLElement} container The container element. |
| 11 * @param {Viewport} viewport The viewport. | 11 * @param {Viewport} viewport The viewport. |
| 12 * @constructor | 12 * @constructor |
| 13 * @extends {ImageBuffer.Overlay} | 13 * @extends {ImageBuffer.Overlay} |
| 14 */ | 14 */ |
| 15 function ImageView(container, viewport) { | 15 function ImageView(container, viewport) { |
| 16 ImageBuffer.Overlay.call(this); | 16 ImageBuffer.Overlay.call(this); |
| 17 | 17 |
| 18 this.container_ = container; | 18 this.container_ = container; |
| 19 this.viewport_ = viewport; | 19 this.viewport_ = viewport; |
| 20 this.document_ = container.ownerDocument; | 20 this.document_ = container.ownerDocument; |
| 21 this.contentGeneration_ = 0; | 21 this.contentGeneration_ = 0; |
| 22 this.displayedContentGeneration_ = 0; | 22 this.displayedContentGeneration_ = 0; |
| 23 | 23 |
| 24 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_); | 24 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_); |
| 25 // We have a separate image loader for prefetch which does not get cancelled | 25 // We have a separate image loader for prefetch which does not get cancelled |
| 26 // when the selection changes. | 26 // when the selection changes. |
| 27 this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_); | 27 this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_); |
| 28 | 28 |
| 29 // The content cache is used for prefetching the next image when going | |
| 30 // through the images sequentially. The real life photos can be large | |
| 31 // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching. | |
| 32 this.contentCache_ = new ImageView.Cache(2); | |
| 33 | |
| 34 // We reuse previously generated screen-scale images so that going back to | |
| 35 // a recently loaded image looks instant even if the image is not in | |
| 36 // the content cache any more. Screen-scale images are small (~1Mpix) | |
| 37 // so we can afford to cache more of them. | |
| 38 this.screenCache_ = new ImageView.Cache(5); | |
| 39 this.contentCallbacks_ = []; | 29 this.contentCallbacks_ = []; |
| 40 | 30 |
| 41 /** | 31 /** |
| 42 * The element displaying the current content. | 32 * The element displaying the current content. |
| 43 * | 33 * |
| 44 * @type {HTMLCanvasElement} | 34 * @type {HTMLCanvasElement} |
| 45 * @private | 35 * @private |
| 46 */ | 36 */ |
| 47 this.screenImage_ = null; | 37 this.screenImage_ = null; |
| 48 } | 38 } |
| (...skipping 228 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 277 (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { | 267 (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { |
| 278 effect = null; | 268 effect = null; |
| 279 } | 269 } |
| 280 this.lastLoadTime_ = time; | 270 this.lastLoadTime_ = time; |
| 281 } | 271 } |
| 282 | 272 |
| 283 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime')); | 273 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime')); |
| 284 | 274 |
| 285 var self = this; | 275 var self = this; |
| 286 | 276 |
| 287 this.contentEntry_ = entry; | 277 this.contentItem_ = item; |
| 288 this.contentRevision_ = -1; | 278 this.contentRevision_ = -1; |
| 289 | 279 |
| 290 // Cache has to be evicted in advance, so the returned cached image is not | 280 var cached = item.contentImage; |
| 291 // evicted later by the prefetched image. | |
| 292 this.contentCache_.evictLRU(); | |
| 293 | |
| 294 var cached = this.contentCache_.getItem(this.contentEntry_); | |
| 295 if (cached) { | 281 if (cached) { |
| 296 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, | 282 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, |
| 297 false /* no preview */, cached); | 283 false /* no preview */, cached); |
| 298 } else { | 284 } else { |
| 299 var cachedScreen = this.screenCache_.getItem(this.contentEntry_); | 285 var cachedScreen = item.screenImage; |
| 300 var imageWidth = metadata.media && metadata.media.width || | 286 var imageWidth = metadata.media && metadata.media.width || |
| 301 metadata.drive && metadata.drive.imageWidth; | 287 metadata.drive && metadata.drive.imageWidth; |
| 302 var imageHeight = metadata.media && metadata.media.height || | 288 var imageHeight = metadata.media && metadata.media.height || |
| 303 metadata.drive && metadata.drive.imageHeight; | 289 metadata.drive && metadata.drive.imageHeight; |
| 304 if (cachedScreen) { | 290 if (cachedScreen) { |
| 305 // We have a cached screen-scale canvas, use it instead of a thumbnail. | 291 // We have a cached screen-scale canvas, use it instead of a thumbnail. |
| 306 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen); | 292 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen); |
| 307 // As far as the user can tell the image is loaded. We still need to load | 293 // As far as the user can tell the image is loaded. We still need to load |
| 308 // the full res image to make editing possible, but we can report now. | 294 // the full res image to make editing possible, but we can report now. |
| 309 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); | 295 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 402 if (loadCallback) loadCallback(loadType, animationDuration, opt_error); | 388 if (loadCallback) loadCallback(loadType, animationDuration, opt_error); |
| 403 } | 389 } |
| 404 }; | 390 }; |
| 405 | 391 |
| 406 /** | 392 /** |
| 407 * Prefetches an image. | 393 * Prefetches an image. |
| 408 * @param {Gallery.Item} item The image item. | 394 * @param {Gallery.Item} item The image item. |
| 409 * @param {number} delay Image load delay in ms. | 395 * @param {number} delay Image load delay in ms. |
| 410 */ | 396 */ |
| 411 ImageView.prototype.prefetch = function(item, delay) { | 397 ImageView.prototype.prefetch = function(item, delay) { |
| 412 var self = this; | 398 if (item.contentImage) |
| 413 var entry = item.getEntry(); | 399 return; |
| 414 function prefetchDone(canvas) { | 400 this.prefetchLoader_.load(item, function(canvas) { |
| 415 if (canvas.width) | 401 if (canvas.width && canvas.height && !item.contentImage) |
| 416 self.contentCache_.putItem(entry, canvas); | 402 item.contentImage = canvas; |
| 417 } | 403 }, delay); |
| 418 | |
| 419 var cached = this.contentCache_.getItem(entry); | |
| 420 if (cached) { | |
| 421 prefetchDone(cached); | |
| 422 } else if (FileType.getMediaType(entry) === 'image') { | |
| 423 // Evict the LRU item before we allocate the new canvas to avoid unneeded | |
| 424 // strain on memory. | |
| 425 this.contentCache_.evictLRU(); | |
| 426 | |
| 427 this.prefetchLoader_.load(item, prefetchDone, delay); | |
| 428 } | |
| 429 }; | 404 }; |
| 430 | 405 |
| 431 /** | 406 /** |
| 432 * Renames the current image. | |
| 433 * @param {FileEntry} newEntry The new image Entry. | |
| 434 */ | |
| 435 ImageView.prototype.changeEntry = function(newEntry) { | |
| 436 this.contentCache_.renameItem(this.contentEntry_, newEntry); | |
| 437 this.screenCache_.renameItem(this.contentEntry_, newEntry); | |
| 438 this.contentEntry_ = newEntry; | |
| 439 }; | |
| 440 | |
| 441 /** | |
| 442 * Unloads content. | 407 * Unloads content. |
| 443 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect. | 408 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect. |
| 444 */ | 409 */ |
| 445 ImageView.prototype.unload = function(zoomToRect) { | 410 ImageView.prototype.unload = function(zoomToRect) { |
| 446 if (this.unloadTimer_) { | 411 if (this.unloadTimer_) { |
| 447 clearTimeout(this.unloadTimer_); | 412 clearTimeout(this.unloadTimer_); |
| 448 this.unloadTimer_ = null; | 413 this.unloadTimer_ = null; |
| 449 } | 414 } |
| 450 if (zoomToRect && this.screenImage_) { | 415 if (zoomToRect && this.screenImage_) { |
| 451 var effect = this.createZoomEffect(zoomToRect); | 416 var effect = this.createZoomEffect(zoomToRect); |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 488 | 453 |
| 489 this.container_.appendChild(this.screenImage_); | 454 this.container_.appendChild(this.screenImage_); |
| 490 | 455 |
| 491 this.preview_ = opt_preview; | 456 this.preview_ = opt_preview; |
| 492 // If this is not a thumbnail, cache the content and the screen-scale image. | 457 // If this is not a thumbnail, cache the content and the screen-scale image. |
| 493 if (this.hasValidImage()) { | 458 if (this.hasValidImage()) { |
| 494 // Insert the full resolution canvas into DOM so that it can be printed. | 459 // Insert the full resolution canvas into DOM so that it can be printed. |
| 495 this.container_.appendChild(this.contentCanvas_); | 460 this.container_.appendChild(this.contentCanvas_); |
| 496 this.contentCanvas_.classList.add('fullres'); | 461 this.contentCanvas_.classList.add('fullres'); |
| 497 | 462 |
| 498 this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true); | 463 this.contentItem_.contentImage = this.contentCanvas_; |
| 499 this.screenCache_.putItem(this.contentEntry_, this.screenImage_); | 464 this.contentItem_.screenImage = this.screenImage_; |
| 500 | 465 |
| 501 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually | 466 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually |
| 502 // much smaller than contentCanvas_ and still contains the entire image. | 467 // much smaller than contentCanvas_ and still contains the entire image. |
| 503 // Once we implement zoom/pan we should pass contentCanvas_ instead. | 468 // Once we implement zoom/pan we should pass contentCanvas_ instead. |
| 504 this.updateThumbnail_(this.screenImage_); | 469 this.updateThumbnail_(this.screenImage_); |
| 505 | 470 |
| 506 this.contentRevision_++; | 471 this.contentRevision_++; |
| 507 for (var i = 0; i !== this.contentCallbacks_.length; i++) { | 472 for (var i = 0; i !== this.contentCallbacks_.length; i++) { |
| 508 try { | 473 try { |
| 509 this.contentCallbacks_[i](); | 474 this.contentCallbacks_[i](); |
| (...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 681 this.setTransform_(oldScreenImage, this,viewport_, effect); | 646 this.setTransform_(oldScreenImage, this,viewport_, effect); |
| 682 setTimeout(setFade.bind(null, false), 0); | 647 setTimeout(setFade.bind(null, false), 0); |
| 683 setTimeout(function() { | 648 setTimeout(function() { |
| 684 if (oldScreenImage.parentNode) | 649 if (oldScreenImage.parentNode) |
| 685 oldScreenImage.parentNode.removeChild(oldScreenImage); | 650 oldScreenImage.parentNode.removeChild(oldScreenImage); |
| 686 }, effect.getSafeInterval()); | 651 }, effect.getSafeInterval()); |
| 687 | 652 |
| 688 return effect.getSafeInterval(); | 653 return effect.getSafeInterval(); |
| 689 }; | 654 }; |
| 690 | 655 |
| 691 /** | |
| 692 * Generic cache with a limited capacity and LRU eviction. | |
| 693 * @param {number} capacity Maximum number of cached item. | |
| 694 * @constructor | |
| 695 */ | |
| 696 ImageView.Cache = function(capacity) { | |
| 697 this.capacity_ = capacity; | |
| 698 this.map_ = {}; | |
| 699 this.order_ = []; | |
| 700 }; | |
| 701 | |
| 702 /** | |
| 703 * Fetches the item from the cache. | |
| 704 * @param {FileEntry} entry The entry. | |
| 705 * @return {Object} The cached item. | |
| 706 */ | |
| 707 ImageView.Cache.prototype.getItem = function(entry) { | |
| 708 return this.map_[entry.toURL()]; | |
| 709 }; | |
| 710 | |
| 711 /** | |
| 712 * Puts the item into the cache. | |
| 713 * | |
| 714 * @param {FileEntry} entry The entry. | |
| 715 * @param {Object} item The item object. | |
| 716 * @param {boolean=} opt_keepLRU True if the LRU order should not be modified. | |
| 717 */ | |
| 718 ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) { | |
| 719 var pos = this.order_.indexOf(entry.toURL()); | |
| 720 | |
| 721 if ((pos >= 0) !== (entry.toURL() in this.map_)) | |
| 722 throw new Error('Inconsistent cache state'); | |
| 723 | |
| 724 if (entry.toURL() in this.map_) { | |
| 725 if (!opt_keepLRU) { | |
| 726 // Move to the end (most recently used). | |
| 727 this.order_.splice(pos, 1); | |
| 728 this.order_.push(entry.toURL()); | |
| 729 } | |
| 730 } else { | |
| 731 this.evictLRU(); | |
| 732 this.order_.push(entry.toURL()); | |
| 733 } | |
| 734 | |
| 735 if ((pos >= 0) && (item !== this.map_[entry.toURL()])) | |
| 736 this.deleteItem_(this.map_[entry.toURL()]); | |
| 737 this.map_[entry.toURL()] = item; | |
| 738 | |
| 739 if (this.order_.length > this.capacity_) | |
| 740 throw new Error('Exceeded cache capacity'); | |
| 741 }; | |
| 742 | |
| 743 /** | |
| 744 * Evicts the least recently used items. | |
| 745 */ | |
| 746 ImageView.Cache.prototype.evictLRU = function() { | |
| 747 if (this.order_.length === this.capacity_) { | |
| 748 var url = this.order_.shift(); | |
| 749 this.deleteItem_(this.map_[url]); | |
| 750 delete this.map_[url]; | |
| 751 } | |
| 752 }; | |
| 753 | |
| 754 /** | |
| 755 * Changes the Entry. | |
| 756 * @param {FileEntry} oldEntry The old Entry. | |
| 757 * @param {FileEntry} newEntry The new Entry. | |
| 758 */ | |
| 759 ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) { | |
| 760 if (util.isSameEntry(oldEntry, newEntry)) | |
| 761 return; // No need to rename. | |
| 762 | |
| 763 var pos = this.order_.indexOf(oldEntry.toURL()); | |
| 764 if (pos < 0) | |
| 765 return; // Not cached. | |
| 766 | |
| 767 this.order_[pos] = newEntry.toURL(); | |
| 768 this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()]; | |
| 769 delete this.map_[oldEntry.toURL()]; | |
| 770 }; | |
| 771 | |
| 772 /** | |
| 773 * Disposes an object. | |
| 774 * | |
| 775 * @param {Object} item The item object. | |
| 776 * @private | |
| 777 */ | |
| 778 ImageView.Cache.prototype.deleteItem_ = function(item) { | |
| 779 // Trick to reduce memory usage without waiting for gc. | |
| 780 if (item instanceof HTMLCanvasElement) { | |
| 781 // If the canvas is being used somewhere else (eg. displayed on the screen), | |
| 782 // it will be cleared. | |
| 783 item.width = 0; | |
| 784 item.height = 0; | |
| 785 } | |
| 786 }; | |
| 787 | |
| 788 /* Transition effects */ | 656 /* Transition effects */ |
| 789 | 657 |
| 790 /** | 658 /** |
| 791 * Base class for effects. | 659 * Base class for effects. |
| 792 * | 660 * |
| 793 * @param {number} duration Duration in ms. | 661 * @param {number} duration Duration in ms. |
| 794 * @param {string=} opt_timing CSS transition timing function name. | 662 * @param {string=} opt_timing CSS transition timing function name. |
| 795 * @constructor | 663 * @constructor |
| 796 */ | 664 */ |
| 797 ImageView.Effect = function(duration, opt_timing) { | 665 ImageView.Effect = function(duration, opt_timing) { |
| (...skipping 167 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 965 }; | 833 }; |
| 966 | 834 |
| 967 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype }; | 835 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype }; |
| 968 | 836 |
| 969 /** | 837 /** |
| 970 * @override | 838 * @override |
| 971 */ | 839 */ |
| 972 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) { | 840 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) { |
| 973 return viewport.getInverseTransformForRotatedImage(this.orientation_); | 841 return viewport.getInverseTransformForRotatedImage(this.orientation_); |
| 974 }; | 842 }; |
| OLD | NEW |