Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / media / media_util.js
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 {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  */
22 function 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.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(entry)) {
58     this.thumbnailUrl_ = entry.toURL();
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       this.thumbnailUrl_ = this.fallbackUrl_;
166       this.fallbackUrl_ = null;
167       this.load(box, fillMode, opt_optimizationMode, opt_onSuccess);
168     } else {
169       box.setAttribute('generic-thumbnail', this.mediaType_);
170     }
171   }.bind(this);
172
173   if (this.image_.src) {
174     console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_);
175     return;
176   }
177
178   // TODO(mtomasz): Smarter calculation of the requested size.
179   var wasAttached = box.ownerDocument.contains(box);
180   var modificationTime = this.metadata_ &&
181                          this.metadata_.filesystem &&
182                          this.metadata_.filesystem.modificationTime &&
183                          this.metadata_.filesystem.modificationTime.getTime();
184   this.taskId_ = util.loadImage(
185       this.image_,
186       this.thumbnailUrl_,
187       { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
188         maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
189         cache: true,
190         priority: this.priority_,
191         timestamp: modificationTime },
192       function() {
193         if (opt_optimizationMode ==
194             ThumbnailLoader.OptimizationMode.DISCARD_DETACHED &&
195             !box.ownerDocument.contains(box)) {
196           // If the container is not attached, then invalidate the download.
197           return false;
198         }
199         return true;
200       });
201 };
202
203 /**
204  * Cancels loading the current image.
205  */
206 ThumbnailLoader.prototype.cancel = function() {
207   if (this.taskId_) {
208     this.image_.onload = function() {};
209     this.image_.onerror = function() {};
210     util.cancelLoadImage(this.taskId_);
211     this.taskId_ = null;
212   }
213 };
214
215 /**
216  * @return {boolean} True if a valid image is loaded.
217  */
218 ThumbnailLoader.prototype.hasValidImage = function() {
219   return !!(this.image_ && this.image_.width && this.image_.height);
220 };
221
222 /**
223  * @return {boolean} True if the image is rotated 90 degrees left or right.
224  * @private
225  */
226 ThumbnailLoader.prototype.isRotated_ = function() {
227   return this.transform_ && (this.transform_.rotate90 % 2 === 1);
228 };
229
230 /**
231  * @return {number} Image width (corrected for rotation).
232  */
233 ThumbnailLoader.prototype.getWidth = function() {
234   return this.isRotated_() ? this.image_.height : this.image_.width;
235 };
236
237 /**
238  * @return {number} Image height (corrected for rotation).
239  */
240 ThumbnailLoader.prototype.getHeight = function() {
241   return this.isRotated_() ? this.image_.width : this.image_.height;
242 };
243
244 /**
245  * Load an image but do not attach it.
246  *
247  * @param {function(boolean)} callback Callback, parameter is true if the image
248  *     has loaded successfully or a stock icon has been used.
249  */
250 ThumbnailLoader.prototype.loadDetachedImage = function(callback) {
251   if (!this.thumbnailUrl_) {
252     callback(true);
253     return;
254   }
255
256   this.cancel();
257   this.canvasUpToDate_ = false;
258   this.image_ = new Image();
259   this.image_.onload = callback.bind(null, true);
260   this.image_.onerror = callback.bind(null, false);
261
262   // TODO(mtomasz): Smarter calculation of the requested size.
263   var modificationTime = this.metadata_ &&
264                          this.metadata_.filesystem &&
265                          this.metadata_.filesystem.modificationTime &&
266                          this.metadata_.filesystem.modificationTime.getTime();
267   this.taskId_ = util.loadImage(
268       this.image_,
269       this.thumbnailUrl_,
270       { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
271         maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
272         cache: true,
273         priority: this.priority_,
274         timestamp: modificationTime });
275 };
276
277 /**
278  * Renders the thumbnail into either canvas or an image element.
279  * @private
280  */
281 ThumbnailLoader.prototype.renderMedia_ = function() {
282   if (this.loaderType_ !== ThumbnailLoader.LoaderType.CANVAS)
283     return;
284
285   if (!this.canvas_)
286     this.canvas_ = document.createElement('canvas');
287
288   // Copy the image to a canvas if the canvas is outdated.
289   if (!this.canvasUpToDate_) {
290     this.canvas_.width = this.image_.width;
291     this.canvas_.height = this.image_.height;
292     var context = this.canvas_.getContext('2d');
293     context.drawImage(this.image_, 0, 0);
294     this.canvasUpToDate_ = true;
295   }
296 };
297
298 /**
299  * Attach the image to a given element.
300  * @param {Element} container Parent element.
301  * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
302  */
303 ThumbnailLoader.prototype.attachImage = function(container, fillMode) {
304   if (!this.hasValidImage()) {
305     container.setAttribute('generic-thumbnail', this.mediaType_);
306     return;
307   }
308
309   this.renderMedia_();
310   util.applyTransform(container, this.transform_);
311   var attachableMedia = this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ?
312       this.canvas_ : this.image_;
313
314   ThumbnailLoader.centerImage_(
315       container, attachableMedia, fillMode, this.isRotated_());
316
317   if (attachableMedia.parentNode !== container) {
318     container.textContent = '';
319     container.appendChild(attachableMedia);
320   }
321
322   if (!this.taskId_)
323     attachableMedia.classList.add('cached');
324 };
325
326 /**
327  * Gets the loaded image.
328  * TODO(mtomasz): Apply transformations.
329  *
330  * @return {Image|HTMLCanvasElement} Either image or a canvas object.
331  */
332 ThumbnailLoader.prototype.getImage = function() {
333   this.renderMedia_();
334   return this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ :
335       this.image_;
336 };
337
338 /**
339  * Update the image style to fit/fill the container.
340  *
341  * Using webkit center packing does not align the image properly, so we need
342  * to wait until the image loads and its dimensions are known, then manually
343  * position it at the center.
344  *
345  * @param {HTMLElement} box Containing element.
346  * @param {Image|HTMLCanvasElement} img Element containing an image.
347  * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
348  * @param {boolean} rotate True if the image should be rotated 90 degrees.
349  * @private
350  */
351 ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) {
352   var imageWidth = img.width;
353   var imageHeight = img.height;
354
355   var fractionX;
356   var fractionY;
357
358   var boxWidth = box.clientWidth;
359   var boxHeight = box.clientHeight;
360
361   var fill;
362   switch (fillMode) {
363     case ThumbnailLoader.FillMode.FILL:
364     case ThumbnailLoader.FillMode.OVER_FILL:
365       fill = true;
366       break;
367     case ThumbnailLoader.FillMode.FIT:
368       fill = false;
369       break;
370     case ThumbnailLoader.FillMode.AUTO:
371       var imageRatio = imageWidth / imageHeight;
372       var boxRatio = 1.0;
373       if (boxWidth && boxHeight)
374         boxRatio = boxWidth / boxHeight;
375       // Cropped area in percents.
376       var ratioFactor = boxRatio / imageRatio;
377       fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) &&
378              (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD);
379       break;
380   }
381
382   if (boxWidth && boxHeight) {
383     // When we know the box size we can position the image correctly even
384     // in a non-square box.
385     var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth;
386     var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight;
387
388     var scale = fill ?
389         Math.max(fitScaleX, fitScaleY) :
390         Math.min(fitScaleX, fitScaleY);
391
392     if (fillMode !== ThumbnailLoader.FillMode.OVER_FILL)
393         scale = Math.min(scale, 1);  // Never overscale.
394
395     fractionX = imageWidth * scale / boxWidth;
396     fractionY = imageHeight * scale / boxHeight;
397   } else {
398     // We do not know the box size so we assume it is square.
399     // Compute the image position based only on the image dimensions.
400     // First try vertical fit or horizontal fill.
401     fractionX = imageWidth / imageHeight;
402     fractionY = 1;
403     if ((fractionX < 1) === !!fill) {  // Vertical fill or horizontal fit.
404       fractionY = 1 / fractionX;
405       fractionX = 1;
406     }
407   }
408
409   function percent(fraction) {
410     return (fraction * 100).toFixed(2) + '%';
411   }
412
413   img.style.width = percent(fractionX);
414   img.style.height = percent(fractionY);
415   img.style.left = percent((1 - fractionX) / 2);
416   img.style.top = percent((1 - fractionY) / 2);
417 };