diff options
Diffstat (limited to 'chrome')
23 files changed, 777 insertions, 103 deletions
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 |