• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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