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.
11 ImageUtil
.trace
= (function() {
17 function PerformanceTrace() {
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
);
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_
)
53 PerformanceTrace
.prototype.dumpLine = function(key
) {
54 console
.log('trace.' + this.lines_
[key
].textContent
);
57 return new PerformanceTrace();
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
));
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;
83 * @param {number} left Left.
84 * @param {number} top Top.
85 * @param {number} width Width.
86 * @param {number} height Height.
90 function ImageRect(left
, top
, width
, height
) {
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
,
117 * Creates an image rect with a bound.
118 * @param {{left: number, top: number, right: number, bottom: number}} 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
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
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(
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
;
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
289 * @param {ImageRect=} opt_srcRect Rectangle in the image (whole image by
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())
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
) {
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
) {
335 outer
.left
, outer
.top
, outer
.width
, inner
.top
- outer
.top
);
337 if (inner
.left
> outer
.left
) {
339 outer
.left
, inner
.top
, inner
.left
- outer
.left
, inner
.height
);
341 if (inner
.width
< outerRight
) {
343 innerRight
, inner
.top
, outerRight
- innerRight
, inner
.height
);
345 if (inner
.height
< outerBottom
) {
347 outer
.left
, innerBottom
, outer
.width
, outerBottom
- innerBottom
);
353 * @param {number} x X coordinate of circle center.
354 * @param {number} y Y coordinate of circle center.
355 * @param {number} r Radius.
358 function Circle(x
, y
, r
) {
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
) {
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');
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);
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
) {
403 element
.setAttribute(attribute
, '');
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
;
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
429 * @param {!HTMLDocument} document Owner document.
430 * @param {!MetadataModel} metadataModel
434 ImageUtil
.ImageLoader = function(document
, metadataModel
) {
435 this.document_
= document
;
438 * @private {!MetadataModel}
441 this.metadataModel_
= metadataModel
;
443 this.image_
= new Image();
444 this.generation_
= 0;
453 * @type {?function(!HTMLCanvasElement, string=)}
456 this.callback_
= null;
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();
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_
) {
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'),
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
);
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();
536 // Loads the image. If already loaded, then forces a reload.
537 var startLoad
= this.resetImage_
.bind(this, function() {
539 }.bind(this), onError
);
542 this.timeout_
= setTimeout(startLoad
, opt_delay
);
549 * Resets the image by forcing the garbage collection and clearing the src
552 * @param {function()} onSuccess Success callback.
553 * @param {function(string=)} onError Failure callback with an optional error
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
= '';
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
;
573 // Empty image already loaded, so clear src immediately.
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
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;
608 clearTimeout(this.timeout_
);
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.
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
;
631 canvas
.width
= image
.width
;
632 canvas
.height
= image
.height
;
635 var context
= canvas
.getContext('2d');
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.
654 ImageUtil
.ImageLoader
.prototype.copyStrip_ = function(
655 context
, image
, firstRow
, rowCount
) {
656 var lastRow
= Math
.min(firstRow
+ rowCount
, image
.height
);
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
) {
665 if (this.entry_
.toURL().substr(0, 5) !== 'data:') { // Ignore data urls.
666 ImageUtil
.metrics
.recordInterval(ImageUtil
.getMetricName('LoadTime'));
669 setTimeout(this.callback_
, 0, context
.canvas
);
673 this.callback_
= null;
676 this.timeout_
= setTimeout(
679 self
.copyStrip_(context
, image
, lastRow
, rowCount
);
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('.');
698 return name
.substr(0, index
);
704 * @param {string} name File name.
705 * @return {string} File extension.
707 ImageUtil
.getExtensionFromFullName = function(name
) {
708 var index
= name
.lastIndexOf('.');
710 return name
.substring(index
);
716 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame.
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>}
734 ImageUtil
.FILE_TYPES
= ['jpg', 'png', 'gif', 'bmp', 'webp'];