28a15bd105a67d8abd4a5a9b0c1011eb4318d08f
[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 '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.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  */
81 ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3;
82
83 /**
84  * Type of displaying a thumbnail within a box.
85  * @enum {number}
86  */
87 ThumbnailLoader.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  */
98 ThumbnailLoader.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  */
107 ThumbnailLoader.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  */
117 ThumbnailLoader.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  */
127 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500;
128
129 /**
130  * Maximum thumbnail's height when generating from the full resolution image.
131  * @const
132  * @type {number}
133  */
134 ThumbnailLoader.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  */
148 ThumbnailLoader.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  */
213 ThumbnailLoader.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  */
225 ThumbnailLoader.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  */
233 ThumbnailLoader.prototype.isRotated_ = function() {
234   return this.transform_ && (this.transform_.rotate90 % 2 === 1);
235 };
236
237 /**
238  * @return {number} Image width (corrected for rotation).
239  */
240 ThumbnailLoader.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  */
247 ThumbnailLoader.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  */
257 ThumbnailLoader.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  */
288 ThumbnailLoader.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  */
310 ThumbnailLoader.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  */
339 ThumbnailLoader.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  */
358 ThumbnailLoader.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 };