Disable Enhanced Bookmark for ICS devices
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_util.js
blob76481b5021430c14e5f400fdea52641ae09ab499
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.
5 // Namespace object for the utilities.
6 var ImageUtil = {};
8 /**
9 * Performance trace.
11 ImageUtil.trace = (function() {
12 /**
13 * Performance trace.
14 * @constructor
15 * @struct
17 function PerformanceTrace() {
18 this.lines_ = {};
19 this.timers_ = {};
20 this.container_ = null;
23 PerformanceTrace.prototype.bindToDOM = function(container) {
24 this.container_ = container;
27 PerformanceTrace.prototype.report = function(key, value) {
28 if (!(key in this.lines_)) {
29 if (this.container_) {
30 var div = this.lines_[key] = document.createElement('div');
31 this.container_.appendChild(div);
32 } else {
33 this.lines_[key] = {};
36 this.lines_[key].textContent = key + ': ' + value;
37 if (ImageUtil.trace.log) this.dumpLine(key);
40 PerformanceTrace.prototype.resetTimer = function(key) {
41 this.timers_[key] = Date.now();
44 PerformanceTrace.prototype.reportTimer = function(key) {
45 this.report(key, (Date.now() - this.timers_[key]) + 'ms');
48 PerformanceTrace.prototype.dump = function() {
49 for (var key in this.lines_)
50 this.dumpLine(key);
53 PerformanceTrace.prototype.dumpLine = function(key) {
54 console.log('trace.' + this.lines_[key].textContent);
57 return new PerformanceTrace();
58 })();
60 /**
61 * @param {number} min Minimum value.
62 * @param {number} value Value to adjust.
63 * @param {number} max Maximum value.
64 * @return {number} The closest to the |value| number in span [min, max].
66 ImageUtil.clamp = function(min, value, max) {
67 return Math.max(min, Math.min(max, value));
70 /**
71 * @param {number} min Minimum value.
72 * @param {number} value Value to check.
73 * @param {number} max Maximum value.
74 * @return {boolean} True if value is between.
76 ImageUtil.between = function(min, value, max) {
77 return (value - min) * (value - max) <= 0;
80 /**
81 * Rectangle class.
83 * @param {number} left Left.
84 * @param {number} top Top.
85 * @param {number} width Width.
86 * @param {number} height Height.
87 * @constructor
88 * @struct
90 function ImageRect(left, top, width, height) {
91 this.left = left;
92 this.top = top;
93 this.width = width;
94 this.height = height;
97 /**
98 * Creates an image rect with an image or a canvas.
99 * @param {!(HTMLImageElement|HTMLCanvasElement)} image An image or a canvas.
100 * @return {!ImageRect}
102 ImageRect.createFromImage = function(image) {
103 return new ImageRect(0, 0, image.width, image.height);
107 * Clone an image rect.
108 * @param {!ImageRect} imageRect An image rect.
109 * @return {!ImageRect}
111 ImageRect.clone = function(imageRect) {
112 return new ImageRect(imageRect.left, imageRect.top, imageRect.width,
113 imageRect.height);
117 * Creates an image rect with a bound.
118 * @param {{left: number, top: number, right: number, bottom: number}} bound
119 * A bound.
120 * @return {!ImageRect}
122 ImageRect.createFromBounds = function(bound) {
123 return new ImageRect(bound.left, bound.top,
124 bound.right - bound.left, bound.bottom - bound.top);
128 * Creates an image rect with width and height.
129 * @param {number} width Width.
130 * @param {number} height Height.
131 * @return {!ImageRect}
133 ImageRect.createFromWidthAndHeight = function(width, height) {
134 return new ImageRect(0, 0, width, height);
137 ImageRect.prototype = /** @struct */ ({
138 // TODO(yawano): Change getters to methods (e.g. getRight()).
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}
146 get right() {
147 return this.left + this.width;
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}
156 get bottom() {
157 return this.top + this.height;
162 * @param {number} factor Factor to scale.
163 * @return {!ImageRect} A rectangle with every dimension scaled.
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);
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.
178 ImageRect.prototype.shift = function(dx, dy) {
179 return new ImageRect(this.left + dx, this.top + dy, this.width, this.height);
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.
187 ImageRect.prototype.moveTo = function(x, y) {
188 return new ImageRect(x, y, this.width, this.height);
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.
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);
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.
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;
212 * @param {!ImageRect} rect Rectangle to check.
213 * @return {boolean} True if this rectangle intersects with the |rect|.
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;
223 * @param {!ImageRect} rect Rectangle to check.
224 * @return {boolean} True if this rectangle containing the |rect|.
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);
234 * @return {boolean} True if rectangle is empty.
236 ImageRect.prototype.isEmpty = function() {
237 return this.width === 0 || this.height === 0;
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.
246 ImageRect.prototype.clamp = function(bounds) {
247 var rect = ImageRect.clone(this);
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;
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;
269 return rect;
273 * @return {string} String representation.
275 ImageRect.prototype.toString = function() {
276 return '(' + this.left + ',' + this.top + '):' +
277 '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')';
280 * Useful shortcuts for drawing (static functions).
284 * Draw the image in context with appropriate scaling.
285 * @param {!CanvasRenderingContext2D} context Context to draw.
286 * @param {!(HTMLCanvasElement|HTMLImageElement)} 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).
292 ImageRect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
293 opt_dstRect = opt_dstRect ||
294 ImageRect.createFromImage(assert(context.canvas));
295 opt_srcRect = opt_srcRect || ImageRect.createFromImage(image);
296 if (opt_dstRect.isEmpty() || opt_srcRect.isEmpty())
297 return;
298 context.drawImage(image,
299 opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height,
300 opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height);
304 * Draw a box around the rectangle.
305 * @param {!CanvasRenderingContext2D} context Context to draw.
306 * @param {!ImageRect} rect Rectangle.
308 ImageRect.outline = function(context, rect) {
309 context.strokeRect(
310 rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1);
314 * Fill the rectangle.
315 * @param {!CanvasRenderingContext2D} context Context to draw.
316 * @param {!ImageRect} rect Rectangle.
318 ImageRect.fill = function(context, rect) {
319 context.fillRect(rect.left, rect.top, rect.width, rect.height);
323 * Fills the space between the two rectangles.
324 * @param {!CanvasRenderingContext2D} context Context to draw.
325 * @param {!ImageRect} inner Inner rectangle.
326 * @param {!ImageRect} outer Outer rectangle.
328 ImageRect.fillBetween = function(context, inner, outer) {
329 var innerRight = inner.left + inner.width;
330 var innerBottom = inner.top + inner.height;
331 var outerRight = outer.left + outer.width;
332 var outerBottom = outer.top + outer.height;
333 if (inner.top > outer.top) {
334 context.fillRect(
335 outer.left, outer.top, outer.width, inner.top - outer.top);
337 if (inner.left > outer.left) {
338 context.fillRect(
339 outer.left, inner.top, inner.left - outer.left, inner.height);
341 if (inner.width < outerRight) {
342 context.fillRect(
343 innerRight, inner.top, outerRight - innerRight, inner.height);
345 if (inner.height < outerBottom) {
346 context.fillRect(
347 outer.left, innerBottom, outer.width, outerBottom - innerBottom);
352 * Circle class.
353 * @param {number} x X coordinate of circle center.
354 * @param {number} y Y coordinate of circle center.
355 * @param {number} r Radius.
356 * @constructor
358 function Circle(x, y, r) {
359 this.x = x;
360 this.y = y;
361 this.squaredR = r * r;
365 * Check if the point is inside the circle.
366 * @param {number} x X coordinate of the point.
367 * @param {number} y Y coordinate of the point.
368 * @return {boolean} True if the point is inside.
370 Circle.prototype.inside = function(x, y) {
371 x -= this.x;
372 y -= this.y;
373 return x * x + y * y <= this.squaredR;
377 * Copy an image applying scaling and rotation.
379 * @param {!HTMLCanvasElement} dst Destination.
380 * @param {!(HTMLCanvasElement|HTMLImageElement)} src Source.
381 * @param {number} scaleX Y scale transformation.
382 * @param {number} scaleY X scale transformation.
383 * @param {number} angle (in radians).
385 ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) {
386 var context = dst.getContext('2d');
387 context.save();
388 context.translate(context.canvas.width / 2, context.canvas.height / 2);
389 context.rotate(angle);
390 context.scale(scaleX, scaleY);
391 context.drawImage(src, -src.width / 2, -src.height / 2);
392 context.restore();
396 * Adds or removes an attribute to/from an HTML element.
397 * @param {!HTMLElement} element To be applied to.
398 * @param {string} attribute Name of attribute.
399 * @param {boolean} on True if add, false if remove.
401 ImageUtil.setAttribute = function(element, attribute, on) {
402 if (on)
403 element.setAttribute(attribute, '');
404 else
405 element.removeAttribute(attribute);
409 * Adds or removes CSS class to/from an HTML element.
410 * @param {!HTMLElement} element To be applied to.
411 * @param {string} className Name of CSS class.
412 * @param {boolean} on True if add, false if remove.
414 ImageUtil.setClass = function(element, className, on) {
415 var cl = element.classList;
416 if (on)
417 cl.add(className);
418 else
419 cl.remove(className);
423 * ImageLoader loads an image from a given Entry into a canvas in two steps:
424 * 1. Loads the image into an HTMLImageElement.
425 * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done
426 * stripe-by-stripe to avoid freezing up the UI. The transform is taken into
427 * account.
429 * @param {!HTMLDocument} document Owner document.
430 * @param {!MetadataModel} metadataModel
431 * @constructor
432 * @struct
434 ImageUtil.ImageLoader = function(document, metadataModel) {
435 this.document_ = document;
438 * @private {!MetadataModel}
439 * @const
441 this.metadataModel_ = metadataModel;
443 this.image_ = new Image();
444 this.generation_ = 0;
447 * @type {number}
448 * @private
450 this.timeout_ = 0;
453 * @type {?function(!HTMLCanvasElement, string=)}
454 * @private
456 this.callback_ = null;
459 * @type {FileEntry}
460 * @private
462 this.entry_ = null;
466 * Loads an image.
467 * TODO(mtomasz): Simplify, or even get rid of this class and merge with the
468 * ThumbnaiLoader class.
470 * @param {!Gallery.Item} item Item representing the image to be loaded.
471 * @param {function(!HTMLCanvasElement, string=)} callback Callback to be
472 * called when loaded. The second optional argument is an error identifier.
473 * @param {number=} opt_delay Load delay in milliseconds, useful to let the
474 * animations play out before the computation heavy image loading starts.
476 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) {
477 var entry = item.getEntry();
479 this.cancel();
480 this.entry_ = entry;
481 this.callback_ = callback;
483 // The transform fetcher is not cancellable so we need a generation counter.
484 var generation = ++this.generation_;
487 * @param {!HTMLImageElement} image Image to be transformed.
488 * @param {Object=} opt_transform Transformation.
490 var onTransform = function(image, opt_transform) {
491 if (generation === this.generation_) {
492 this.convertImage_(
493 image, opt_transform || { scaleX: 1, scaleY: 1, rotate90: 0});
496 onTransform = onTransform.bind(this);
499 * @param {string=} opt_error Error.
501 var onError = function(opt_error) {
502 this.image_.onerror = null;
503 this.image_.onload = null;
504 var tmpCallback = this.callback_;
505 this.callback_ = null;
506 var emptyCanvas = assertInstanceof(this.document_.createElement('canvas'),
507 HTMLCanvasElement);
508 emptyCanvas.width = 0;
509 emptyCanvas.height = 0;
510 tmpCallback(emptyCanvas, opt_error);
512 onError = onError.bind(this);
514 var loadImage = function() {
515 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime'));
516 this.timeout_ = null;
518 this.image_.onload = function() {
519 this.image_.onerror = null;
520 this.image_.onload = null;
521 this.metadataModel_.get([entry], ['contentImageTransform']).then(
522 function(metadataItems) {
523 onTransform(this.image_, metadataItems[0].contentImageTransform);
524 }.bind(this));
525 }.bind(this);
527 // The error callback has an optional error argument, which in case of a
528 // general error should not be specified
529 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
531 // Load the image directly. The query parameter is workaround for
532 // crbug.com/379678, which force to update the contents of the image.
533 this.image_.src = entry.toURL() + '?nocache=' + Date.now();
534 }.bind(this);
536 // Loads the image. If already loaded, then forces a reload.
537 var startLoad = this.resetImage_.bind(this, function() {
538 loadImage();
539 }.bind(this), onError);
541 if (opt_delay) {
542 this.timeout_ = setTimeout(startLoad, opt_delay);
543 } else {
544 startLoad();
549 * Resets the image by forcing the garbage collection and clearing the src
550 * attribute.
552 * @param {function()} onSuccess Success callback.
553 * @param {function(string=)} onError Failure callback with an optional error
554 * identifier.
555 * @private
557 ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) {
558 var clearSrc = function() {
559 this.image_.onload = onSuccess;
560 this.image_.onerror = onSuccess;
561 this.image_.src = '';
562 }.bind(this);
564 var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' +
565 'AAABAAEAAAICTAEAOw==';
567 if (this.image_.src !== emptyImage) {
568 // Load an empty image, then clear src.
569 this.image_.onload = clearSrc;
570 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
571 this.image_.src = emptyImage;
572 } else {
573 // Empty image already loaded, so clear src immediately.
574 clearSrc();
579 * @return {boolean} True if an image is loading.
581 ImageUtil.ImageLoader.prototype.isBusy = function() {
582 return !!this.callback_;
586 * @param {Entry} entry Image entry.
587 * @return {boolean} True if loader loads this image.
589 ImageUtil.ImageLoader.prototype.isLoading = function(entry) {
590 return this.isBusy() && util.isSameEntry(this.entry_, entry);
594 * @param {function(!HTMLCanvasElement, string=)} callback To be called when the
595 * image loaded.
597 ImageUtil.ImageLoader.prototype.setCallback = function(callback) {
598 this.callback_ = callback;
602 * Stops loading image.
604 ImageUtil.ImageLoader.prototype.cancel = function() {
605 if (!this.callback_) return;
606 this.callback_ = null;
607 if (this.timeout_) {
608 clearTimeout(this.timeout_);
609 this.timeout_ = 0;
611 if (this.image_) {
612 this.image_.onload = function() {};
613 this.image_.onerror = function() {};
614 this.image_.src = '';
616 this.generation_++; // Silence the transform fetcher if it is in progress.
620 * @param {!HTMLImageElement} image Image to be transformed.
621 * @param {!Object} transform transformation description to apply to the image.
622 * @private
624 ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) {
625 var canvas = this.document_.createElement('canvas');
627 if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions.
628 canvas.width = image.height;
629 canvas.height = image.width;
630 } else {
631 canvas.width = image.width;
632 canvas.height = image.height;
635 var context = canvas.getContext('2d');
636 context.save();
637 context.translate(canvas.width / 2, canvas.height / 2);
638 context.rotate(transform.rotate90 * Math.PI / 2);
639 context.scale(transform.scaleX, transform.scaleY);
641 var stripCount = Math.ceil(image.width * image.height / (1 << 21));
642 var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0;
644 this.copyStrip_(context, image, 0, step);
648 * @param {!CanvasRenderingContext2D} context Context to draw.
649 * @param {!HTMLImageElement} image Image to draw.
650 * @param {number} firstRow Number of the first pixel row to draw.
651 * @param {number} rowCount Count of pixel rows to draw.
652 * @private
654 ImageUtil.ImageLoader.prototype.copyStrip_ = function(
655 context, image, firstRow, rowCount) {
656 var lastRow = Math.min(firstRow + rowCount, image.height);
658 context.drawImage(
659 image, 0, firstRow, image.width, lastRow - firstRow,
660 -image.width / 2, firstRow - image.height / 2,
661 image.width, lastRow - firstRow);
663 if (lastRow === image.height) {
664 context.restore();
665 if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls.
666 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime'));
668 try {
669 setTimeout(this.callback_, 0, context.canvas);
670 } catch (e) {
671 console.error(e);
673 this.callback_ = null;
674 } else {
675 var self = this;
676 this.timeout_ = setTimeout(
677 function() {
678 self.timeout_ = 0;
679 self.copyStrip_(context, image, lastRow, rowCount);
680 }, 0);
685 * @param {!HTMLElement} element To remove children from.
687 ImageUtil.removeChildren = function(element) {
688 element.textContent = '';
692 * @param {string} name File name (with extension).
693 * @return {string} File name without extension.
695 ImageUtil.getDisplayNameFromName = function(name) {
696 var index = name.lastIndexOf('.');
697 if (index !== -1)
698 return name.substr(0, index);
699 else
700 return name;
704 * @param {string} name File name.
705 * @return {string} File extension.
707 ImageUtil.getExtensionFromFullName = function(name) {
708 var index = name.lastIndexOf('.');
709 if (index !== -1)
710 return name.substring(index);
711 else
712 return '';
716 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame.
717 * @type {Object}
719 ImageUtil.metrics = null;
722 * @param {string} name Local name.
723 * @return {string} Full name.
725 ImageUtil.getMetricName = function(name) {
726 return 'PhotoEditor.' + name;
730 * Used for metrics reporting, keep in sync with the histogram description.
731 * @type {Array.<string>}
732 * @const
734 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp'];