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