| OLD | NEW |
| (Empty) |
| 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. | |
| 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 /** | |
| 143 * @param {number} factor Factor to scale. | |
| 144 * @return {Rect} A rectangle with every dimension scaled. | |
| 145 */ | |
| 146 Rect.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 */ | |
| 159 Rect.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 */ | |
| 168 Rect.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 */ | |
| 177 Rect.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 */ | |
| 187 Rect.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 */ | |
| 196 Rect.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 */ | |
| 207 Rect.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 */ | |
| 217 Rect.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 */ | |
| 227 Rect.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 */ | |
| 256 Rect.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 */ | |
| 271 Rect.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 */ | |
| 286 Rect.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 */ | |
| 296 Rect.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 */ | |
| 306 Rect.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 */ | |
| 336 function 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 */ | |
| 348 Circle.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 */ | |
| 363 ImageUtil.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 */ | |
| 379 ImageUtil.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 */ | |
| 392 ImageUtil.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 URL 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 */ | |
| 412 ImageUtil.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 */ | |
| 422 ImageUtil.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 */ | |
| 429 ImageUtil.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 {string} url Image URL. | |
| 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 */ | |
| 446 ImageUtil.ImageLoader.prototype.load = function( | |
| 447 url, transformFetcher, callback, opt_delay) { | |
| 448 this.cancel(); | |
| 449 | |
| 450 this.url_ = url; | |
| 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('IMAGE_TOO_BIG_ERROR'); | |
| 483 return; | |
| 484 } | |
| 485 transformFetcher(url, 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, '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 = url; | |
| 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(url, '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 */ | |
| 527 ImageUtil.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, '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 */ | |
| 551 ImageUtil.ImageLoader.prototype.isBusy = function() { | |
| 552 return !!this.callback_; | |
| 553 }; | |
| 554 | |
| 555 /** | |
| 556 * @param {string} url Image url. | |
| 557 * @return {boolean} True if loader loads this image. | |
| 558 */ | |
| 559 ImageUtil.ImageLoader.prototype.isLoading = function(url) { | |
| 560 return this.isBusy() && (this.url_ == url); | |
| 561 }; | |
| 562 | |
| 563 /** | |
| 564 * @param {function} callback To be called when the image loaded. | |
| 565 */ | |
| 566 ImageUtil.ImageLoader.prototype.setCallback = function(callback) { | |
| 567 this.callback_ = callback; | |
| 568 }; | |
| 569 | |
| 570 /** | |
| 571 * Stops loading image. | |
| 572 */ | |
| 573 ImageUtil.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 */ | |
| 593 ImageUtil.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 */ | |
| 623 ImageUtil.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.url_.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 */ | |
| 656 ImageUtil.removeChildren = function(element) { | |
| 657 element.textContent = ''; | |
| 658 }; | |
| 659 | |
| 660 /** | |
| 661 * @param {string} url "filesystem:" URL. | |
| 662 * @return {string} File name. | |
| 663 */ | |
| 664 ImageUtil.getFullNameFromUrl = function(url) { | |
| 665 url = decodeURIComponent(url); | |
| 666 if (url.indexOf('/') != -1) | |
| 667 return url.substr(url.lastIndexOf('/') + 1); | |
| 668 else | |
| 669 return url; | |
| 670 }; | |
| 671 | |
| 672 /** | |
| 673 * @param {string} name File name (with extension). | |
| 674 * @return {string} File name without extension. | |
| 675 */ | |
| 676 ImageUtil.getFileNameFromFullName = function(name) { | |
| 677 var index = name.lastIndexOf('.'); | |
| 678 if (index != -1) | |
| 679 return name.substr(0, index); | |
| 680 else | |
| 681 return name; | |
| 682 }; | |
| 683 | |
| 684 /** | |
| 685 * @param {string} url "filesystem:" URL. | |
| 686 * @return {string} File name. | |
| 687 */ | |
| 688 ImageUtil.getFileNameFromUrl = function(url) { | |
| 689 return ImageUtil.getFileNameFromFullName(ImageUtil.getFullNameFromUrl(url)); | |
| 690 }; | |
| 691 | |
| 692 /** | |
| 693 * @param {string} fullName Original file name. | |
| 694 * @param {string} name New file name without extension. | |
| 695 * @return {string} New file name with base of |name| and extension of | |
| 696 * |fullName|. | |
| 697 */ | |
| 698 ImageUtil.replaceFileNameInFullName = function(fullName, name) { | |
| 699 var index = fullName.lastIndexOf('.'); | |
| 700 if (index != -1) | |
| 701 return name + fullName.substr(index); | |
| 702 else | |
| 703 return name; | |
| 704 }; | |
| 705 | |
| 706 /** | |
| 707 * @param {string} name File name. | |
| 708 * @return {string} File extension. | |
| 709 */ | |
| 710 ImageUtil.getExtensionFromFullName = function(name) { | |
| 711 var index = name.lastIndexOf('.'); | |
| 712 if (index != -1) | |
| 713 return name.substring(index); | |
| 714 else | |
| 715 return ''; | |
| 716 }; | |
| 717 | |
| 718 /** | |
| 719 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame. | |
| 720 * @type {Object?} | |
| 721 */ | |
| 722 ImageUtil.metrics = null; | |
| 723 | |
| 724 /** | |
| 725 * @param {string} name Local name. | |
| 726 * @return {string} Full name. | |
| 727 */ | |
| 728 ImageUtil.getMetricName = function(name) { | |
| 729 return 'PhotoEditor.' + name; | |
| 730 }; | |
| 731 | |
| 732 /** | |
| 733 * Used for metrics reporting, keep in sync with the histogram description. | |
| 734 */ | |
| 735 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp']; | |
| OLD | NEW |