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 3// found in the LICENSE file. 4 5'use strict'; 6 7/** 8 * Loads a thumbnail using provided url. In CANVAS mode, loaded images 9 * are attached as <canvas> element, while in IMAGE mode as <img>. 10 * <canvas> renders faster than <img>, however has bigger memory overhead. 11 * 12 * @param {FileEntry} entry File entry. 13 * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader, 14 * default: IMAGE. 15 * @param {Object=} opt_metadata Metadata object. 16 * @param {string=} opt_mediaType Media type. 17 * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded 18 * jpeg thumbnail if available. Default: USE_EMBEDDED. 19 * @param {number=} opt_priority Priority, the highest is 0. default: 2. 20 * @constructor 21 */ 22function ThumbnailLoader(entry, opt_loaderType, opt_metadata, opt_mediaType, 23 opt_useEmbedded, opt_priority) { 24 opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED; 25 26 this.mediaType_ = opt_mediaType || FileType.getMediaType(entry); 27 this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE; 28 this.metadata_ = opt_metadata; 29 this.priority_ = (opt_priority !== undefined) ? opt_priority : 2; 30 this.transform_ = null; 31 32 if (!opt_metadata) { 33 this.thumbnailUrl_ = entry.toURL(); // Use the URL directly. 34 return; 35 } 36 37 this.fallbackUrl_ = null; 38 this.thumbnailUrl_ = null; 39 if (opt_metadata.external && opt_metadata.external.customIconUrl) 40 this.fallbackUrl_ = opt_metadata.external.customIconUrl; 41 42 // Fetch the rotation from the external properties (if available). 43 var externalTransform; 44 if (opt_metadata.external && 45 opt_metadata.external.imageRotation !== undefined) { 46 externalTransform = { 47 scaleX: 1, 48 scaleY: 1, 49 rotate90: opt_metadata.external.imageRotation / 90 50 }; 51 } 52 53 if (((opt_metadata.thumbnail && opt_metadata.thumbnail.url) || 54 (opt_metadata.external && opt_metadata.external.thumbnailUrl)) && 55 opt_useEmbedded === ThumbnailLoader.UseEmbedded.USE_EMBEDDED) { 56 // If the thumbnail generated from the local cache (metadata.thumbnail.url) 57 // is available, use it. If not, use the one passed from the external 58 // provider (metadata.external.thumbnailUrl). 59 this.thumbnailUrl_ = 60 (opt_metadata.thumbnail && opt_metadata.thumbnail.url) || 61 (opt_metadata.external && opt_metadata.external.thumbnailUrl); 62 this.transform_ = externalTransform !== undefined ? externalTransform : 63 (opt_metadata.thumbnail && opt_metadata.thumbnail.transform); 64 } else if (FileType.isImage(entry)) { 65 this.thumbnailUrl_ = entry.toURL(); 66 this.transform_ = externalTransform !== undefined ? externalTransform : 67 opt_metadata.media && opt_metadata.media.imageTransform; 68 } else if (this.fallbackUrl_) { 69 // Use fallback as the primary thumbnail. 70 this.thumbnailUrl_ = this.fallbackUrl_; 71 this.fallbackUrl_ = null; 72 } // else the generic thumbnail based on the media type will be used. 73} 74 75/** 76 * In percents (0.0 - 1.0), how much area can be cropped to fill an image 77 * in a container, when loading a thumbnail in FillMode.AUTO mode. 78 * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element. 79 * @type {number} 80 */ 81ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3; 82 83/** 84 * Type of displaying a thumbnail within a box. 85 * @enum {number} 86 */ 87ThumbnailLoader.FillMode = { 88 FILL: 0, // Fill whole box. Image may be cropped. 89 FIT: 1, // Keep aspect ratio, do not crop. 90 OVER_FILL: 2, // Fill whole box with possible stretching. 91 AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit. 92}; 93 94/** 95 * Optimization mode for downloading thumbnails. 96 * @enum {number} 97 */ 98ThumbnailLoader.OptimizationMode = { 99 NEVER_DISCARD: 0, // Never discards downloading. No optimization. 100 DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore. 101}; 102 103/** 104 * Type of element to store the image. 105 * @enum {number} 106 */ 107ThumbnailLoader.LoaderType = { 108 IMAGE: 0, 109 CANVAS: 1 110}; 111 112/** 113 * Whether to use the embedded thumbnail, or not. The embedded thumbnail may 114 * be small. 115 * @enum {number} 116 */ 117ThumbnailLoader.UseEmbedded = { 118 USE_EMBEDDED: 0, 119 NO_EMBEDDED: 1 120}; 121 122/** 123 * Maximum thumbnail's width when generating from the full resolution image. 124 * @const 125 * @type {number} 126 */ 127ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500; 128 129/** 130 * Maximum thumbnail's height when generating from the full resolution image. 131 * @const 132 * @type {number} 133 */ 134ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500; 135 136/** 137 * Loads and attaches an image. 138 * 139 * @param {HTMLElement} box Container element. 140 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. 141 * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization 142 * for downloading thumbnails. By default optimizations are disabled. 143 * @param {function(Image, Object)=} opt_onSuccess Success callback, 144 * accepts the image and the transform. 145 * @param {function()=} opt_onError Error callback. 146 * @param {function()=} opt_onGeneric Callback for generic image used. 147 */ 148ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, 149 opt_onSuccess, opt_onError, opt_onGeneric) { 150 opt_optimizationMode = opt_optimizationMode || 151 ThumbnailLoader.OptimizationMode.NEVER_DISCARD; 152 153 if (!this.thumbnailUrl_) { 154 // Relevant CSS rules are in file_types.css. 155 box.setAttribute('generic-thumbnail', this.mediaType_); 156 if (opt_onGeneric) opt_onGeneric(); 157 return; 158 } 159 160 this.cancel(); 161 this.canvasUpToDate_ = false; 162 this.image_ = new Image(); 163 this.image_.onload = function() { 164 this.attachImage(box, fillMode); 165 if (opt_onSuccess) 166 opt_onSuccess(this.image_, this.transform_); 167 }.bind(this); 168 this.image_.onerror = function() { 169 if (opt_onError) 170 opt_onError(); 171 if (this.fallbackUrl_) { 172 this.thumbnailUrl_ = this.fallbackUrl_; 173 this.fallbackUrl_ = null; 174 this.load(box, fillMode, opt_optimizationMode, opt_onSuccess); 175 } else { 176 box.setAttribute('generic-thumbnail', this.mediaType_); 177 } 178 }.bind(this); 179 180 if (this.image_.src) { 181 console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_); 182 return; 183 } 184 185 // TODO(mtomasz): Smarter calculation of the requested size. 186 var wasAttached = box.ownerDocument.contains(box); 187 var modificationTime = this.metadata_ && 188 this.metadata_.filesystem && 189 this.metadata_.filesystem.modificationTime && 190 this.metadata_.filesystem.modificationTime.getTime(); 191 this.taskId_ = util.loadImage( 192 this.image_, 193 this.thumbnailUrl_, 194 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, 195 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, 196 cache: true, 197 priority: this.priority_, 198 timestamp: modificationTime }, 199 function() { 200 if (opt_optimizationMode == 201 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED && 202 !box.ownerDocument.contains(box)) { 203 // If the container is not attached, then invalidate the download. 204 return false; 205 } 206 return true; 207 }); 208}; 209 210/** 211 * Cancels loading the current image. 212 */ 213ThumbnailLoader.prototype.cancel = function() { 214 if (this.taskId_) { 215 this.image_.onload = function() {}; 216 this.image_.onerror = function() {}; 217 util.cancelLoadImage(this.taskId_); 218 this.taskId_ = null; 219 } 220}; 221 222/** 223 * @return {boolean} True if a valid image is loaded. 224 */ 225ThumbnailLoader.prototype.hasValidImage = function() { 226 return !!(this.image_ && this.image_.width && this.image_.height); 227}; 228 229/** 230 * @return {boolean} True if the image is rotated 90 degrees left or right. 231 * @private 232 */ 233ThumbnailLoader.prototype.isRotated_ = function() { 234 return this.transform_ && (this.transform_.rotate90 % 2 === 1); 235}; 236 237/** 238 * @return {number} Image width (corrected for rotation). 239 */ 240ThumbnailLoader.prototype.getWidth = function() { 241 return this.isRotated_() ? this.image_.height : this.image_.width; 242}; 243 244/** 245 * @return {number} Image height (corrected for rotation). 246 */ 247ThumbnailLoader.prototype.getHeight = function() { 248 return this.isRotated_() ? this.image_.width : this.image_.height; 249}; 250 251/** 252 * Load an image but do not attach it. 253 * 254 * @param {function(boolean)} callback Callback, parameter is true if the image 255 * has loaded successfully or a stock icon has been used. 256 */ 257ThumbnailLoader.prototype.loadDetachedImage = function(callback) { 258 if (!this.thumbnailUrl_) { 259 callback(true); 260 return; 261 } 262 263 this.cancel(); 264 this.canvasUpToDate_ = false; 265 this.image_ = new Image(); 266 this.image_.onload = callback.bind(null, true); 267 this.image_.onerror = callback.bind(null, false); 268 269 // TODO(mtomasz): Smarter calculation of the requested size. 270 var modificationTime = this.metadata_ && 271 this.metadata_.filesystem && 272 this.metadata_.filesystem.modificationTime && 273 this.metadata_.filesystem.modificationTime.getTime(); 274 this.taskId_ = util.loadImage( 275 this.image_, 276 this.thumbnailUrl_, 277 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, 278 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, 279 cache: true, 280 priority: this.priority_, 281 timestamp: modificationTime }); 282}; 283 284/** 285 * Renders the thumbnail into either canvas or an image element. 286 * @private 287 */ 288ThumbnailLoader.prototype.renderMedia_ = function() { 289 if (this.loaderType_ !== ThumbnailLoader.LoaderType.CANVAS) 290 return; 291 292 if (!this.canvas_) 293 this.canvas_ = document.createElement('canvas'); 294 295 // Copy the image to a canvas if the canvas is outdated. 296 if (!this.canvasUpToDate_) { 297 this.canvas_.width = this.image_.width; 298 this.canvas_.height = this.image_.height; 299 var context = this.canvas_.getContext('2d'); 300 context.drawImage(this.image_, 0, 0); 301 this.canvasUpToDate_ = true; 302 } 303}; 304 305/** 306 * Attach the image to a given element. 307 * @param {Element} container Parent element. 308 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. 309 */ 310ThumbnailLoader.prototype.attachImage = function(container, fillMode) { 311 if (!this.hasValidImage()) { 312 container.setAttribute('generic-thumbnail', this.mediaType_); 313 return; 314 } 315 316 this.renderMedia_(); 317 util.applyTransform(container, this.transform_); 318 var attachableMedia = this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? 319 this.canvas_ : this.image_; 320 321 ThumbnailLoader.centerImage_( 322 container, attachableMedia, fillMode, this.isRotated_()); 323 324 if (attachableMedia.parentNode !== container) { 325 container.textContent = ''; 326 container.appendChild(attachableMedia); 327 } 328 329 if (!this.taskId_) 330 attachableMedia.classList.add('cached'); 331}; 332 333/** 334 * Gets the loaded image. 335 * TODO(mtomasz): Apply transformations. 336 * 337 * @return {Image|HTMLCanvasElement} Either image or a canvas object. 338 */ 339ThumbnailLoader.prototype.getImage = function() { 340 this.renderMedia_(); 341 return this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ : 342 this.image_; 343}; 344 345/** 346 * Update the image style to fit/fill the container. 347 * 348 * Using webkit center packing does not align the image properly, so we need 349 * to wait until the image loads and its dimensions are known, then manually 350 * position it at the center. 351 * 352 * @param {HTMLElement} box Containing element. 353 * @param {Image|HTMLCanvasElement} img Element containing an image. 354 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. 355 * @param {boolean} rotate True if the image should be rotated 90 degrees. 356 * @private 357 */ 358ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) { 359 var imageWidth = img.width; 360 var imageHeight = img.height; 361 362 var fractionX; 363 var fractionY; 364 365 var boxWidth = box.clientWidth; 366 var boxHeight = box.clientHeight; 367 368 var fill; 369 switch (fillMode) { 370 case ThumbnailLoader.FillMode.FILL: 371 case ThumbnailLoader.FillMode.OVER_FILL: 372 fill = true; 373 break; 374 case ThumbnailLoader.FillMode.FIT: 375 fill = false; 376 break; 377 case ThumbnailLoader.FillMode.AUTO: 378 var imageRatio = imageWidth / imageHeight; 379 var boxRatio = 1.0; 380 if (boxWidth && boxHeight) 381 boxRatio = boxWidth / boxHeight; 382 // Cropped area in percents. 383 var ratioFactor = boxRatio / imageRatio; 384 fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) && 385 (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD); 386 break; 387 } 388 389 if (boxWidth && boxHeight) { 390 // When we know the box size we can position the image correctly even 391 // in a non-square box. 392 var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth; 393 var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight; 394 395 var scale = fill ? 396 Math.max(fitScaleX, fitScaleY) : 397 Math.min(fitScaleX, fitScaleY); 398 399 if (fillMode !== ThumbnailLoader.FillMode.OVER_FILL) 400 scale = Math.min(scale, 1); // Never overscale. 401 402 fractionX = imageWidth * scale / boxWidth; 403 fractionY = imageHeight * scale / boxHeight; 404 } else { 405 // We do not know the box size so we assume it is square. 406 // Compute the image position based only on the image dimensions. 407 // First try vertical fit or horizontal fill. 408 fractionX = imageWidth / imageHeight; 409 fractionY = 1; 410 if ((fractionX < 1) === !!fill) { // Vertical fill or horizontal fit. 411 fractionY = 1 / fractionX; 412 fractionX = 1; 413 } 414 } 415 416 function percent(fraction) { 417 return (fraction * 100).toFixed(2) + '%'; 418 } 419 420 img.style.width = percent(fractionX); 421 img.style.height = percent(fractionY); 422 img.style.left = percent((1 - fractionX) / 2); 423 img.style.top = percent((1 - fractionY) / 2); 424}; 425