diff options
author | mtomasz@chromium.org <mtomasz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-22 11:17:25 +0000 |
---|---|---|
committer | mtomasz@chromium.org <mtomasz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-22 11:17:25 +0000 |
commit | 77a84826ef757c200e459d0d6985e796c7590014 (patch) | |
tree | 9f2a0969e73d3713fb76e074352233392dbf6437 | |
parent | 466a583aa3b19736c510b0c10d481eb3787a6c8c (diff) | |
download | chromium_src-77a84826ef757c200e459d0d6985e796c7590014.zip chromium_src-77a84826ef757c200e459d0d6985e796c7590014.tar.gz chromium_src-77a84826ef757c200e459d0d6985e796c7590014.tar.bz2 |
Introduce Image loader extension.
Files.app's ui used to be unresponsive, when displaying large images. This was caused because of contex.drawImage() synchronous
call on the UI thread, which takes sometimes even 300 ms per one picture. In case of loading more images it causes ui freezes.
This patch solves this issue by introducing an image loader extension, which loads and resizes images asynchronously.
With this fix, we will be able to enable mosaic view on each volume, not only on Drive.
TEST=Check thumbnails in Files.app, including photo importer and gallery.
BUG=175697, 176237, 168035
Review URL: https://chromiumcodereview.appspot.com/12304013
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@184088 0039d316-1c4b-4281-b951-d872f2087c98
24 files changed, 790 insertions, 104 deletions
diff --git a/build/common.gypi b/build/common.gypi index a1aa02f..6048a19 100644 --- a/build/common.gypi +++ b/build/common.gypi @@ -195,6 +195,9 @@ # Disable file manager component extension by default. 'file_manager_extension%': 0, + # Disable image loader component extension by default. + 'image_loader_extension%': 0, + # Python version. 'python_ver%': '2.6', @@ -472,11 +475,13 @@ 'use_titlecase_in_grd_files%': 1, }], - # Enable file manager extension on Chrome OS. + # Enable file manager and image loader extensions on Chrome OS. ['chromeos==1', { 'file_manager_extension%': 1, + 'image_loader_extension%': 1, }, { 'file_manager_extension%': 0, + 'image_loader_extension%': 0, }], ['OS=="win" or OS=="mac" or (OS=="linux" and chromeos==0)', { @@ -687,6 +692,7 @@ 'enable_touch_ui%': '<(enable_touch_ui)', 'use_xi2_mt%':'<(use_xi2_mt)', 'file_manager_extension%': '<(file_manager_extension)', + 'image_loader_extension%': '<(image_loader_extension)', 'inside_chromium_build%': '<(inside_chromium_build)', 'fastbuild%': '<(fastbuild)', 'dcheck_always_on%': '<(dcheck_always_on)', @@ -1389,6 +1395,9 @@ ['file_manager_extension==1', { 'grit_defines': ['-D', 'file_manager_extension'], }], + ['image_loader_extension==1', { + 'grit_defines': ['-D', 'image_loader_extension'], + }], ['remoting==1', { 'grit_defines': ['-D', 'remoting'], }], @@ -1736,6 +1745,9 @@ ['file_manager_extension==1', { 'defines': ['FILE_MANAGER_EXTENSION=1'], }], + ['image_loader_extension==1', { + 'defines': ['IMAGE_LOADER_EXTENSION=1'], + }], ['profiling==1', { 'defines': ['ENABLE_PROFILING=1'], }], diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd index 464f8ef..cf41df8 100644 --- a/chrome/browser/browser_resources.grd +++ b/chrome/browser/browser_resources.grd @@ -294,6 +294,9 @@ <include name="IDR_FILEMANAGER_MANIFEST" file="resources\file_manager\manifest.json" type="BINDATA" /> <include name="IDR_FILEMANAGER_MANIFEST_V1" file="resources\file_manager\manifest_v1.json" type="BINDATA" /> </if> + <if expr="pp_ifdef('image_loader_extension')"> + <include name="IDR_IMAGE_LOADER_MANIFEST" file="resources\image_loader\manifest.json" type="BINDATA" /> + </if> <if expr="pp_ifdef('chromeos')"> <include name="IDR_WALLPAPERMANAGER_MANIFEST" file="resources\chromeos\wallpaper_manager\manifest.json" type="BINDATA" /> </if> diff --git a/chrome/browser/extensions/component_loader.cc b/chrome/browser/extensions/component_loader.cc index 247eecc..cf1f4da 100644 --- a/chrome/browser/extensions/component_loader.cc +++ b/chrome/browser/extensions/component_loader.cc @@ -254,6 +254,22 @@ void ComponentLoader::AddFileManagerExtension() { #endif // defined(FILE_MANAGER_EXTENSION) } +void ComponentLoader::AddImageLoaderExtension() { +#if defined(IMAGE_LOADER_EXTENSION) +#ifndef NDEBUG + const CommandLine* command_line = CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(switches::kImageLoaderExtensionPath)) { + base::FilePath image_loader_extension_path( + command_line->GetSwitchValuePath(switches::kImageLoaderExtensionPath)); + Add(IDR_IMAGE_LOADER_MANIFEST, image_loader_extension_path); + return; + } +#endif // NDEBUG + Add(IDR_IMAGE_LOADER_MANIFEST, + base::FilePath(FILE_PATH_LITERAL("image_loader"))); +#endif // defined(IMAGE_LOADER_EXTENSION) +} + #if defined(OS_CHROMEOS) void ComponentLoader::AddGaiaAuthExtension() { const CommandLine* command_line = CommandLine::ForCurrentProcess(); @@ -390,6 +406,7 @@ void ComponentLoader::AddDefaultComponentExtensionsWithBackgroundPages( } AddFileManagerExtension(); + AddImageLoaderExtension(); #if defined(ENABLE_SETTINGS_APP) Add(IDR_SETTINGS_APP_MANIFEST, diff --git a/chrome/browser/extensions/component_loader.h b/chrome/browser/extensions/component_loader.h index 8a02e31..58b998f 100644 --- a/chrome/browser/extensions/component_loader.h +++ b/chrome/browser/extensions/component_loader.h @@ -117,6 +117,7 @@ class ComponentLoader { void AddDefaultComponentExtensionsWithBackgroundPages( bool skip_session_components); void AddFileManagerExtension(); + void AddImageLoaderExtension(); #if defined(OS_CHROMEOS) void AddGaiaAuthExtension(); diff --git a/chrome/browser/resources/component_extension_resources.grd b/chrome/browser/resources/component_extension_resources.grd index 0b9e0b0..1e74a7d 100644 --- a/chrome/browser/resources/component_extension_resources.grd +++ b/chrome/browser/resources/component_extension_resources.grd @@ -104,6 +104,10 @@ <include name="IDR_FILE_MANAGER_IMG_GALLERY_2X_CURSOR_UPDOWN" file="file_manager/images/gallery/2x/cursor_updown.png" type="BINDATA" /> </if> + <if expr="pp_ifdef('image_loader_extension')"> + <include name="IDR_IMAGE_LOADER_MAIN_JS" file="image_loader/image_loader.js" type="BINDATA" /> + <include name="IDR_IMAGE_LOADER_CLIENT_JS" file="image_loader/client.js" type="BINDATA" /> + </if> <if expr="pp_ifdef('enable_google_now')"> <include name="IDR_GOOGLE_NOW_BACKGROUND_JS" file="google_now/background.js" type="BINDATA" /> </if> diff --git a/chrome/browser/resources/file_manager/css/photo_import.css b/chrome/browser/resources/file_manager/css/photo_import.css index 565419f..41a1254 100644 --- a/chrome/browser/resources/file_manager/css/photo_import.css +++ b/chrome/browser/resources/file_manager/css/photo_import.css @@ -151,6 +151,21 @@ button.import { position: absolute; } +.img-container > img:not(.cached) { + -webkit-animation: fadeIn ease-in 1; + -webkit-animation-duration: 100ms; + -webkit-animation-fill-mode: forwards; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity :1; + } +} + .grid-item[lead] { border: 2px solid transparent !important; } diff --git a/chrome/browser/resources/file_manager/gallery.html b/chrome/browser/resources/file_manager/gallery.html index 40f08ec..ae3542f 100644 --- a/chrome/browser/resources/file_manager/gallery.html +++ b/chrome/browser/resources/file_manager/gallery.html @@ -19,6 +19,8 @@ Keep the list in sync with gallery_scripts.js. --> <script src="js/metrics.js"></script> + <script src="../image_loader/client.js"></script> + <script src="../../../../ui/webui/resources/js/cr.js"></script> <script src="../../../../ui/webui/resources/js/event_tracker.js"></script> <script src="../../../../ui/webui/resources/js/load_time_data.js"></script> diff --git a/chrome/browser/resources/file_manager/js/action_choice.js b/chrome/browser/resources/file_manager/js/action_choice.js index d14d52b..39ce80d 100644 --- a/chrome/browser/resources/file_manager/js/action_choice.js +++ b/chrome/browser/resources/file_manager/js/action_choice.js @@ -220,7 +220,12 @@ ActionChoice.prototype.renderPreview_ = function(entries, count) { new ThumbnailLoader(entry.toURL(), ThumbnailLoader.LoaderType.IMAGE, metadata).load( - box, ThumbnailLoader.FillMode.FILL, onSuccess, onError, onError); + box, + ThumbnailLoader.OptimizationMode.DISCARD_DETACHED, + ThumbnailLoader.FillMode.FILL, + onSuccess, + onError, + onError); }); }; diff --git a/chrome/browser/resources/file_manager/js/file_selection.js b/chrome/browser/resources/file_manager/js/file_selection.js index deffef8..e030cb6 100644 --- a/chrome/browser/resources/file_manager/js/file_selection.js +++ b/chrome/browser/resources/file_manager/js/file_selection.js @@ -597,6 +597,7 @@ FileSelectionHandler.prototype.renderThumbnail_ = function(entry, callback) { entry, this.fileManager_.metadataCache_, ThumbnailLoader.FillMode.FILL, + ThumbnailLoader.OptimizationMode.NEVER_DISCARD, callback); return thumbnail; }; diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_util.js b/chrome/browser/resources/file_manager/js/image_editor/image_util.js index 39e375b..acdbb81 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_util.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_util.js @@ -487,7 +487,7 @@ ImageUtil.ImageLoader.prototype.load = function( // errorCallback has an optional error argument, which in case of general // error should not be specified this.image_.onerror = errorCallback.bind(this, 'IMAGE_ERROR'); - this.remoteLoader_ = util.loadImage(this.image_, url); + this.taskId_ = util.loadImage(this.image_, url); }.bind(this); if (opt_delay) { this.timeout_ = setTimeout(startLoad, opt_delay); @@ -533,10 +533,8 @@ ImageUtil.ImageLoader.prototype.cancel = function() { this.image_.onerror = function() {}; this.image_.src = ''; } - if (this.remoteLoader_) { - this.remoteLoader_.cancel(); - this.remoteLoader_ = null; - } + if (this.taskId_) + util.cancelLoadImage(this.taskId_); this.generation_++; // Silence the transform fetcher if it is in progress. }; diff --git a/chrome/browser/resources/file_manager/js/main_scripts.js b/chrome/browser/resources/file_manager/js/main_scripts.js index cdbfe70..2570550 100644 --- a/chrome/browser/resources/file_manager/js/main_scripts.js +++ b/chrome/browser/resources/file_manager/js/main_scripts.js @@ -12,6 +12,8 @@ // //so we want to parse it as early as possible. //<include src="metrics.js"/> // +//<include src="../../image_loader/client.js"/> +// //<include src="../../../../../ui/webui/resources/js/load_time_data.js"/> //<include src="../../../../../ui/webui/resources/js/cr.js"/> //<include src="../../../../../ui/webui/resources/js/util.js"/> diff --git a/chrome/browser/resources/file_manager/js/media/media_util.js b/chrome/browser/resources/file_manager/js/media/media_util.js index b9934bc..126b6b2 100644 --- a/chrome/browser/resources/file_manager/js/media/media_util.js +++ b/chrome/browser/resources/file_manager/js/media/media_util.js @@ -38,8 +38,7 @@ function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType) { if (opt_metadata.thumbnail && opt_metadata.thumbnail.url) { this.thumbnailUrl_ = opt_metadata.thumbnail.url; this.transform_ = opt_metadata.thumbnail.transform; - } else if (FileType.isImage(url) && - ThumbnailLoader.canUseImageUrl_(opt_metadata)) { + } else if (FileType.isImage(url)) { this.thumbnailUrl_ = url; this.transform_ = opt_metadata.media && opt_metadata.media.imageTransform; } else if (this.fallbackUrl_) { @@ -50,16 +49,6 @@ function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType) { } /** - * Files with more pixels won't have thumbnails. - */ -ThumbnailLoader.MAX_PIXEL_COUNT = 1 << 21; // 2 MPix - -/** - * Files of bigger size won't have thumbnails. - */ -ThumbnailLoader.MAX_FILE_SIZE = 1 << 20; // 1 Mb - -/** * In percents (0.0 - 1.0), how much area can be cropped to fill an image * in a container, when loading a thumbnail in FillMode.AUTO mode. * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element. @@ -78,6 +67,15 @@ ThumbnailLoader.FillMode = { }; /** + * Optimization mode for downloading thumbnails. + * @enum + */ +ThumbnailLoader.OptimizationMode = { + NEVER_DISCARD: 0, // Never discards downloading. No optimization. + DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore. +}; + +/** * Type of element to store the image. * @enum */ @@ -87,33 +85,36 @@ ThumbnailLoader.LoaderType = { }; /** - * If an image file does not have an embedded thumbnail we might want to use - * the image itself as a thumbnail. If the image is too large it hurts - * the performance a lot so we allow it only for moderately sized files. - * - * @param {Object} metadata Metadata object. - * @return {boolean} Whether it is OK to use the image url for a preview. - * @private + * Maximum thumbnail's width when generating from the full resolution image. + * @const + * @type {number} */ -ThumbnailLoader.canUseImageUrl_ = function(metadata) { - return (metadata.filesystem && metadata.filesystem.size && - metadata.filesystem.size <= ThumbnailLoader.MAX_FILE_SIZE) || - (metadata.media && metadata.media.width && metadata.media.height && - metadata.media.width * metadata.media.height <= - ThumbnailLoader.MAX_PIXEL_COUNT); -}; +ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500; /** + * Maximum thumbnail's height when generating from the full resolution image. + * @const + * @type {number} + */ +ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500; + +/** + * Loads and attaches an image. * * @param {HTMLElement} box Container element. * @param {ThumbnailLoader.FillMode} fillMode Fill mode. + * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization + * for downloading thumbnails. By default optimizations are disabled. * @param {function(Image, object} opt_onSuccess Success callback, - * accepts the image and the transform. + * accepts the image and the transform. * @param {function} opt_onError Error callback. * @param {function} opt_onGeneric Callback for generic image used. */ -ThumbnailLoader.prototype.load = function( - box, fillMode, opt_onSuccess, opt_onError, opt_onGeneric) { +ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, + opt_onSuccess, opt_onError, opt_onGeneric) { + opt_optimizationMode = opt_optimizationMode || + ThumbnailLoader.OptimizationMode.NEVER_DISCARD; + if (!this.thumbnailUrl_) { // Relevant CSS rules are in file_types.css. box.setAttribute('generic-thumbnail', this.mediaType_); @@ -142,12 +143,31 @@ ThumbnailLoader.prototype.load = function( } }.bind(this); - if (this.image_.src == this.thumbnailUrl_) { - console.warn('Thumnbnail already loaded: ' + this.thumbnailUrl_); + if (this.image_.src) { + console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_); return; } - util.loadImage(this.image_, this.thumbnailUrl_); + // TODO(mtomasz): Smarter calculation of the requested size. + var wasAttached = box.ownerDocument.contains(box); + var taskId = util.loadImage( + this.image_, + this.thumbnailUrl_, + { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, + maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, + cache: true }, + function() { + if (opt_optimizationMode == + ThumbnailLoader.OptimizationMode.DISCARD_DETACHED && + !box.ownerDocument.contains(box)) { + // If the container is not attached, then invalidate the download. + return false; + } + return true; + }); + + if (!taskId) + this.image_.classList.add('cached'); }; /** @@ -195,7 +215,17 @@ ThumbnailLoader.prototype.loadDetachedImage = function(callback) { this.image_ = new Image(); this.image_.onload = callback.bind(null, true); this.image_.onerror = callback.bind(null, false); - util.loadImage(this.image_, this.thumbnailUrl_); + + // TODO(mtomasz): Smarter calculation of the requested size. + var taskId = util.loadImage( + this.image_, + this.thumbnailUrl_, + { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, + maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, + cache: true }); + + if (!taskId) + this.image_.classList.add('cached'); }; /** diff --git a/chrome/browser/resources/file_manager/js/photo/gallery_scripts.js b/chrome/browser/resources/file_manager/js/photo/gallery_scripts.js index 8dc7f0c..e369001 100644 --- a/chrome/browser/resources/file_manager/js/photo/gallery_scripts.js +++ b/chrome/browser/resources/file_manager/js/photo/gallery_scripts.js @@ -10,6 +10,8 @@ //<include src="../metrics.js"> +//<include src="../../../image_loader/client.js"/> + //<include src="../../../../../../ui/webui/resources/js/cr.js"> //<include src="../../../../../../ui/webui/resources/js/event_tracker.js"> //<include src="../../../../../../ui/webui/resources/js/load_time_data.js"> diff --git a/chrome/browser/resources/file_manager/js/photo/photo_import.js b/chrome/browser/resources/file_manager/js/photo/photo_import.js index c0c795e..54f0667 100644 --- a/chrome/browser/resources/file_manager/js/photo/photo_import.js +++ b/chrome/browser/resources/file_manager/js/photo/photo_import.js @@ -336,7 +336,8 @@ PhotoImport.prototype.decorateGridItem_ = function(li, entry) { new ThumbnailLoader(entry.toURL(), ThumbnailLoader.LoaderType.IMAGE, metadata). - load(box, ThumbnailLoader.FillMode.FIT); + load(box, ThumbnailLoader.FillMode.FIT, + ThumbnailLoader.OptimizationMode.DISCARD_DETACHED); }); frame.appendChild(box); diff --git a/chrome/browser/resources/file_manager/js/photo/photo_import_scripts.js b/chrome/browser/resources/file_manager/js/photo/photo_import_scripts.js index 00db271..cd7d591 100644 --- a/chrome/browser/resources/file_manager/js/photo/photo_import_scripts.js +++ b/chrome/browser/resources/file_manager/js/photo/photo_import_scripts.js @@ -8,6 +8,8 @@ // included file but that's all right since any javascript file should start // with a copyright comment anyway. +//<include src="../../../image_loader/client.js"/> + //<include src="../../../../../../ui/webui/resources/js/load_time_data.js"/> //<include src="../../../../../../ui/webui/resources/js/util.js"/> //<include src="../../../../../../ui/webui/resources/js/i18n_template_no_process.js"/> diff --git a/chrome/browser/resources/file_manager/js/photo/ribbon.js b/chrome/browser/resources/file_manager/js/photo/ribbon.js index 66c3689..9f595c0 100644 --- a/chrome/browser/resources/file_manager/js/photo/ribbon.js +++ b/chrome/browser/resources/file_manager/js/photo/ribbon.js @@ -333,6 +333,7 @@ Ribbon.prototype.setThumbnailImage_ = function(thumbnail, url, metadata) { new ThumbnailLoader(url, ThumbnailLoader.LoaderType.IMAGE, metadata).load( thumbnail.querySelector('.image-wrapper'), ThumbnailLoader.FillMode.FILL /* fill */, + ThumbnailLoader.OptimizationMode.NEVER_DISCARD, null /* success callback */, this.onThumbnailError_.bind(null, url)); }; diff --git a/chrome/browser/resources/file_manager/js/util.js b/chrome/browser/resources/file_manager/js/util.js index 0d600db..ecda8931 100644 --- a/chrome/browser/resources/file_manager/js/util.js +++ b/chrome/browser/resources/file_manager/js/util.js @@ -1173,76 +1173,32 @@ util.AppCache.cleanup_ = function(map) { }; /** - * RemoteImageLoader loads an image from a remote url. + * Load an image. * - * Fetches a blob via XHR, converts it to a data: url and assigns to img.src. - * @constructor - */ -util.RemoteImageLoader = function() {}; - -/** * @param {Image} image Image element. - * @param {string} url Remote url to load into the image. - */ -util.RemoteImageLoader.prototype.load = function(image, url) { - this.onSuccess_ = function(dataURL) { image.src = dataURL }; - this.onError_ = function() { image.onerror() }; - - var xhr = new XMLHttpRequest(); - xhr.responseType = 'blob'; - xhr.onload = function() { - if (xhr.status == 200) { - var reader = new FileReader; - reader.onload = function(e) { - this.onSuccess_(e.target.result); - }.bind(this); - reader.onerror = this.onError_; - reader.readAsDataURL(xhr.response); - } else { - this.onError_(); - } - }.bind(this); - xhr.onerror = this.onError_; - - try { - xhr.open('GET', url, true); - xhr.send(); - } catch (e) { - console.log(e); - this.onError_(); - } -}; - -/** - * Cancels the loading. + * @param {string} url Source url. + * @param {Object=} opt_options Hash array of options, eg. width, height, + * maxWidth, maxHeight, scale, cache. + * @param {function()=} opt_isValid Function returning false iff the task + * is not valid and should be aborted. + * @return {?number} Task identifier or null if fetched immediately from + * cache. */ -util.RemoteImageLoader.prototype.cancel = function() { - // We cannot really cancel the XHR.send and FileReader.readAsDataURL, - // silencing the callbacks instead. - this.onSuccess_ = this.onError_ = function() {}; +util.loadImage = function(image, url, opt_options, opt_isValid) { + return ImageLoader.Client.loadToImage(url, + image, + opt_options || {}, + function() { }, + function() { image.onerror(); }, + opt_isValid); }; /** - * Load an image. - * - * In packaged apps img.src is not allowed to point to http(s)://. - * For such urls util.RemoteImageLoader is used. - * - * @param {Image} image Image element. - * @param {string} url Source url. - * @return {util.RemoteImageLoader?} RemoteImageLoader object reference, use it - * to cancel the loading. + * Cancels loading an image. + * @param {number} taskId Task identifier returned by util.loadImage(). */ -util.loadImage = function(image, url) { - if (util.platform.v2() && url.match(/^http(s):/)) { - var imageLoader = new util.RemoteImageLoader(); - imageLoader.load(image, url); - return imageLoader; - } - - // OK to load directly. - image.src = url; - return null; +util.cancelLoadImage = function(taskId) { + ImageLoader.Client.getInstance().cancel(taskId); }; /** diff --git a/chrome/browser/resources/file_manager/main.html b/chrome/browser/resources/file_manager/main.html index 82f5050..d2f455d 100644 --- a/chrome/browser/resources/file_manager/main.html +++ b/chrome/browser/resources/file_manager/main.html @@ -34,6 +34,8 @@ so we want to parse it as early as possible --> <script src="js/metrics.js"></script> + <script src="../image_loader/client.js"></script> + <script src="../../../../ui/webui/resources/js/load_time_data.js"></script> <script src="../../../../ui/webui/resources/js/cr.js"></script> <script src="../../../../ui/webui/resources/js/util.js"></script> diff --git a/chrome/browser/resources/file_manager/photo_import.html b/chrome/browser/resources/file_manager/photo_import.html index ba5a146..2d500b6 100644 --- a/chrome/browser/resources/file_manager/photo_import.html +++ b/chrome/browser/resources/file_manager/photo_import.html @@ -21,6 +21,8 @@ <if expr="0"> <!-- This file has not been flattened, load individual scripts. Keep the list in sync with photo_import_scripts.js. --> + <script src="../image_loader/client.js"></script> + <script src="../../../../ui/webui/resources/js/load_time_data.js"></script> <script src="../../../../ui/webui/resources/js/util.js"></script> <script src="../../../../ui/webui/resources/js/i18n_template_no_process.js"></script> diff --git a/chrome/browser/resources/image_loader/client.js b/chrome/browser/resources/image_loader/client.js new file mode 100644 index 0000000..c2c7149 --- /dev/null +++ b/chrome/browser/resources/image_loader/client.js @@ -0,0 +1,302 @@ +// 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. + +var ImageLoader = ImageLoader || {}; + +/** + * Image loader's extension id. + * @const + * @type {string} + */ +ImageLoader.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; + +/** + * Client used to connect to the remote ImageLoader extension. Client class runs + * in the extension, where the client.js is included (eg. Files.app). + * It sends remote requests using IPC to the ImageLoader class and forwards + * its responses. + * + * Implements cache, which is stored in the calling extension. + * + * @constructor + */ +ImageLoader.Client = function() { + /** + * @type {Port} + * @private + */ + this.port_ = chrome.extension.connect(ImageLoader.EXTENSION_ID); + this.port_.onMessage.addListener(this.handleMessage_.bind(this)); + + /** + * Hash array with active tasks. + * @type {Object} + * @private + */ + this.tasks_ = {}; + + /** + * @type {number} + * @private + */ + this.lastTaskId_ = 0; + + /** + * LRU cache for images. + * @type {ImageLoader.Client.Cache} + * @private + */ + this.cache_ = new ImageLoader.Client.Cache(); +}; + +/** + * Returns a singleton instance. + * @return {ImageLoader.Client} ImageLoader.Client instance. + */ +ImageLoader.Client.getInstance = function() { + if (!ImageLoader.Client.instance_) + ImageLoader.Client.instance_ = new ImageLoader.Client(); + return ImageLoader.Client.instance_; +}; + +/** + * Handles a message from the remote image loader and calls the registered + * callback to pass the response back to the requester. + * + * @param {Object} message Response message as a hash array. + * @private + */ +ImageLoader.Client.prototype.handleMessage_ = function(message) { + if (!(message.taskId in this.tasks_)) { + // This task has been canceled, but was already fetched, so it's result + // should be discarded anyway. + return; + } + + var task = this.tasks_[message.taskId]; + + // Check if the task is still valid. + if (task.isValid()) + task.accept(message); + + delete this.tasks_[message.taskId]; +}; + +/** + * Loads and resizes and image. Use opt_isValid to easily cancel requests + * which are not valid anymore, which will reduce cpu consumption. + * + * @param {string} url Url of the requested image. + * @param {function} callback Callback used to return response. + * @param {Object=} opt_options Loader options, such as: scale, maxHeight, + * width, height and/or cache. + * @param {function=} opt_isValid Function returning false in case + * a request is not valid anymore, eg. parent node has been detached. + * @return {?number} Remote task id or null if loaded from cache. + */ +ImageLoader.Client.prototype.load = function( + url, callback, opt_options, opt_isValid) { + opt_options = opt_options || {}; + opt_isValid = opt_isValid || function() { return true; }; + + // Cancel old, invalid tasks. + var taskKeys = Object.keys(this.tasks_); + for (var index = 0; index < taskKeys.length; index++) { + var taskKey = taskKeys[index]; + var task = this.tasks_[taskKey]; + if (!task.isValid()) { + // Cancel this task since it is not valid anymore. + this.cancel(taskKey); + delete this.tasks_[taskKey]; + } + } + + // Replace the extension id. + var sourceId = chrome.i18n.getMessage('@@extension_id'); + var targetId = ImageLoader.EXTENSION_ID; + + url = url.replace('filesystem:chrome-extension://' + sourceId, + 'filesystem:chrome-extension://' + targetId); + + // Try to load from cache, if available. + var cacheKey = ImageLoader.Client.Cache.createKey(url, opt_options); + if (opt_options.cache) { + // Load from cache. + // TODO(mtomasz): Add cache invalidating if the file has changed. + var cachedData = this.cache_.loadImage(cacheKey); + if (cachedData) { + callback({ status: 'success', data: cachedData }); + return null; + } + } else { + // Remove from cache. + this.cache_.removeImage(cacheKey); + } + + // Not available in cache, performing a request to a remote extension. + request = opt_options; + this.lastTaskId_++; + var task = { isValid: opt_isValid, accept: function(result) { + // Save to cache. + if (result.status == 'success' && opt_options.cache) + this.cache_.saveImage(cacheKey, result.data); + callback(result); + }.bind(this) }; + this.tasks_[this.lastTaskId_] = task; + + request.url = url; + request.taskId = this.lastTaskId_; + + this.port_.postMessage(request); + return request.taskId; +}; + +/** + * Cancels the request. + * @param {number} taskId Task id returned by ImageLoader.Client.load(). + */ +ImageLoader.Client.prototype.cancel = function(taskId) { + this.port_.postMessage({ taskId: taskId, cancel: true }); +}; + +/** + * Prints the cache usage statistics. + */ +ImageLoader.Client.prototype.stat = function() { + this.cache_.stat(); +}; + +/** + * Least Recently Used (LRU) cache implementation to be used by + * ImageLoader.Client class. It has memory constraints, so it will never + * exceed specified memory limit defined in MEMORY_LIMIT. + * + * @constructor + */ +ImageLoader.Client.Cache = function() { + this.images_ = []; + this.size_ = 0; +}; + +/** + * Memory limit for images data in bytes. + * + * @const + * @type {number} + */ +ImageLoader.Client.Cache.MEMORY_LIMIT = 100 * 1024 * 1024; // 100 MB. + +/** + * Creates a cache key. + * + * @param {string} url Image url. + * @param {Object=} opt_options Loader options as a hash array. + * @return {string} Cache key. + */ +ImageLoader.Client.Cache.createKey = function(url, opt_options) { + var array = opt_options || {}; + array.url = url; + return JSON.stringify(array); +}; + +/** + * Evicts the least used elements in cache to make space for a new image. + * + * @param {number} size Requested size. + * @private + */ +ImageLoader.Client.Cache.prototype.evictCache_ = function(size) { + // Sort from the most recent to the oldest. + this.images_.sort(function(a, b) { + return b.timestamp - a.timestamp; + }); + + while (this.images_.length > 0 && + (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ < size)) { + var entry = this.images_.pop(); + this.size_ -= entry.data.length; + } +}; + +/** + * Saves an image in the cache. + * + * @param {string} key Cache key. + * @param {string} data Image data. + */ +ImageLoader.Client.Cache.prototype.saveImage = function(key, data) { + this.evictCache_(data.length); + if (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ >= data.length) { + this.images_[key] = { timestamp: Date.now(), data: data }; + this.size_ += data.length; + } +}; + +/** + * Loads an image from the cache (if available) or returns null. + * + * @param {string} key Cache key. + * @return {?string} Data of the loaded image or null. + */ +ImageLoader.Client.Cache.prototype.loadImage = function(key) { + if (!(key in this.images_)) + return null; + + var entry = this.images_[key]; + entry.timestamp = Date.now(); + return entry.data; +}; + +/** + * Prints the cache usage stats. + */ +ImageLoader.Client.Cache.prototype.stat = function() { + console.log('Cache entries: ' + Object.keys(this.images_).length); + console.log('Usage: ' + Math.round(this.size_ / + ImageLoader.Client.Cache.MEMORY_LIMIT * 100.0) + '%'); +}; + +/** + * Removes the image from the cache. + * @param {string} key Cache key. + */ +ImageLoader.Client.Cache.prototype.removeImage = function(key) { + if (!(key in this.images_)) + return; + + var entry = this.images_[key]; + this.size_ -= entry.data.length; + delete this.images_[key]; +}; + +// Helper functions. + +/** + * Loads and resizes and image. Use opt_isValid to easily cancel requests + * which are not valid anymore, which will reduce cpu consumption. + * + * @param {string} url Url of the requested image. + * @param {Image} image Image node to load the requested picture into. + * @param {Object} options Loader options, such as: scale, maxHeight, width, + * height and/or cache. + * @param {function=} onSuccess Callback for success. + * @param {function=} onError Callback for failure. + * @param {function=} opt_isValid Function returning false in case + * a request is not valid anymore, eg. parent node has been detached. + * @return {?number} Remote task id or null if loaded from cache. + */ +ImageLoader.Client.loadToImage = function(url, image, options, onSuccess, + onError, opt_isValid) { + var callback = function(result) { + if (result.status == 'error') { + onError(); + return; + } + image.src = result.data; + onSuccess(); + }; + + return ImageLoader.Client.getInstance().load( + url, callback, options, opt_isValid); +}; diff --git a/chrome/browser/resources/image_loader/image_loader.js b/chrome/browser/resources/image_loader/image_loader.js new file mode 100644 index 0000000..f3a5e97 --- /dev/null +++ b/chrome/browser/resources/image_loader/image_loader.js @@ -0,0 +1,305 @@ +// 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. + +/** + * Loads and resizes an image. + * @constructor + */ +var ImageLoader = function() { + /** + * Hash array of active requests. + * @type {Object} + * @private + */ + this.requests_ = {}; + + chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) { + // TODO(mtomasz): Handle. + }); + + chrome.extension.onConnectExternal.addListener(function(port) { + if (ImageLoader.ALLOWED_CLIENTS.indexOf(port.sender.id) !== -1) + port.onMessage.addListener(function(request) { + this.onMessage_(port, request, port.postMessage.bind(port)); + }.bind(this)); + }.bind(this)); +}; + +/** + * List of extensions allowed to perform image requests. + * + * @const + * @type {Array.<string>} + */ +ImageLoader.ALLOWED_CLIENTS = + ['hhaomjibdihmijegdhdafkllkbggdgoj']; // File Manager's extension id. + +/** + * Handles a request. Depending on type of the request, starts or stops + * an image task. + * + * @param {Port} port Connection port. + * @param {Object} request Request message as a hash array. + * @param {function} callback Callback to be called to return response. + * @private + */ +ImageLoader.prototype.onMessage_ = function(port, request, callback) { + var requestId = port.sender.id + ':' + request.taskId; + if (request.cancel) { + // Cancel a task. + if (requestId in this.requests_) { + this.requests_[requestId].cancel(); + delete this.requests_[requestId]; + } + } else { + // Start a task. + this.requests_[requestId] = + new ImageLoader.Request(request, callback); + } +}; + +/** + * Returns the singleton instance. + * @return {ImageLoader} ImageLoader object. + */ +ImageLoader.getInstance = function() { + if (!ImageLoader.instance_) + ImageLoader.instance_ = new ImageLoader(); + return ImageLoader.instance_; +}; + +/** + * Calculates dimensions taking into account resize options, such as: + * - scale: for scaling, + * - maxWidth, maxHeight: for maximum dimensions, + * - width, height: for exact requested size. + * Returns the target size as hash array with width, height properties. + * + * @param {number} width Source width. + * @param {number} height Source height. + * @param {Object} options Resizing options as a hash array. + * @return {Object} Dimensions, eg. { width: 100, height: 50 }. + */ +ImageLoader.resizeDimensions = function(width, height, options) { + var sourceWidth = width; + var sourceHeight = height; + + var targetWidth = sourceWidth; + var targetHeight = sourceHeight; + + if ('scale' in options) { + targetWidth = sourceWidth * options.scale; + targetHeight = sourceHeight * options.scale; + } + + if (options.maxWidth && + targetWidth > options.maxWidth) { + var scale = options.maxWidth / targetWidth; + targetWidth *= scale; + targetHeight *= scale; + } + + if (options.maxHeight && + targetHeight > options.maxHeight) { + var scale = options.maxHeight / targetHeight; + targetWidth *= scale; + targetHeight *= scale; + } + + if (options.width) + targetWidth = options.width; + + if (options.height) + targetHeight = options.height; + + targetWidth = Math.round(targetWidth); + targetHeight = Math.round(targetHeight); + + return { width: targetWidth, height: targetHeight }; +}; + +/** + * Performs resizing of the source image into the target canvas. + * + * @param {HTMLCanvasElement|Image} source Source image or canvas. + * @param {HTMLCanvasElement} target Target canvas. + * @param {Object} options Resizing options as a hash array. + */ +ImageLoader.resize = function(source, target, options) { + var targetDimensions = ImageLoader.resizeDimensions( + source.width, source.height, options); + + target.width = targetDimensions.width; + target.height = targetDimensions.height; + + var targetContext = target.getContext('2d'); + targetContext.drawImage(source, + 0, 0, source.width, source.height, + 0, 0, target.width, target.height); +}; + +/** + * Creates and starts downloading and then resizing of the image. Finally, + * returns the image using the callback. + * + * @param {Object} request Request message as a hash array. + * @param {function} callback Callback used to send the response. + * @constructor + */ +ImageLoader.Request = function(request, callback) { + /** + * @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(); + + /** + * 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'); + + this.downloadOriginal_(); +}; + +/** + * Downloads an image directly or for remote resources using the XmlHttpRequest. + * @private + */ +ImageLoader.Request.prototype.downloadOriginal_ = function() { + this.image_.onload = this.onImageLoad_.bind(this); + this.image_.onerror = this.onImageError_.bind(this); + + if (window.harness || !this.request_.url.match(/^https?:/)) { + // Download directly. + this.image_.src = this.request_.url; + 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 returnes data. + 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. + * @private + */ +ImageLoader.Request.prototype.sendImage_ = function() { + // TODO(mtomasz): Keep format. Never compress using jpeg codec for lossless + // images such as png, gif. + var pngData = this.canvas_.toDataURL('image/png'); + var jpegData = this.canvas_.toDataURL('image/jpeg', 0.9); + var imageData = pngData.length < jpegData.length * 2 ? pngData : jpegData; + this.sendResponse_({ status: 'success', + data: imageData, + taskId: this.request_.taskId }); +}; + +/** + * Handler, when contents are loaded into the image element. Performs resizing + * and finalizes the request process. + * + * @private + */ +ImageLoader.Request.prototype.onImageLoad_ = function() { + ImageLoader.resize(this.image_, this.canvas_, this.request_); + this.sendImage_(); + this.cleanup_(); +}; + +/** + * Handler, when loading of the image fails. Sends a failure response and + * finalizes the request process. + * + * @private + */ +ImageLoader.Request.prototype.onImageError_ = function() { + this.sendResponse_({ status: 'error', + taskId: this.request_.taskId }); + this.cleanup_(); +}; + +/** + * Cancels the request. + */ +ImageLoader.Request.prototype.cancel = function() { + this.cleanup_(); +}; + +/** + * Cleans up memory used by this request. + * @private + */ +ImageLoader.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; +}; + +// Load the extension. +ImageLoader.getInstance(); diff --git a/chrome/browser/resources/image_loader/manifest.json b/chrome/browser/resources/image_loader/manifest.json new file mode 100644 index 0000000..f95e63c --- /dev/null +++ b/chrome/browser/resources/image_loader/manifest.json @@ -0,0 +1,19 @@ +{ + // chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp + "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDowC9B4+K6zbl4PnALNyOUgra/MPdD8gZ39Fk/IAJWt03qrN7vz1gd/mmrBg0EEIsyLRmUmfyVEfvcIUOZxFqn4A9D2aaRSvNHy9qkasZMBDEql8Nt2iNZm/kGS7sizidDV6Bc/vyLNiH1gKOXBQ42JIxKjgtrmnhGV2giw2vJGwIDAQAB", + "name": "Image loader", + "version": "0.1", + "description": "Image loader", + "incognito" : "split", + "manifest_version": 2, + "permissions": [ + "fileBrowserHandler", + "fileBrowserPrivate", + "https://*.googleusercontent.com", + "https://drive.google.com" + ], + "content_security_policy": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' about:; img-src 'self' chrome://resources chrome://theme data: https://docs.google.com https://*.googleusercontent.com chrome://extension-icon; media-src 'self' https://*.googleusercontent.com; connect-src https://drive.google.com https://*.googleusercontent.com", + "background": { + "scripts": ["image_loader.js" ] + } +} diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc index 06f8901..5250503 100644 --- a/chrome/common/chrome_switches.cc +++ b/chrome/common/chrome_switches.cc @@ -1650,6 +1650,9 @@ const char kWaitForMutex[] = "wait-for-mutex"; // Enables overriding the path of file manager extension. const char kFileManagerExtensionPath[] = "filemgr-ext-path"; +// Enables overriding the path of image loader extension. +const char kImageLoaderExtensionPath[] = "image-loader-ext-path"; + // Dumps dependency information about our profile services into a dot file in // the profile directory. const char kDumpProfileDependencyGraph[] = "dump-profile-graph"; diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h index 4e4daef..ad00c29 100644 --- a/chrome/common/chrome_switches.h +++ b/chrome/common/chrome_switches.h @@ -468,6 +468,7 @@ extern const char kWaitForMutex[]; #ifndef NDEBUG extern const char kFileManagerExtensionPath[]; +extern const char kImageLoaderExtensionPath[]; extern const char kDumpProfileDependencyGraph[]; #endif |