1// Copyright (c) 2012 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. 9function ImageUtil() {} 10 11/** 12 * Performance trace. 13 */ 14ImageUtil.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 */ 64ImageUtil.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 */ 74ImageUtil.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 */ 93function 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/** 143 * @param {number} factor Factor to scale. 144 * @return {Rect} A rectangle with every dimension scaled. 145 */ 146Rect.prototype.scale = function(factor) { 147 return new Rect( 148 this.left * factor, 149 this.top * factor, 150 this.width * factor, 151 this.height * factor); 152}; 153 154/** 155 * @param {number} dx Difference in X. 156 * @param {number} dy Difference in Y. 157 * @return {Rect} A rectangle shifted by (dx,dy), same size. 158 */ 159Rect.prototype.shift = function(dx, dy) { 160 return new Rect(this.left + dx, this.top + dy, this.width, this.height); 161}; 162 163/** 164 * @param {number} x Coordinate of the left top corner. 165 * @param {number} y Coordinate of the left top corner. 166 * @return {Rect} A rectangle with left==x and top==y, same size. 167 */ 168Rect.prototype.moveTo = function(x, y) { 169 return new Rect(x, y, this.width, this.height); 170}; 171 172/** 173 * @param {number} dx Difference in X. 174 * @param {number} dy Difference in Y. 175 * @return {Rect} A rectangle inflated by (dx, dy), same center. 176 */ 177Rect.prototype.inflate = function(dx, dy) { 178 return new Rect( 179 this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy); 180}; 181 182/** 183 * @param {number} x Coordinate of the point. 184 * @param {number} y Coordinate of the point. 185 * @return {boolean} True if the point lies inside the rectangle. 186 */ 187Rect.prototype.inside = function(x, y) { 188 return this.left <= x && x < this.left + this.width && 189 this.top <= y && y < this.top + this.height; 190}; 191 192/** 193 * @param {Rect} rect Rectangle to check. 194 * @return {boolean} True if this rectangle intersects with the |rect|. 195 */ 196Rect.prototype.intersects = function(rect) { 197 return (this.left + this.width) > rect.left && 198 (rect.left + rect.width) > this.left && 199 (this.top + this.height) > rect.top && 200 (rect.top + rect.height) > this.top; 201}; 202 203/** 204 * @param {Rect} rect Rectangle to check. 205 * @return {boolean} True if this rectangle containing the |rect|. 206 */ 207Rect.prototype.contains = function(rect) { 208 return (this.left <= rect.left) && 209 (rect.left + rect.width) <= (this.left + this.width) && 210 (this.top <= rect.top) && 211 (rect.top + rect.height) <= (this.top + this.height); 212}; 213 214/** 215 * @return {boolean} True if rectangle is empty. 216 */ 217Rect.prototype.isEmpty = function() { 218 return this.width === 0 || this.height === 0; 219}; 220 221/** 222 * Clamp the rectangle to the bounds by moving it. 223 * Decrease the size only if necessary. 224 * @param {Rect} bounds Bounds. 225 * @return {Rect} Calculated rectangle. 226 */ 227Rect.prototype.clamp = function(bounds) { 228 var rect = new Rect(this); 229 230 if (rect.width > bounds.width) { 231 rect.left = bounds.left; 232 rect.width = bounds.width; 233 } else if (rect.left < bounds.left) { 234 rect.left = bounds.left; 235 } else if (rect.left + rect.width > 236 bounds.left + bounds.width) { 237 rect.left = bounds.left + bounds.width - rect.width; 238 } 239 240 if (rect.height > bounds.height) { 241 rect.top = bounds.top; 242 rect.height = bounds.height; 243 } else if (rect.top < bounds.top) { 244 rect.top = bounds.top; 245 } else if (rect.top + rect.height > 246 bounds.top + bounds.height) { 247 rect.top = bounds.top + bounds.height - rect.height; 248 } 249 250 return rect; 251}; 252 253/** 254 * @return {string} String representation. 255 */ 256Rect.prototype.toString = function() { 257 return '(' + this.left + ',' + this.top + '):' + 258 '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')'; 259}; 260/* 261 * Useful shortcuts for drawing (static functions). 262 */ 263 264/** 265 * Draw the image in context with appropriate scaling. 266 * @param {CanvasRenderingContext2D} context Context to draw. 267 * @param {Image} image Image to draw. 268 * @param {Rect=} opt_dstRect Rectangle in the canvas (whole canvas by default). 269 * @param {Rect=} opt_srcRect Rectangle in the image (whole image by default). 270 */ 271Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) { 272 opt_dstRect = opt_dstRect || new Rect(context.canvas); 273 opt_srcRect = opt_srcRect || new Rect(image); 274 if (opt_dstRect.isEmpty() || opt_srcRect.isEmpty()) 275 return; 276 context.drawImage(image, 277 opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height, 278 opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height); 279}; 280 281/** 282 * Draw a box around the rectangle. 283 * @param {CanvasRenderingContext2D} context Context to draw. 284 * @param {Rect} rect Rectangle. 285 */ 286Rect.outline = function(context, rect) { 287 context.strokeRect( 288 rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1); 289}; 290 291/** 292 * Fill the rectangle. 293 * @param {CanvasRenderingContext2D} context Context to draw. 294 * @param {Rect} rect Rectangle. 295 */ 296Rect.fill = function(context, rect) { 297 context.fillRect(rect.left, rect.top, rect.width, rect.height); 298}; 299 300/** 301 * Fills the space between the two rectangles. 302 * @param {CanvasRenderingContext2D} context Context to draw. 303 * @param {Rect} inner Inner rectangle. 304 * @param {Rect} outer Outer rectangle. 305 */ 306Rect.fillBetween = function(context, inner, outer) { 307 var innerRight = inner.left + inner.width; 308 var innerBottom = inner.top + inner.height; 309 var outerRight = outer.left + outer.width; 310 var outerBottom = outer.top + outer.height; 311 if (inner.top > outer.top) { 312 context.fillRect( 313 outer.left, outer.top, outer.width, inner.top - outer.top); 314 } 315 if (inner.left > outer.left) { 316 context.fillRect( 317 outer.left, inner.top, inner.left - outer.left, inner.height); 318 } 319 if (inner.width < outerRight) { 320 context.fillRect( 321 innerRight, inner.top, outerRight - innerRight, inner.height); 322 } 323 if (inner.height < outerBottom) { 324 context.fillRect( 325 outer.left, innerBottom, outer.width, outerBottom - innerBottom); 326 } 327}; 328 329/** 330 * Circle class. 331 * @param {number} x X coordinate of circle center. 332 * @param {number} y Y coordinate of circle center. 333 * @param {number} r Radius. 334 * @constructor 335 */ 336function Circle(x, y, r) { 337 this.x = x; 338 this.y = y; 339 this.squaredR = r * r; 340} 341 342/** 343 * Check if the point is inside the circle. 344 * @param {number} x X coordinate of the point. 345 * @param {number} y Y coordinate of the point. 346 * @return {boolean} True if the point is inside. 347 */ 348Circle.prototype.inside = function(x, y) { 349 x -= this.x; 350 y -= this.y; 351 return x * x + y * y <= this.squaredR; 352}; 353 354/** 355 * Copy an image applying scaling and rotation. 356 * 357 * @param {HTMLCanvasElement} dst Destination. 358 * @param {HTMLCanvasElement|HTMLImageElement} src Source. 359 * @param {number} scaleX Y scale transformation. 360 * @param {number} scaleY X scale transformation. 361 * @param {number} angle (in radians). 362 */ 363ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) { 364 var context = dst.getContext('2d'); 365 context.save(); 366 context.translate(context.canvas.width / 2, context.canvas.height / 2); 367 context.rotate(angle); 368 context.scale(scaleX, scaleY); 369 context.drawImage(src, -src.width / 2, -src.height / 2); 370 context.restore(); 371}; 372 373/** 374 * Adds or removes an attribute to/from an HTML element. 375 * @param {HTMLElement} element To be applied to. 376 * @param {string} attribute Name of attribute. 377 * @param {boolean} on True if add, false if remove. 378 */ 379ImageUtil.setAttribute = function(element, attribute, on) { 380 if (on) 381 element.setAttribute(attribute, ''); 382 else 383 element.removeAttribute(attribute); 384}; 385 386/** 387 * Adds or removes CSS class to/from an HTML element. 388 * @param {HTMLElement} element To be applied to. 389 * @param {string} className Name of CSS class. 390 * @param {boolean} on True if add, false if remove. 391 */ 392ImageUtil.setClass = function(element, className, on) { 393 var cl = element.classList; 394 if (on) 395 cl.add(className); 396 else 397 cl.remove(className); 398}; 399 400/** 401 * ImageLoader loads an image from a given Entry into a canvas in two steps: 402 * 1. Loads the image into an HTMLImageElement. 403 * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done 404 * stripe-by-stripe to avoid freezing up the UI. The transform is taken into 405 * account. 406 * 407 * @param {HTMLDocument} document Owner document. 408 * @param {MetadataCache=} opt_metadataCache Metadata cache. Required for 409 * caching. If not passed, caching will be disabled. 410 * @constructor 411 */ 412ImageUtil.ImageLoader = function(document, opt_metadataCache) { 413 this.document_ = document; 414 this.metadataCache_ = opt_metadataCache || null; 415 this.image_ = new Image(); 416 this.generation_ = 0; 417}; 418 419/** 420 * Max size of image to be displayed (in pixels) 421 */ 422ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT = 25 * 1000 * 1000; 423 424/** 425 * @param {number} width Width of the image. 426 * @param {number} height Height of the image. 427 * @return {boolean} True if the image is too large to be loaded. 428 */ 429ImageUtil.ImageLoader.isTooLarge = function(width, height) { 430 return width * height > ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT; 431}; 432 433/** 434 * Loads an image. 435 * TODO(mtomasz): Simplify, or even get rid of this class and merge with the 436 * ThumbnaiLoader class. 437 * 438 * @param {FileEntry} entry Image entry to be loaded. 439 * @param {function(function(object))} transformFetcher function to get 440 * the image transform (which we need for the image orientation). 441 * @param {function(HTMLCanvasElement, string=)} callback Callback to be 442 * called when loaded. The second optional argument is an error identifier. 443 * @param {number=} opt_delay Load delay in milliseconds, useful to let the 444 * animations play out before the computation heavy image loading starts. 445 */ 446ImageUtil.ImageLoader.prototype.load = function( 447 entry, transformFetcher, callback, opt_delay) { 448 this.cancel(); 449 450 this.entry_ = entry; 451 this.callback_ = callback; 452 453 // The transform fetcher is not cancellable so we need a generation counter. 454 var generation = ++this.generation_; 455 var onTransform = function(image, transform) { 456 if (generation === this.generation_) { 457 this.convertImage_( 458 image, transform || { scaleX: 1, scaleY: 1, rotate90: 0}); 459 } 460 }; 461 462 var onError = function(opt_error) { 463 this.image_.onerror = null; 464 this.image_.onload = null; 465 var tmpCallback = this.callback_; 466 this.callback_ = null; 467 var emptyCanvas = this.document_.createElement('canvas'); 468 emptyCanvas.width = 0; 469 emptyCanvas.height = 0; 470 tmpCallback(emptyCanvas, opt_error); 471 }.bind(this); 472 473 var loadImage = function(opt_metadata) { 474 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime')); 475 this.timeout_ = null; 476 477 this.image_.onload = function(e) { 478 this.image_.onerror = null; 479 this.image_.onload = null; 480 if (ImageUtil.ImageLoader.isTooLarge(this.image_.width, 481 this.image_.height)) { 482 onError('GALLERY_IMAGE_TOO_BIG_ERROR'); 483 return; 484 } 485 transformFetcher(entry, onTransform.bind(this, e.target)); 486 }.bind(this); 487 488 // The error callback has an optional error argument, which in case of a 489 // general error should not be specified 490 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR'); 491 492 // Extract the last modification date to determine if the cached image 493 // is outdated. 494 var modificationTime = opt_metadata && 495 opt_metadata.modificationTime && 496 opt_metadata.modificationTime.getTime(); 497 498 // Load the image directly. 499 this.image_.src = entry.toURL(); 500 }.bind(this); 501 502 // Loads the image. If already loaded, then forces a reload. 503 var startLoad = this.resetImage_.bind(this, function() { 504 // Fetch metadata to detect last modification time for the caching purpose. 505 if (this.metadataCache_) 506 this.metadataCache_.get(entry, 'filesystem', loadImage); 507 else 508 loadImage(); 509 }.bind(this), onError); 510 511 if (opt_delay) { 512 this.timeout_ = setTimeout(startLoad, opt_delay); 513 } else { 514 startLoad(); 515 } 516}; 517 518/** 519 * Resets the image by forcing the garbage collection and clearing the src 520 * attribute. 521 * 522 * @param {function()} onSuccess Success callback. 523 * @param {function(opt_string)} onError Failure callback with an optional 524 * error identifier. 525 * @private 526 */ 527ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) { 528 var clearSrc = function() { 529 this.image_.onload = onSuccess; 530 this.image_.onerror = onSuccess; 531 this.image_.src = ''; 532 }.bind(this); 533 534 var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' + 535 'AAABAAEAAAICTAEAOw=='; 536 537 if (this.image_.src !== emptyImage) { 538 // Load an empty image, then clear src. 539 this.image_.onload = clearSrc; 540 this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR'); 541 this.image_.src = emptyImage; 542 } else { 543 // Empty image already loaded, so clear src immediately. 544 clearSrc(); 545 } 546}; 547 548/** 549 * @return {boolean} True if an image is loading. 550 */ 551ImageUtil.ImageLoader.prototype.isBusy = function() { 552 return !!this.callback_; 553}; 554 555/** 556 * @param {Entry} entry Image entry. 557 * @return {boolean} True if loader loads this image. 558 */ 559ImageUtil.ImageLoader.prototype.isLoading = function(entry) { 560 return this.isBusy() && util.isSameEntry(this.entry_, entry); 561}; 562 563/** 564 * @param {function} callback To be called when the image loaded. 565 */ 566ImageUtil.ImageLoader.prototype.setCallback = function(callback) { 567 this.callback_ = callback; 568}; 569 570/** 571 * Stops loading image. 572 */ 573ImageUtil.ImageLoader.prototype.cancel = function() { 574 if (!this.callback_) return; 575 this.callback_ = null; 576 if (this.timeout_) { 577 clearTimeout(this.timeout_); 578 this.timeout_ = null; 579 } 580 if (this.image_) { 581 this.image_.onload = function() {}; 582 this.image_.onerror = function() {}; 583 this.image_.src = ''; 584 } 585 this.generation_++; // Silence the transform fetcher if it is in progress. 586}; 587 588/** 589 * @param {HTMLImageElement} image Image to be transformed. 590 * @param {Object} transform transformation description to apply to the image. 591 * @private 592 */ 593ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) { 594 var canvas = this.document_.createElement('canvas'); 595 596 if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. 597 canvas.width = image.height; 598 canvas.height = image.width; 599 } else { 600 canvas.width = image.width; 601 canvas.height = image.height; 602 } 603 604 var context = canvas.getContext('2d'); 605 context.save(); 606 context.translate(canvas.width / 2, canvas.height / 2); 607 context.rotate(transform.rotate90 * Math.PI / 2); 608 context.scale(transform.scaleX, transform.scaleY); 609 610 var stripCount = Math.ceil(image.width * image.height / (1 << 21)); 611 var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0; 612 613 this.copyStrip_(context, image, 0, step); 614}; 615 616/** 617 * @param {CanvasRenderingContext2D} context Context to draw. 618 * @param {HTMLImageElement} image Image to draw. 619 * @param {number} firstRow Number of the first pixel row to draw. 620 * @param {number} rowCount Count of pixel rows to draw. 621 * @private 622 */ 623ImageUtil.ImageLoader.prototype.copyStrip_ = function( 624 context, image, firstRow, rowCount) { 625 var lastRow = Math.min(firstRow + rowCount, image.height); 626 627 context.drawImage( 628 image, 0, firstRow, image.width, lastRow - firstRow, 629 -image.width / 2, firstRow - image.height / 2, 630 image.width, lastRow - firstRow); 631 632 if (lastRow === image.height) { 633 context.restore(); 634 if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls. 635 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime')); 636 } 637 try { 638 setTimeout(this.callback_, 0, context.canvas); 639 } catch (e) { 640 console.error(e); 641 } 642 this.callback_ = null; 643 } else { 644 var self = this; 645 this.timeout_ = setTimeout( 646 function() { 647 self.timeout_ = null; 648 self.copyStrip_(context, image, lastRow, rowCount); 649 }, 0); 650 } 651}; 652 653/** 654 * @param {HTMLElement} element To remove children from. 655 */ 656ImageUtil.removeChildren = function(element) { 657 element.textContent = ''; 658}; 659 660/** 661 * @param {string} name File name (with extension). 662 * @return {string} File name without extension. 663 */ 664ImageUtil.getDisplayNameFromName = function(name) { 665 var index = name.lastIndexOf('.'); 666 if (index !== -1) 667 return name.substr(0, index); 668 else 669 return name; 670}; 671 672/** 673 * @param {string} name File name. 674 * @return {string} File extension. 675 */ 676ImageUtil.getExtensionFromFullName = function(name) { 677 var index = name.lastIndexOf('.'); 678 if (index !== -1) 679 return name.substring(index); 680 else 681 return ''; 682}; 683 684/** 685 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame. 686 * @type {Object?} 687 */ 688ImageUtil.metrics = null; 689 690/** 691 * @param {string} name Local name. 692 * @return {string} Full name. 693 */ 694ImageUtil.getMetricName = function(name) { 695 return 'PhotoEditor.' + name; 696}; 697 698/** 699 * Used for metrics reporting, keep in sync with the histogram description. 700 */ 701ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp']; 702