Home | History | Annotate | Download | only in media
      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