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