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