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