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