| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 // Namespace object for the utilities. | 5 // Namespace object for the utilities. |
| 6 var ImageUtil = {}; | 6 var ImageUtil = {}; |
| 7 | 7 |
| 8 /** | 8 /** |
| 9 * Performance trace. | 9 * Performance trace. |
| 10 */ | 10 */ |
| (...skipping 402 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 413 */ | 413 */ |
| 414 ImageUtil.setClass = function(element, className, on) { | 414 ImageUtil.setClass = function(element, className, on) { |
| 415 var cl = element.classList; | 415 var cl = element.classList; |
| 416 if (on) | 416 if (on) |
| 417 cl.add(className); | 417 cl.add(className); |
| 418 else | 418 else |
| 419 cl.remove(className); | 419 cl.remove(className); |
| 420 }; | 420 }; |
| 421 | 421 |
| 422 /** | 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 * @param {!MetadataModel} metadataModel | |
| 431 * @constructor | |
| 432 * @struct | |
| 433 */ | |
| 434 ImageUtil.ImageLoader = function(document, metadataModel) { | |
| 435 this.document_ = document; | |
| 436 | |
| 437 /** | |
| 438 * @private {!MetadataModel} | |
| 439 * @const | |
| 440 */ | |
| 441 this.metadataModel_ = metadataModel; | |
| 442 | |
| 443 this.generation_ = 0; | |
| 444 | |
| 445 /** | |
| 446 * @type {number} | |
| 447 * @private | |
| 448 */ | |
| 449 this.timeout_ = 0; | |
| 450 | |
| 451 /** | |
| 452 * @type {?function(!HTMLCanvasElement, string=)} | |
| 453 * @private | |
| 454 */ | |
| 455 this.callback_ = null; | |
| 456 | |
| 457 /** | |
| 458 * @type {FileEntry} | |
| 459 * @private | |
| 460 */ | |
| 461 this.entry_ = null; | |
| 462 }; | |
| 463 | |
| 464 /** | |
| 465 * Loads an image. | |
| 466 * TODO(mtomasz): Simplify, or even get rid of this class and merge with the | |
| 467 * ThumbnaiLoader class. | |
| 468 * | |
| 469 * @param {!GalleryItem} item Item representing the image to be loaded. | |
| 470 * @param {function(!HTMLCanvasElement, string=)} callback Callback to be | |
| 471 * called when loaded. The second optional argument is an error identifier. | |
| 472 * @param {number=} opt_delay Load delay in milliseconds, useful to let the | |
| 473 * animations play out before the computation heavy image loading starts. | |
| 474 */ | |
| 475 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) { | |
| 476 var entry = item.getEntry(); | |
| 477 | |
| 478 this.cancel(); | |
| 479 this.entry_ = entry; | |
| 480 this.callback_ = callback; | |
| 481 | |
| 482 var targetImage = assertInstanceof(this.document_.createElement('img'), | |
| 483 HTMLImageElement); | |
| 484 // The transform fetcher is not cancellable so we need a generation counter. | |
| 485 var generation = ++this.generation_; | |
| 486 | |
| 487 /** | |
| 488 * @param {!HTMLImageElement} image Image to be transformed. | |
| 489 * @param {Object=} opt_transform Transformation. | |
| 490 */ | |
| 491 var onTransform = function(image, opt_transform) { | |
| 492 if (generation === this.generation_) { | |
| 493 this.convertImage_(image, opt_transform); | |
| 494 } | |
| 495 }; | |
| 496 onTransform = onTransform.bind(this); | |
| 497 | |
| 498 /** | |
| 499 * @param {string=} opt_error Error. | |
| 500 */ | |
| 501 var onError = function(opt_error) { | |
| 502 targetImage.onerror = null; | |
| 503 targetImage.onload = null; | |
| 504 var tmpCallback = this.callback_; | |
| 505 this.callback_ = null; | |
| 506 var emptyCanvas = assertInstanceof(this.document_.createElement('canvas'), | |
| 507 HTMLCanvasElement); | |
| 508 emptyCanvas.width = 0; | |
| 509 emptyCanvas.height = 0; | |
| 510 tmpCallback(emptyCanvas, opt_error); | |
| 511 }; | |
| 512 onError = onError.bind(this); | |
| 513 | |
| 514 var loadImage = function(url) { | |
| 515 if (generation !== this.generation_) | |
| 516 return; | |
| 517 | |
| 518 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime')); | |
| 519 this.timeout_ = 0; | |
| 520 | |
| 521 targetImage.onload = function() { | |
| 522 targetImage.onerror = null; | |
| 523 targetImage.onload = null; | |
| 524 if (generation !== this.generation_) | |
| 525 return; | |
| 526 this.metadataModel_.get([entry], ['contentImageTransform']).then( | |
| 527 function(metadataItems) { | |
| 528 onTransform(targetImage, metadataItems[0].contentImageTransform); | |
| 529 }.bind(this)); | |
| 530 }.bind(this); | |
| 531 | |
| 532 // The error callback has an optional error argument, which in case of a | |
| 533 // general error should not be specified | |
| 534 targetImage.onerror = onError.bind(null, 'GALLERY_IMAGE_ERROR'); | |
| 535 | |
| 536 targetImage.src = url; | |
| 537 }.bind(this); | |
| 538 | |
| 539 // Loads the image. If already loaded, then forces a reload. | |
| 540 var startLoad = function() { | |
| 541 if (generation !== this.generation_) | |
| 542 return; | |
| 543 | |
| 544 // Obtain target URL. | |
| 545 if (FileType.isRaw(entry)) { | |
| 546 var timestamp = | |
| 547 item.getMetadataItem() && | |
| 548 item.getMetadataItem().modificationTime && | |
| 549 item.getMetadataItem().modificationTime.getTime(); | |
| 550 ImageLoaderClient.getInstance().load(entry.toURL(), function(result) { | |
| 551 if (generation !== this.generation_) | |
| 552 return; | |
| 553 if (result.status === 'success') | |
| 554 loadImage(result.data); | |
| 555 else | |
| 556 onError('GALLERY_IMAGE_ERROR'); | |
| 557 }.bind(this), { | |
| 558 cache: true, | |
| 559 timestamp: timestamp, | |
| 560 priority: 0 // Use highest priority to show main image. | |
| 561 }); | |
| 562 return; | |
| 563 } | |
| 564 | |
| 565 // Load the image directly. The query parameter is workaround for | |
| 566 // crbug.com/379678, which force to update the contents of the image. | |
| 567 loadImage(entry.toURL() + '?nocache=' + Date.now()); | |
| 568 }.bind(this); | |
| 569 | |
| 570 if (opt_delay) { | |
| 571 this.timeout_ = setTimeout(startLoad, opt_delay); | |
| 572 } else { | |
| 573 startLoad(); | |
| 574 } | |
| 575 }; | |
| 576 | |
| 577 /** | |
| 578 * @return {boolean} True if an image is loading. | |
| 579 */ | |
| 580 ImageUtil.ImageLoader.prototype.isBusy = function() { | |
| 581 return !!this.callback_; | |
| 582 }; | |
| 583 | |
| 584 /** | |
| 585 * @param {Entry} entry Image entry. | |
| 586 * @return {boolean} True if loader loads this image. | |
| 587 */ | |
| 588 ImageUtil.ImageLoader.prototype.isLoading = function(entry) { | |
| 589 return this.isBusy() && util.isSameEntry(this.entry_, entry); | |
| 590 }; | |
| 591 | |
| 592 /** | |
| 593 * @param {function(!HTMLCanvasElement, string=)} callback To be called when the | |
| 594 * image loaded. | |
| 595 */ | |
| 596 ImageUtil.ImageLoader.prototype.setCallback = function(callback) { | |
| 597 this.callback_ = callback; | |
| 598 }; | |
| 599 | |
| 600 /** | |
| 601 * Stops loading image. | |
| 602 */ | |
| 603 ImageUtil.ImageLoader.prototype.cancel = function() { | |
| 604 if (!this.callback_) | |
| 605 return; | |
| 606 this.callback_ = null; | |
| 607 if (this.timeout_) { | |
| 608 clearTimeout(this.timeout_); | |
| 609 this.timeout_ = 0; | |
| 610 } | |
| 611 this.generation_++; // Silence the transform fetcher if it is in progress. | |
| 612 }; | |
| 613 | |
| 614 /** | |
| 615 * @param {!HTMLImageElement} image Image to be transformed. | |
| 616 * @param {!Object} transform transformation description to apply to the image. | |
| 617 * @private | |
| 618 */ | |
| 619 ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) { | |
| 620 if (!transform || | |
| 621 (transform.rotate90 === 0 && | |
| 622 transform.scaleX === 1 && | |
| 623 transform.scaleY === 1)) { | |
| 624 setTimeout(this.callback_, 0, image); | |
| 625 this.callback_ = null; | |
| 626 return; | |
| 627 } | |
| 628 var canvas = this.document_.createElement('canvas'); | |
| 629 | |
| 630 if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. | |
| 631 canvas.width = image.height; | |
| 632 canvas.height = image.width; | |
| 633 } else { | |
| 634 canvas.width = image.width; | |
| 635 canvas.height = image.height; | |
| 636 } | |
| 637 | |
| 638 var context = canvas.getContext('2d'); | |
| 639 context.save(); | |
| 640 context.translate(canvas.width / 2, canvas.height / 2); | |
| 641 context.rotate(transform.rotate90 * Math.PI / 2); | |
| 642 context.scale(transform.scaleX, transform.scaleY); | |
| 643 | |
| 644 var stripCount = Math.ceil(image.width * image.height / (1 << 21)); | |
| 645 var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0; | |
| 646 | |
| 647 this.copyStrip_(context, image, 0, step); | |
| 648 }; | |
| 649 | |
| 650 /** | |
| 651 * @param {!CanvasRenderingContext2D} context Context to draw. | |
| 652 * @param {!HTMLImageElement} image Image to draw. | |
| 653 * @param {number} firstRow Number of the first pixel row to draw. | |
| 654 * @param {number} rowCount Count of pixel rows to draw. | |
| 655 * @private | |
| 656 */ | |
| 657 ImageUtil.ImageLoader.prototype.copyStrip_ = function( | |
| 658 context, image, firstRow, rowCount) { | |
| 659 var lastRow = Math.min(firstRow + rowCount, image.height); | |
| 660 | |
| 661 context.drawImage( | |
| 662 image, 0, firstRow, image.width, lastRow - firstRow, | |
| 663 -image.width / 2, firstRow - image.height / 2, | |
| 664 image.width, lastRow - firstRow); | |
| 665 | |
| 666 if (lastRow === image.height) { | |
| 667 context.restore(); | |
| 668 if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls. | |
| 669 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime')); | |
| 670 } | |
| 671 setTimeout(this.callback_, 0, context.canvas); | |
| 672 this.callback_ = null; | |
| 673 } else { | |
| 674 var self = this; | |
| 675 this.timeout_ = setTimeout( | |
| 676 function() { | |
| 677 self.timeout_ = 0; | |
| 678 self.copyStrip_(context, image, lastRow, rowCount); | |
| 679 }, 0); | |
| 680 } | |
| 681 }; | |
| 682 | |
| 683 /** | |
| 684 * @param {!HTMLElement} element To remove children from. | 423 * @param {!HTMLElement} element To remove children from. |
| 685 */ | 424 */ |
| 686 ImageUtil.removeChildren = function(element) { | 425 ImageUtil.removeChildren = function(element) { |
| 687 element.textContent = ''; | 426 element.textContent = ''; |
| 688 }; | 427 }; |
| 689 | 428 |
| 690 /** | 429 /** |
| 691 * @param {string} name File name (with extension). | 430 * @param {string} name File name (with extension). |
| 692 * @return {string} File name without extension. | 431 * @return {string} File name without extension. |
| 693 */ | 432 */ |
| (...skipping 11 matching lines...) Expand all Loading... |
| 705 */ | 444 */ |
| 706 ImageUtil.getExtensionFromFullName = function(name) { | 445 ImageUtil.getExtensionFromFullName = function(name) { |
| 707 var index = name.lastIndexOf('.'); | 446 var index = name.lastIndexOf('.'); |
| 708 if (index !== -1) | 447 if (index !== -1) |
| 709 return name.substring(index); | 448 return name.substring(index); |
| 710 else | 449 else |
| 711 return ''; | 450 return ''; |
| 712 }; | 451 }; |
| 713 | 452 |
| 714 /** | 453 /** |
| 715 * Metrics (from metrics.js) itnitialized by the File Manager from owner frame. | |
| 716 * @type {Object} | |
| 717 */ | |
| 718 ImageUtil.metrics = null; | |
| 719 | |
| 720 /** | |
| 721 * @param {string} name Local name. | 454 * @param {string} name Local name. |
| 722 * @return {string} Full name. | 455 * @return {string} Full name. |
| 723 */ | 456 */ |
| 724 ImageUtil.getMetricName = function(name) { | 457 ImageUtil.getMetricName = function(name) { |
| 725 return 'PhotoEditor.' + name; | 458 return 'PhotoEditor.' + name; |
| 726 }; | 459 }; |
| 727 | 460 |
| 728 /** | 461 /** |
| 729 * Ensures argument is canvas. If it's not, creates new canvas and copy. | 462 * Ensures argument is canvas. If it's not, creates new canvas and copy. |
| 730 * | 463 * |
| (...skipping 13 matching lines...) Expand all Loading... |
| 744 context.drawImage(imgOrCanvas, 0, 0); | 477 context.drawImage(imgOrCanvas, 0, 0); |
| 745 return canvas; | 478 return canvas; |
| 746 }; | 479 }; |
| 747 | 480 |
| 748 /** | 481 /** |
| 749 * Used for metrics reporting, keep in sync with the histogram description. | 482 * Used for metrics reporting, keep in sync with the histogram description. |
| 750 * @type {Array<string>} | 483 * @type {Array<string>} |
| 751 * @const | 484 * @const |
| 752 */ | 485 */ |
| 753 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp']; | 486 ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp']; |
| OLD | NEW |