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.
8 // Namespace object for the utilities.
9 function ImageUtil() {}
14 ImageUtil.trace = (function() {
15 function PerformanceTrace() {
18 this.container_ = null;
21 PerformanceTrace.prototype.bindToDOM = function(container) {
22 this.container_ = container;
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);
31 this.lines_[key] = {};
34 this.lines_[key].textContent = key + ': ' + value;
35 if (ImageUtil.trace.log) this.dumpLine(key);
38 PerformanceTrace.prototype.resetTimer = function(key) {
39 this.timers_[key] = Date.now();
42 PerformanceTrace.prototype.reportTimer = function(key) {
43 this.report(key, (Date.now() - this.timers_[key]) + 'ms');
46 PerformanceTrace.prototype.dump = function() {
47 for (var key in this.lines_)
51 PerformanceTrace.prototype.dumpLine = function(key) {
52 console.log('trace.' + this.lines_[key].textContent);
55 return new PerformanceTrace();
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].
64 ImageUtil.clamp = function(min, value, max) {
65 return Math.max(min, Math.min(max, value));
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.
74 ImageUtil.between = function(min, value, max) {
75 return (value - min) * (value - max) <= 0;
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.
94 switch (arguments.length) {
96 this.left = arguments[0];
97 this.top = arguments[1];
98 this.width = arguments[2];
99 this.height = arguments[3];
105 this.width = arguments[0];
106 this.height = arguments[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;
123 if ('width' in source && 'height' in source) {
124 this.width = source.width;
125 this.height = source.height;
128 break; // Fall through to the error message.
138 console.error('Invalid Rect constructor arguments:',
139 Array.apply(null, arguments));
144 * Obtains the x coordinate of right edge. The most right pixels in the
145 * rectangle are (x = right - 1) and the pixels (x = right) are not included
150 return this.left + this.width;
154 * Obtains the y coordinate of bottom edge. The most bottom pixels in the
155 * rectangle are (y = bottom - 1) and the pixels (y = bottom) are not included
160 return this.top + this.height;
165 * @param {number} factor Factor to scale.
166 * @return {Rect} A rectangle with every dimension scaled.
168 Rect.prototype.scale = function(factor) {
173 this.height * factor);
177 * @param {number} dx Difference in X.
178 * @param {number} dy Difference in Y.
179 * @return {Rect} A rectangle shifted by (dx,dy), same size.
181 Rect.prototype.shift = function(dx, dy) {
182 return new Rect(this.left + dx, this.top + dy, this.width, this.height);
186 * @param {number} x Coordinate of the left top corner.
187 * @param {number} y Coordinate of the left top corner.
188 * @return {Rect} A rectangle with left==x and top==y, same size.
190 Rect.prototype.moveTo = function(x, y) {
191 return new Rect(x, y, this.width, this.height);
195 * @param {number} dx Difference in X.
196 * @param {number} dy Difference in Y.
197 * @return {Rect} A rectangle inflated by (dx, dy), same center.
199 Rect.prototype.inflate = function(dx, dy) {
201 this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy);
205 * @param {number} x Coordinate of the point.
206 * @param {number} y Coordinate of the point.
207 * @return {boolean} True if the point lies inside the rectangle.
209 Rect.prototype.inside = function(x, y) {
210 return this.left <= x && x < this.left + this.width &&
211 this.top <= y && y < this.top + this.height;
215 * @param {Rect} rect Rectangle to check.
216 * @return {boolean} True if this rectangle intersects with the |rect|.
218 Rect.prototype.intersects = function(rect) {
219 return (this.left + this.width) > rect.left &&
220 (rect.left + rect.width) > this.left &&
221 (this.top + this.height) > rect.top &&
222 (rect.top + rect.height) > this.top;
226 * @param {Rect} rect Rectangle to check.
227 * @return {boolean} True if this rectangle containing the |rect|.
229 Rect.prototype.contains = function(rect) {
230 return (this.left <= rect.left) &&
231 (rect.left + rect.width) <= (this.left + this.width) &&
232 (this.top <= rect.top) &&
233 (rect.top + rect.height) <= (this.top + this.height);
237 * @return {boolean} True if rectangle is empty.
239 Rect.prototype.isEmpty = function() {
240 return this.width === 0 || this.height === 0;
244 * Clamp the rectangle to the bounds by moving it.
245 * Decrease the size only if necessary.
246 * @param {Rect} bounds Bounds.
247 * @return {Rect} Calculated rectangle.
249 Rect.prototype.clamp = function(bounds) {
250 var rect = new Rect(this);
252 if (rect.width > bounds.width) {
253 rect.left = bounds.left;
254 rect.width = bounds.width;
255 } else if (rect.left < bounds.left) {
256 rect.left = bounds.left;
257 } else if (rect.left + rect.width >
258 bounds.left + bounds.width) {
259 rect.left = bounds.left + bounds.width - rect.width;
262 if (rect.height > bounds.height) {
263 rect.top = bounds.top;
264 rect.height = bounds.height;
265 } else if (rect.top < bounds.top) {
266 rect.top = bounds.top;
267 } else if (rect.top + rect.height >
268 bounds.top + bounds.height) {
269 rect.top = bounds.top + bounds.height - rect.height;
276 * @return {string} String representation.
278 Rect.prototype.toString = function() {
279 return '(' + this.left + ',' + this.top + '):' +
280 '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')';
283 * Useful shortcuts for drawing (static functions).
287 * Draw the image in context with appropriate scaling.
288 * @param {CanvasRenderingContext2D} context Context to draw.
289 * @param {Image} image Image to draw.
290 * @param {Rect=} opt_dstRect Rectangle in the canvas (whole canvas by default).
291 * @param {Rect=} opt_srcRect Rectangle in the image (whole image by default).
293 Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
294 opt_dstRect = opt_dstRect || new Rect(context.canvas);
295 opt_srcRect = opt_srcRect || new Rect(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 {Rect} rect Rectangle.
308 Rect.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 {Rect} rect Rectangle.
318 Rect.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 {Rect} inner Inner rectangle.
326 * @param {Rect} outer Outer rectangle.
328 Rect.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.
432 ImageUtil.ImageLoader = function(document) {
433 this.document_ = document;
434 this.image_ = new Image();
435 this.generation_ = 0;
440 * TODO(mtomasz): Simplify, or even get rid of this class and merge with the
441 * ThumbnaiLoader class.
443 * @param {FileEntry} entry Image entry to be loaded.
444 * @param {function(HTMLCanvasElement, string=)} callback Callback to be
445 * called when loaded. The second optional argument is an error identifier.
446 * @param {number=} opt_delay Load delay in milliseconds, useful to let the
447 * animations play out before the computation heavy image loading starts.
449 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) {
450 var entry = item.getEntry();
454 this.callback_ = callback;
456 // The transform fetcher is not cancellable so we need a generation counter.
457 var generation = ++this.generation_;
458 var onTransform = function(image, transform) {
459 if (generation === this.generation_) {
461 image, transform || { scaleX: 1, scaleY: 1, rotate90: 0});
465 var onError = function(opt_error) {
466 this.image_.onerror = null;
467 this.image_.onload = null;
468 var tmpCallback = this.callback_;
469 this.callback_ = null;
470 var emptyCanvas = this.document_.createElement('canvas');
471 emptyCanvas.width = 0;
472 emptyCanvas.height = 0;
473 tmpCallback(emptyCanvas, opt_error);
476 var loadImage = function() {
477 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime'));
478 this.timeout_ = null;
480 this.image_.onload = function() {
481 this.image_.onerror = null;
482 this.image_.onload = null;
483 item.getFetchedMedia().then(function(fetchedMediaMetadata) {
484 onTransform(this.image_, fetchedMediaMetadata.imageTransform);
485 }.bind(this)).catch(function(error) {
486 console.error(error.stack || error);
490 // The error callback has an optional error argument, which in case of a
491 // general error should not be specified
492 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
494 // Load the image directly. The query parameter is workaround for
495 // crbug.com/379678, which force to update the contents of the image.
496 this.image_.src = entry.toURL() + "?nocache=" + Date.now();
499 // Loads the image. If already loaded, then forces a reload.
500 var startLoad = this.resetImage_.bind(this, function() {
502 }.bind(this), onError);
505 this.timeout_ = setTimeout(startLoad, opt_delay);
512 * Resets the image by forcing the garbage collection and clearing the src
515 * @param {function()} onSuccess Success callback.
516 * @param {function(opt_string)} onError Failure callback with an optional
520 ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) {
521 var clearSrc = function() {
522 this.image_.onload = onSuccess;
523 this.image_.onerror = onSuccess;
524 this.image_.src = '';
527 var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' +
528 'AAABAAEAAAICTAEAOw==';
530 if (this.image_.src !== emptyImage) {
531 // Load an empty image, then clear src.
532 this.image_.onload = clearSrc;
533 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
534 this.image_.src = emptyImage;
536 // Empty image already loaded, so clear src immediately.
542 * @return {boolean} True if an image is loading.
544 ImageUtil.ImageLoader.prototype.isBusy = function() {
545 return !!this.callback_;
549 * @param {Entry} entry Image entry.
550 * @return {boolean} True if loader loads this image.
552 ImageUtil.ImageLoader.prototype.isLoading = function(entry) {
553 return this.isBusy() && util.isSameEntry(this.entry_, entry);
557 * @param {function} callback To be called when the image loaded.
559 ImageUtil.ImageLoader.prototype.setCallback = function(callback) {
560 this.callback_ = callback;
564 * Stops loading image.
566 ImageUtil.ImageLoader.prototype.cancel = function() {
567 if (!this.callback_) return;
568 this.callback_ = null;
570 clearTimeout(this.timeout_);
571 this.timeout_ = null;
574 this.image_.onload = function() {};
575 this.image_.onerror = function() {};
576 this.image_.src = '';
578 this.generation_++; // Silence the transform fetcher if it is in progress.
582 * @param {HTMLImageElement} image Image to be transformed.
583 * @param {Object} transform transformation description to apply to the image.
586 ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) {
587 var canvas = this.document_.createElement('canvas');
589 if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions.
590 canvas.width = image.height;
591 canvas.height = image.width;
593 canvas.width = image.width;
594 canvas.height = image.height;
597 var context = canvas.getContext('2d');
599 context.translate(canvas.width / 2, canvas.height / 2);
600 context.rotate(transform.rotate90 * Math.PI / 2);
601 context.scale(transform.scaleX, transform.scaleY);
603 var stripCount = Math.ceil(image.width * image.height / (1 << 21));
604 var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0;
606 this.copyStrip_(context, image, 0, step);
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.
616 ImageUtil.ImageLoader.prototype.copyStrip_ = function(
617 context, image, firstRow, rowCount) {
618 var lastRow = Math.min(firstRow + rowCount, image.height);
621 image, 0, firstRow, image.width, lastRow - firstRow,
622 -image.width / 2, firstRow - image.height / 2,
623 image.width, lastRow - firstRow);
625 if (lastRow === image.height) {
627 if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls.
628 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime'));
631 setTimeout(this.callback_, 0, context.canvas);
635 this.callback_ = null;
638 this.timeout_ = setTimeout(
640 self.timeout_ = null;
641 self.copyStrip_(context, image, lastRow, rowCount);
647 * @param {HTMLElement} element To remove children from.
649 ImageUtil.removeChildren = function(element) {
650 element.textContent = '';
654 * @param {string} name File name (with extension).
655 * @return {string} File name without extension.
657 ImageUtil.getDisplayNameFromName = function(name) {
658 var index = name.lastIndexOf('.');
660 return name.substr(0, index);
666 * @param {string} name File name.
667 * @return {string} File extension.
669 ImageUtil.getExtensionFromFullName = function(name) {
670 var index = name.lastIndexOf('.');
672 return name.substring(index);
678 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame.
681 ImageUtil.metrics = null;
684 * @param {string} name Local name.
685 * @return {string} Full name.
687 ImageUtil.getMetricName = function(name) {
688 return 'PhotoEditor.' + name;
692 * Used for metrics reporting, keep in sync with the histogram description.
694 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp'];