Home | History | Annotate | Download | only in image_editor
      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 '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 Rect.prototype = {
    143   /**
    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
    146    * in the rectangle.
    147    * @return {number}
    148    */
    149   get right() {
    150     return this.left + this.width;
    151   },
    152 
    153   /**
    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
    156    * in the rectangle.
    157    * @return {number}
    158    */
    159   get bottom() {
    160     return this.top + this.height;
    161   }
    162 };
    163 
    164 /**
    165  * @param {number} factor Factor to scale.
    166  * @return {Rect} A rectangle with every dimension scaled.
    167  */
    168 Rect.prototype.scale = function(factor) {
    169   return new Rect(
    170       this.left * factor,
    171       this.top * factor,
    172       this.width * factor,
    173       this.height * factor);
    174 };
    175 
    176 /**
    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.
    180  */
    181 Rect.prototype.shift = function(dx, dy) {
    182   return new Rect(this.left + dx, this.top + dy, this.width, this.height);
    183 };
    184 
    185 /**
    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.
    189  */
    190 Rect.prototype.moveTo = function(x, y) {
    191   return new Rect(x, y, this.width, this.height);
    192 };
    193 
    194 /**
    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.
    198  */
    199 Rect.prototype.inflate = function(dx, dy) {
    200   return new Rect(
    201       this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy);
    202 };
    203 
    204 /**
    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.
    208  */
    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;
    212 };
    213 
    214 /**
    215  * @param {Rect} rect Rectangle to check.
    216  * @return {boolean} True if this rectangle intersects with the |rect|.
    217  */
    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;
    223 };
    224 
    225 /**
    226  * @param {Rect} rect Rectangle to check.
    227  * @return {boolean} True if this rectangle containing the |rect|.
    228  */
    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);
    234 };
    235 
    236 /**
    237  * @return {boolean} True if rectangle is empty.
    238  */
    239 Rect.prototype.isEmpty = function() {
    240   return this.width === 0 || this.height === 0;
    241 };
    242 
    243 /**
    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.
    248  */
    249 Rect.prototype.clamp = function(bounds) {
    250   var rect = new Rect(this);
    251 
    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;
    260   }
    261 
    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;
    270   }
    271 
    272   return rect;
    273 };
    274 
    275 /**
    276  * @return {string} String representation.
    277  */
    278 Rect.prototype.toString = function() {
    279   return '(' + this.left + ',' + this.top + '):' +
    280          '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')';
    281 };
    282 /*
    283  * Useful shortcuts for drawing (static functions).
    284  */
    285 
    286 /**
    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).
    292  */
    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())
    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);
    301 };
    302 
    303 /**
    304  * Draw a box around the rectangle.
    305  * @param {CanvasRenderingContext2D} context Context to draw.
    306  * @param {Rect} rect Rectangle.
    307  */
    308 Rect.outline = function(context, rect) {
    309   context.strokeRect(
    310       rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1);
    311 };
    312 
    313 /**
    314  * Fill the rectangle.
    315  * @param {CanvasRenderingContext2D} context Context to draw.
    316  * @param {Rect} rect Rectangle.
    317  */
    318 Rect.fill = function(context, rect) {
    319   context.fillRect(rect.left, rect.top, rect.width, rect.height);
    320 };
    321 
    322 /**
    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.
    327  */
    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) {
    334     context.fillRect(
    335         outer.left, outer.top, outer.width, inner.top - outer.top);
    336   }
    337   if (inner.left > outer.left) {
    338     context.fillRect(
    339         outer.left, inner.top, inner.left - outer.left, inner.height);
    340   }
    341   if (inner.width < outerRight) {
    342     context.fillRect(
    343         innerRight, inner.top, outerRight - innerRight, inner.height);
    344   }
    345   if (inner.height < outerBottom) {
    346     context.fillRect(
    347         outer.left, innerBottom, outer.width, outerBottom - innerBottom);
    348   }
    349 };
    350 
    351 /**
    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
    357  */
    358 function Circle(x, y, r) {
    359   this.x = x;
    360   this.y = y;
    361   this.squaredR = r * r;
    362 }
    363 
    364 /**
    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.
    369  */
    370 Circle.prototype.inside = function(x, y) {
    371   x -= this.x;
    372   y -= this.y;
    373   return x * x + y * y <= this.squaredR;
    374 };
    375 
    376 /**
    377  * Copy an image applying scaling and rotation.
    378  *
    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).
    384  */
    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();
    393 };
    394 
    395 /**
    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.
    400  */
    401 ImageUtil.setAttribute = function(element, attribute, on) {
    402   if (on)
    403     element.setAttribute(attribute, '');
    404   else
    405     element.removeAttribute(attribute);
    406 };
    407 
    408 /**
    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.
    413  */
    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);
    420 };
    421 
    422 /**
    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.
    428  *
    429  * @param {HTMLDocument} document Owner document.
    430  * @constructor
    431  */
    432 ImageUtil.ImageLoader = function(document) {
    433   this.document_ = document;
    434   this.image_ = new Image();
    435   this.generation_ = 0;
    436 };
    437 
    438 /**
    439  * Loads an image.
    440  * TODO(mtomasz): Simplify, or even get rid of this class and merge with the
    441  * ThumbnaiLoader class.
    442  *
    443  * @param {Gallery.Item} item Item representing the image 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.
    448  */
    449 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) {
    450   var entry = item.getEntry();
    451 
    452   this.cancel();
    453   this.entry_ = entry;
    454   this.callback_ = callback;
    455 
    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_) {
    460       this.convertImage_(
    461           image, transform || { scaleX: 1, scaleY: 1, rotate90: 0});
    462     }
    463   }.bind(this);
    464 
    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);
    474   }.bind(this);
    475 
    476   var loadImage = function() {
    477     ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime'));
    478     this.timeout_ = null;
    479 
    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);
    487       });
    488     }.bind(this);
    489 
    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');
    493 
    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();
    497   }.bind(this);
    498 
    499   // Loads the image. If already loaded, then forces a reload.
    500   var startLoad = this.resetImage_.bind(this, function() {
    501     loadImage();
    502   }.bind(this), onError);
    503 
    504   if (opt_delay) {
    505     this.timeout_ = setTimeout(startLoad, opt_delay);
    506   } else {
    507     startLoad();
    508   }
    509 };
    510 
    511 /**
    512  * Resets the image by forcing the garbage collection and clearing the src
    513  * attribute.
    514  *
    515  * @param {function()} onSuccess Success callback.
    516  * @param {function(opt_string)} onError Failure callback with an optional
    517  *     error identifier.
    518  * @private
    519  */
    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 = '';
    525   }.bind(this);
    526 
    527   var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' +
    528       'AAABAAEAAAICTAEAOw==';
    529 
    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;
    535   } else {
    536     // Empty image already loaded, so clear src immediately.
    537     clearSrc();
    538   }
    539 };
    540 
    541 /**
    542  * @return {boolean} True if an image is loading.
    543  */
    544 ImageUtil.ImageLoader.prototype.isBusy = function() {
    545   return !!this.callback_;
    546 };
    547 
    548 /**
    549  * @param {Entry} entry Image entry.
    550  * @return {boolean} True if loader loads this image.
    551  */
    552 ImageUtil.ImageLoader.prototype.isLoading = function(entry) {
    553   return this.isBusy() && util.isSameEntry(this.entry_, entry);
    554 };
    555 
    556 /**
    557  * @param {function} callback To be called when the 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'];
    695