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