cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / media / media_util.js
bloba4c28348048d77bb57920a3e5f50db4611468918
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.
5 'use strict';
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;
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;
32   if (!opt_metadata) {
33     this.thumbnailUrl_ = url;  // Use the URL directly.
34     return;
35   }
37   this.fallbackUrl_ = null;
38   this.thumbnailUrl_ = null;
39   if (opt_metadata.drive && opt_metadata.drive.customIconUrl)
40     this.fallbackUrl_ = opt_metadata.drive.customIconUrl;
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   }
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.
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;
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.
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.
96 /**
97  * Type of element to store the image.
98  * @enum {number}
99  */
100 ThumbnailLoader.LoaderType = {
101   IMAGE: 0,
102   CANVAS: 1
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
116  * Maximum thumbnail's width when generating from the full resolution image.
117  * @const
118  * @type {number}
119  */
120 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500;
123  * Maximum thumbnail's height when generating from the full resolution image.
124  * @const
125  * @type {number}
126  */
127 ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500;
130  * Loads and attaches an image.
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;
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   }
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);
177   if (this.image_.src) {
178     console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_);
179     return;
180   }
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       });
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   }
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);
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);
235  * @return {number} Image width (corrected for rotation).
236  */
237 ThumbnailLoader.prototype.getWidth = function() {
238   return this.isRotated_() ? this.image_.height : this.image_.width;
242  * @return {number} Image height (corrected for rotation).
243  */
244 ThumbnailLoader.prototype.getHeight = function() {
245   return this.isRotated_() ? this.image_.width : this.image_.height;
249  * Load an image but do not attach it.
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   }
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);
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 });
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;
289   if (!this.canvas_)
290     this.canvas_ = document.createElement('canvas');
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   }
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   }
313   this.renderMedia_();
314   util.applyTransform(container, this.transform_);
315   var attachableMedia = this.loaderType_ == ThumbnailLoader.LoaderType.CANVAS ?
316       this.canvas_ : this.image_;
318   ThumbnailLoader.centerImage_(
319       container, attachableMedia, fillMode, this.isRotated_());
321   if (attachableMedia.parentNode != container) {
322     container.textContent = '';
323     container.appendChild(attachableMedia);
324   }
326   if (!this.taskId_)
327     attachableMedia.classList.add('cached');
331  * Gets the loaded image.
332  * TODO(mtomasz): Apply transformations.
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_;
343  * Update the image style to fit/fill the container.
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.
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;
359   var fractionX;
360   var fractionY;
362   var boxWidth = box.clientWidth;
363   var boxHeight = box.clientHeight;
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   }
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;
392     var scale = fill ?
393         Math.max(fitScaleX, fitScaleY) :
394         Math.min(fitScaleX, fitScaleY);
396     if (fillMode != ThumbnailLoader.FillMode.OVER_FILL)
397         scale = Math.min(scale, 1);  // Never overscale.
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   }
413   function percent(fraction) {
414     return (fraction * 100).toFixed(2) + '%';
415   }
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);