Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / image_editor / image_util.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 // Namespace object for the utilities.
6 function ImageUtil() {}
7
8 /**
9  * Performance trace.
10  */
11 ImageUtil.trace = (function() {
12   function PerformanceTrace() {
13     this.lines_ = {};
14     this.timers_ = {};
15     this.container_ = null;
16   }
17
18   PerformanceTrace.prototype.bindToDOM = function(container) {
19     this.container_ = container;
20   };
21
22   PerformanceTrace.prototype.report = function(key, value) {
23     if (!(key in this.lines_)) {
24       if (this.container_) {
25         var div = this.lines_[key] = document.createElement('div');
26         this.container_.appendChild(div);
27       } else {
28         this.lines_[key] = {};
29       }
30     }
31     this.lines_[key].textContent = key + ': ' + value;
32     if (ImageUtil.trace.log) this.dumpLine(key);
33   };
34
35   PerformanceTrace.prototype.resetTimer = function(key) {
36     this.timers_[key] = Date.now();
37   };
38
39   PerformanceTrace.prototype.reportTimer = function(key) {
40     this.report(key, (Date.now() - this.timers_[key]) + 'ms');
41   };
42
43   PerformanceTrace.prototype.dump = function() {
44     for (var key in this.lines_)
45       this.dumpLine(key);
46   };
47
48   PerformanceTrace.prototype.dumpLine = function(key) {
49     console.log('trace.' + this.lines_[key].textContent);
50   };
51
52   return new PerformanceTrace();
53 })();
54
55 /**
56  * @param {number} min Minimum value.
57  * @param {number} value Value to adjust.
58  * @param {number} max Maximum value.
59  * @return {number} The closest to the |value| number in span [min, max].
60  */
61 ImageUtil.clamp = function(min, value, max) {
62   return Math.max(min, Math.min(max, value));
63 };
64
65 /**
66  * @param {number} min Minimum value.
67  * @param {number} value Value to check.
68  * @param {number} max Maximum value.
69  * @return {boolean} True if value is between.
70  */
71 ImageUtil.between = function(min, value, max) {
72   return (value - min) * (value - max) <= 0;
73 };
74
75 /**
76  * Rectangle class.
77  */
78
79 /**
80  * Rectangle constructor takes 0, 1, 2 or 4 arguments.
81  * Supports following variants:
82  *   new ImageRect(left, top, width, height)
83  *   new ImageRect(width, height)
84  *   new ImageRect(rect)         // anything with left, top, width, height.
85  *   new ImageRect(bounds)       // anything with left, top, right, bottom.
86  *   new ImageRect(canvas|image) // anything with width and height.
87  *   new ImageRect()             // empty rectangle.
88  * @constructor
89  */
90 function ImageRect() {
91   switch (arguments.length) {
92     case 4:
93       this.left = arguments[0];
94       this.top = arguments[1];
95       this.width = arguments[2];
96       this.height = arguments[3];
97       return;
98
99     case 2:
100       this.left = 0;
101       this.top = 0;
102       this.width = arguments[0];
103       this.height = arguments[1];
104       return;
105
106     case 1: {
107       var source = arguments[0];
108       if ('left' in source && 'top' in source) {
109         this.left = source.left;
110         this.top = source.top;
111         if ('right' in source && 'bottom' in source) {
112           this.width = source.right - source.left;
113           this.height = source.bottom - source.top;
114           return;
115         }
116       } else {
117         this.left = 0;
118         this.top = 0;
119       }
120       if ('width' in source && 'height' in source) {
121         this.width = source.width;
122         this.height = source.height;
123         return;
124       }
125       break; // Fall through to the error message.
126     }
127
128     case 0:
129       this.left = 0;
130       this.top = 0;
131       this.width = 0;
132       this.height = 0;
133       return;
134   }
135   console.error('Invalid ImageRect constructor arguments:',
136                 Array.apply(null, arguments));
137 }
138
139 ImageRect.prototype = {
140   /**
141    * Obtains the x coordinate of right edge. The most right pixels in the
142    * rectangle are (x = right - 1) and the pixels (x = right) are not included
143    * in the rectangle.
144    * @return {number}
145    */
146   get right() {
147     return this.left + this.width;
148   },
149
150   /**
151    * Obtains the y coordinate of bottom edge. The most bottom pixels in the
152    * rectangle are (y = bottom - 1) and the pixels (y = bottom) are not included
153    * in the rectangle.
154    * @return {number}
155    */
156   get bottom() {
157     return this.top + this.height;
158   }
159 };
160
161 /**
162  * @param {number} factor Factor to scale.
163  * @return {ImageRect} A rectangle with every dimension scaled.
164  */
165 ImageRect.prototype.scale = function(factor) {
166   return new ImageRect(
167       this.left * factor,
168       this.top * factor,
169       this.width * factor,
170       this.height * factor);
171 };
172
173 /**
174  * @param {number} dx Difference in X.
175  * @param {number} dy Difference in Y.
176  * @return {ImageRect} A rectangle shifted by (dx,dy), same size.
177  */
178 ImageRect.prototype.shift = function(dx, dy) {
179   return new ImageRect(this.left + dx, this.top + dy, this.width, this.height);
180 };
181
182 /**
183  * @param {number} x Coordinate of the left top corner.
184  * @param {number} y Coordinate of the left top corner.
185  * @return {ImageRect} A rectangle with left==x and top==y, same size.
186  */
187 ImageRect.prototype.moveTo = function(x, y) {
188   return new ImageRect(x, y, this.width, this.height);
189 };
190
191 /**
192  * @param {number} dx Difference in X.
193  * @param {number} dy Difference in Y.
194  * @return {ImageRect} A rectangle inflated by (dx, dy), same center.
195  */
196 ImageRect.prototype.inflate = function(dx, dy) {
197   return new ImageRect(
198       this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy);
199 };
200
201 /**
202  * @param {number} x Coordinate of the point.
203  * @param {number} y Coordinate of the point.
204  * @return {boolean} True if the point lies inside the rectangle.
205  */
206 ImageRect.prototype.inside = function(x, y) {
207   return this.left <= x && x < this.left + this.width &&
208          this.top <= y && y < this.top + this.height;
209 };
210
211 /**
212  * @param {ImageRect} rect Rectangle to check.
213  * @return {boolean} True if this rectangle intersects with the |rect|.
214  */
215 ImageRect.prototype.intersects = function(rect) {
216   return (this.left + this.width) > rect.left &&
217          (rect.left + rect.width) > this.left &&
218          (this.top + this.height) > rect.top &&
219          (rect.top + rect.height) > this.top;
220 };
221
222 /**
223  * @param {ImageRect} rect Rectangle to check.
224  * @return {boolean} True if this rectangle containing the |rect|.
225  */
226 ImageRect.prototype.contains = function(rect) {
227   return (this.left <= rect.left) &&
228          (rect.left + rect.width) <= (this.left + this.width) &&
229          (this.top <= rect.top) &&
230          (rect.top + rect.height) <= (this.top + this.height);
231 };
232
233 /**
234  * @return {boolean} True if rectangle is empty.
235  */
236 ImageRect.prototype.isEmpty = function() {
237   return this.width === 0 || this.height === 0;
238 };
239
240 /**
241  * Clamp the rectangle to the bounds by moving it.
242  * Decrease the size only if necessary.
243  * @param {ImageRect} bounds Bounds.
244  * @return {ImageRect} Calculated rectangle.
245  */
246 ImageRect.prototype.clamp = function(bounds) {
247   var rect = new ImageRect(this);
248
249   if (rect.width > bounds.width) {
250     rect.left = bounds.left;
251     rect.width = bounds.width;
252   } else if (rect.left < bounds.left) {
253     rect.left = bounds.left;
254   } else if (rect.left + rect.width >
255              bounds.left + bounds.width) {
256     rect.left = bounds.left + bounds.width - rect.width;
257   }
258
259   if (rect.height > bounds.height) {
260     rect.top = bounds.top;
261     rect.height = bounds.height;
262   } else if (rect.top < bounds.top) {
263     rect.top = bounds.top;
264   } else if (rect.top + rect.height >
265              bounds.top + bounds.height) {
266     rect.top = bounds.top + bounds.height - rect.height;
267   }
268
269   return rect;
270 };
271
272 /**
273  * @return {string} String representation.
274  */
275 ImageRect.prototype.toString = function() {
276   return '(' + this.left + ',' + this.top + '):' +
277          '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')';
278 };
279 /*
280  * Useful shortcuts for drawing (static functions).
281  */
282
283 /**
284  * Draw the image in context with appropriate scaling.
285  * @param {CanvasRenderingContext2D} context Context to draw.
286  * @param {Image} image Image to draw.
287  * @param {ImageRect=} opt_dstRect Rectangle in the canvas (whole canvas by
288  *     default).
289  * @param {ImageRect=} opt_srcRect Rectangle in the image (whole image by
290  *     default).
291  */
292 ImageRect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
293   opt_dstRect = opt_dstRect || new ImageRect(context.canvas);
294   opt_srcRect = opt_srcRect || new ImageRect(image);
295   if (opt_dstRect.isEmpty() || opt_srcRect.isEmpty())
296     return;
297   context.drawImage(image,
298       opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height,
299       opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height);
300 };
301
302 /**
303  * Draw a box around the rectangle.
304  * @param {CanvasRenderingContext2D} context Context to draw.
305  * @param {ImageRect} rect Rectangle.
306  */
307 ImageRect.outline = function(context, rect) {
308   context.strokeRect(
309       rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1);
310 };
311
312 /**
313  * Fill the rectangle.
314  * @param {CanvasRenderingContext2D} context Context to draw.
315  * @param {ImageRect} rect Rectangle.
316  */
317 ImageRect.fill = function(context, rect) {
318   context.fillRect(rect.left, rect.top, rect.width, rect.height);
319 };
320
321 /**
322  * Fills the space between the two rectangles.
323  * @param {CanvasRenderingContext2D} context Context to draw.
324  * @param {ImageRect} inner Inner rectangle.
325  * @param {ImageRect} outer Outer rectangle.
326  */
327 ImageRect.fillBetween = function(context, inner, outer) {
328   var innerRight = inner.left + inner.width;
329   var innerBottom = inner.top + inner.height;
330   var outerRight = outer.left + outer.width;
331   var outerBottom = outer.top + outer.height;
332   if (inner.top > outer.top) {
333     context.fillRect(
334         outer.left, outer.top, outer.width, inner.top - outer.top);
335   }
336   if (inner.left > outer.left) {
337     context.fillRect(
338         outer.left, inner.top, inner.left - outer.left, inner.height);
339   }
340   if (inner.width < outerRight) {
341     context.fillRect(
342         innerRight, inner.top, outerRight - innerRight, inner.height);
343   }
344   if (inner.height < outerBottom) {
345     context.fillRect(
346         outer.left, innerBottom, outer.width, outerBottom - innerBottom);
347   }
348 };
349
350 /**
351  * Circle class.
352  * @param {number} x X coordinate of circle center.
353  * @param {number} y Y coordinate of circle center.
354  * @param {number} r Radius.
355  * @constructor
356  */
357 function Circle(x, y, r) {
358   this.x = x;
359   this.y = y;
360   this.squaredR = r * r;
361 }
362
363 /**
364  * Check if the point is inside the circle.
365  * @param {number} x X coordinate of the point.
366  * @param {number} y Y coordinate of the point.
367  * @return {boolean} True if the point is inside.
368  */
369 Circle.prototype.inside = function(x, y) {
370   x -= this.x;
371   y -= this.y;
372   return x * x + y * y <= this.squaredR;
373 };
374
375 /**
376  * Copy an image applying scaling and rotation.
377  *
378  * @param {HTMLCanvasElement} dst Destination.
379  * @param {HTMLCanvasElement|HTMLImageElement} src Source.
380  * @param {number} scaleX Y scale transformation.
381  * @param {number} scaleY X scale transformation.
382  * @param {number} angle (in radians).
383  */
384 ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) {
385   var context = dst.getContext('2d');
386   context.save();
387   context.translate(context.canvas.width / 2, context.canvas.height / 2);
388   context.rotate(angle);
389   context.scale(scaleX, scaleY);
390   context.drawImage(src, -src.width / 2, -src.height / 2);
391   context.restore();
392 };
393
394 /**
395  * Adds or removes an attribute to/from an HTML element.
396  * @param {HTMLElement} element To be applied to.
397  * @param {string} attribute Name of attribute.
398  * @param {boolean} on True if add, false if remove.
399  */
400 ImageUtil.setAttribute = function(element, attribute, on) {
401   if (on)
402     element.setAttribute(attribute, '');
403   else
404     element.removeAttribute(attribute);
405 };
406
407 /**
408  * Adds or removes CSS class to/from an HTML element.
409  * @param {HTMLElement} element To be applied to.
410  * @param {string} className Name of CSS class.
411  * @param {boolean} on True if add, false if remove.
412  */
413 ImageUtil.setClass = function(element, className, on) {
414   var cl = element.classList;
415   if (on)
416     cl.add(className);
417   else
418     cl.remove(className);
419 };
420
421 /**
422  * ImageLoader loads an image from a given Entry into a canvas in two steps:
423  * 1. Loads the image into an HTMLImageElement.
424  * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done
425  *    stripe-by-stripe to avoid freezing up the UI. The transform is taken into
426  *    account.
427  *
428  * @param {HTMLDocument} document Owner document.
429  * @constructor
430  */
431 ImageUtil.ImageLoader = function(document) {
432   this.document_ = document;
433   this.image_ = new Image();
434   this.generation_ = 0;
435 };
436
437 /**
438  * Loads an image.
439  * TODO(mtomasz): Simplify, or even get rid of this class and merge with the
440  * ThumbnaiLoader class.
441  *
442  * @param {Gallery.Item} item Item representing the image to be loaded.
443  * @param {function(HTMLCanvasElement, string=)} callback Callback to be
444  *     called when loaded. The second optional argument is an error identifier.
445  * @param {number=} opt_delay Load delay in milliseconds, useful to let the
446  *     animations play out before the computation heavy image loading starts.
447  */
448 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) {
449   var entry = item.getEntry();
450
451   this.cancel();
452   this.entry_ = entry;
453   this.callback_ = callback;
454
455   // The transform fetcher is not cancellable so we need a generation counter.
456   var generation = ++this.generation_;
457   var onTransform = function(image, transform) {
458     if (generation === this.generation_) {
459       this.convertImage_(
460           image, transform || { scaleX: 1, scaleY: 1, rotate90: 0});
461     }
462   }.bind(this);
463
464   var onError = function(opt_error) {
465     this.image_.onerror = null;
466     this.image_.onload = null;
467     var tmpCallback = this.callback_;
468     this.callback_ = null;
469     var emptyCanvas = this.document_.createElement('canvas');
470     emptyCanvas.width = 0;
471     emptyCanvas.height = 0;
472     tmpCallback(emptyCanvas, opt_error);
473   }.bind(this);
474
475   var loadImage = function() {
476     ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime'));
477     this.timeout_ = null;
478
479     this.image_.onload = function() {
480       this.image_.onerror = null;
481       this.image_.onload = null;
482       item.getFetchedMedia().then(function(fetchedMediaMetadata) {
483         onTransform(this.image_, fetchedMediaMetadata.imageTransform);
484       }.bind(this)).catch(function(error) {
485         console.error(error.stack || error);
486       });
487     }.bind(this);
488
489     // The error callback has an optional error argument, which in case of a
490     // general error should not be specified
491     this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
492
493     // Load the image directly. The query parameter is workaround for
494     // crbug.com/379678, which force to update the contents of the image.
495     this.image_.src = entry.toURL() + '?nocache=' + Date.now();
496   }.bind(this);
497
498   // Loads the image. If already loaded, then forces a reload.
499   var startLoad = this.resetImage_.bind(this, function() {
500     loadImage();
501   }.bind(this), onError);
502
503   if (opt_delay) {
504     this.timeout_ = setTimeout(startLoad, opt_delay);
505   } else {
506     startLoad();
507   }
508 };
509
510 /**
511  * Resets the image by forcing the garbage collection and clearing the src
512  * attribute.
513  *
514  * @param {function()} onSuccess Success callback.
515  * @param {function(string=)} onError Failure callback with an optional error
516  *     identifier.
517  * @private
518  */
519 ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) {
520   var clearSrc = function() {
521     this.image_.onload = onSuccess;
522     this.image_.onerror = onSuccess;
523     this.image_.src = '';
524   }.bind(this);
525
526   var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' +
527       'AAABAAEAAAICTAEAOw==';
528
529   if (this.image_.src !== emptyImage) {
530     // Load an empty image, then clear src.
531     this.image_.onload = clearSrc;
532     this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
533     this.image_.src = emptyImage;
534   } else {
535     // Empty image already loaded, so clear src immediately.
536     clearSrc();
537   }
538 };
539
540 /**
541  * @return {boolean} True if an image is loading.
542  */
543 ImageUtil.ImageLoader.prototype.isBusy = function() {
544   return !!this.callback_;
545 };
546
547 /**
548  * @param {Entry} entry Image entry.
549  * @return {boolean} True if loader loads this image.
550  */
551 ImageUtil.ImageLoader.prototype.isLoading = function(entry) {
552   return this.isBusy() && util.isSameEntry(this.entry_, entry);
553 };
554
555 /**
556  * @param {function(HTMLCanvasElement, string=)} callback To be called when the
557  *     image loaded.
558  */
559 ImageUtil.ImageLoader.prototype.setCallback = function(callback) {
560   this.callback_ = callback;
561 };
562
563 /**
564  * Stops loading image.
565  */
566 ImageUtil.ImageLoader.prototype.cancel = function() {
567   if (!this.callback_) return;
568   this.callback_ = null;
569   if (this.timeout_) {
570     clearTimeout(this.timeout_);
571     this.timeout_ = null;
572   }
573   if (this.image_) {
574     this.image_.onload = function() {};
575     this.image_.onerror = function() {};
576     this.image_.src = '';
577   }
578   this.generation_++;  // Silence the transform fetcher if it is in progress.
579 };
580
581 /**
582  * @param {HTMLImageElement} image Image to be transformed.
583  * @param {Object} transform transformation description to apply to the image.
584  * @private
585  */
586 ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) {
587   var canvas = this.document_.createElement('canvas');
588
589   if (transform.rotate90 & 1) {  // Rotated +/-90deg, swap the dimensions.
590     canvas.width = image.height;
591     canvas.height = image.width;
592   } else {
593     canvas.width = image.width;
594     canvas.height = image.height;
595   }
596
597   var context = canvas.getContext('2d');
598   context.save();
599   context.translate(canvas.width / 2, canvas.height / 2);
600   context.rotate(transform.rotate90 * Math.PI / 2);
601   context.scale(transform.scaleX, transform.scaleY);
602
603   var stripCount = Math.ceil(image.width * image.height / (1 << 21));
604   var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0;
605
606   this.copyStrip_(context, image, 0, step);
607 };
608
609 /**
610  * @param {CanvasRenderingContext2D} context Context to draw.
611  * @param {HTMLImageElement} image Image to draw.
612  * @param {number} firstRow Number of the first pixel row to draw.
613  * @param {number} rowCount Count of pixel rows to draw.
614  * @private
615  */
616 ImageUtil.ImageLoader.prototype.copyStrip_ = function(
617     context, image, firstRow, rowCount) {
618   var lastRow = Math.min(firstRow + rowCount, image.height);
619
620   context.drawImage(
621       image, 0, firstRow, image.width, lastRow - firstRow,
622       -image.width / 2, firstRow - image.height / 2,
623       image.width, lastRow - firstRow);
624
625   if (lastRow === image.height) {
626     context.restore();
627     if (this.entry_.toURL().substr(0, 5) !== 'data:') {  // Ignore data urls.
628       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime'));
629     }
630     try {
631       setTimeout(this.callback_, 0, context.canvas);
632     } catch (e) {
633       console.error(e);
634     }
635     this.callback_ = null;
636   } else {
637     var self = this;
638     this.timeout_ = setTimeout(
639         function() {
640           self.timeout_ = null;
641           self.copyStrip_(context, image, lastRow, rowCount);
642         }, 0);
643   }
644 };
645
646 /**
647  * @param {HTMLElement} element To remove children from.
648  */
649 ImageUtil.removeChildren = function(element) {
650   element.textContent = '';
651 };
652
653 /**
654  * @param {string} name File name (with extension).
655  * @return {string} File name without extension.
656  */
657 ImageUtil.getDisplayNameFromName = function(name) {
658   var index = name.lastIndexOf('.');
659   if (index !== -1)
660     return name.substr(0, index);
661   else
662     return name;
663 };
664
665 /**
666  * @param {string} name File name.
667  * @return {string} File extension.
668  */
669 ImageUtil.getExtensionFromFullName = function(name) {
670   var index = name.lastIndexOf('.');
671   if (index !== -1)
672     return name.substring(index);
673   else
674     return '';
675 };
676
677 /**
678  * Metrics (from metrics.js) itnitialized by the File Manager from owner frame.
679  * @type {Object?}
680  */
681 ImageUtil.metrics = null;
682
683 /**
684  * @param {string} name Local name.
685  * @return {string} Full name.
686  */
687 ImageUtil.getMetricName = function(name) {
688   return 'PhotoEditor.' + name;
689 };
690
691 /**
692  * Used for metrics reporting, keep in sync with the histogram description.
693  */
694 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp'];