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 |