| OLD | NEW |
| (Empty) |
| 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 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 */ | |
| 74 ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3; | |
| 75 | |
| 76 /** | |
| 77 * Type of displaying a thumbnail within a box. | |
| 78 * @enum {number} | |
| 79 */ | |
| 80 ThumbnailLoader.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 */ | |
| 91 ThumbnailLoader.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 */ | |
| 100 ThumbnailLoader.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 */ | |
| 110 ThumbnailLoader.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 */ | |
| 120 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500; | |
| 121 | |
| 122 /** | |
| 123 * Maximum thumbnail's height when generating from the full resolution image. | |
| 124 * @const | |
| 125 * @type {number} | |
| 126 */ | |
| 127 ThumbnailLoader.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 */ | |
| 141 ThumbnailLoader.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 */ | |
| 210 ThumbnailLoader.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 */ | |
| 222 ThumbnailLoader.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 */ | |
| 230 ThumbnailLoader.prototype.isRotated_ = function() { | |
| 231 return this.transform_ && (this.transform_.rotate90 % 2 == 1); | |
| 232 }; | |
| 233 | |
| 234 /** | |
| 235 * @return {number} Image width (corrected for rotation). | |
| 236 */ | |
| 237 ThumbnailLoader.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 */ | |
| 244 ThumbnailLoader.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 */ | |
| 254 ThumbnailLoader.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 */ | |
| 285 ThumbnailLoader.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 */ | |
| 307 ThumbnailLoader.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 */ | |
| 336 ThumbnailLoader.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 */ | |
| 355 ThumbnailLoader.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 }; | |
| OLD | NEW |