Merge "Add support for images in diffs"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 3175517..da40a9d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -272,6 +272,7 @@
         </div>
       </section>
       <gr-file-list id="fileList"
+          change="[[_change]]"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           comments="[[_comments]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 976cf9b..f15c654 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -170,6 +170,8 @@
         </div>
       </div>
       <gr-diff hidden
+          project="[[change.project]]"
+          commit="[[change.current_revision]]"
           change-num="[[changeNum]]"
           patch-range="[[patchRange]]"
           path="[[file.__path]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 3257469..15f835d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -36,6 +36,7 @@
         type: Object,
         value: function() { return document.body; },
       },
+      change: Object,
 
       _files: {
         type: Array,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 0e99a15..903eabf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -174,12 +174,16 @@
             available-patches="[[_computeAvailablePatches(_change.revisions)]]">
         </gr-patch-range-select>
         <div>
-          <select id="modeSelect" on-change="_handleModeChange">
+          <select
+              id="modeSelect"
+              on-change="_handleModeChange"
+              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
           <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
-            /
+            <span
+                hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
             <gr-button link
                 class="prefsButton"
                 on-tap="_handlePrefsTap">Preferences</gr-button>
@@ -193,6 +197,9 @@
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
       <gr-diff id="diff"
+          project="[[_change.project]]"
+          commit="[[_change.current_revision]]"
+          is-image-diff="{{_isImageDiff}}"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           path="[[_path]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index c75cb72..17aa0c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -74,6 +74,7 @@
         type: String,
         computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
       },
+      _isImageDiff: Boolean,
     },
 
     behaviors: [
@@ -293,11 +294,11 @@
         this._userPrefs = prefs;
       }.bind(this)));
 
-      promises.push(this.$.diff.reload());
+      promises.push(this._getChangeDetail(this._changeNum));
 
-      Promise.all(promises).then(function() {
-        this._loading = false;
-      }.bind(this));
+      Promise.all(promises)
+          .then(function() { return this.$.diff.reload(); }.bind(this))
+          .then(function() { this._loading = false; }.bind(this));
     },
 
     _pathChanged: function(path) {
@@ -462,5 +463,9 @@
         this.$.modeSelect.value = mode;
       }
     },
+
+    _computeModeSelectHidden: function() {
+      return this._isImageDiff;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
new file mode 100644
index 0000000..b897708
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
@@ -0,0 +1,100 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the 'License');
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window, GrDiffBuilderSideBySide) {
+  'use strict';
+
+  function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
+      revisionImage) {
+    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+    this._baseImage = baseImage;
+    this._revisionImage = revisionImage;
+  }
+
+  GrDiffBuilderImage.prototype = Object.create(
+      GrDiffBuilderSideBySide.prototype);
+  GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+
+  GrDiffBuilderImage.prototype.emitDiff = function() {
+    this.emitGroup(this._groups[0]);
+
+    var section = this._createElement('tbody', 'image-diff');
+
+    this._emitImagePair(section);
+    this._emitImageLabels(section);
+
+    this._outputEl.appendChild(section);
+  };
+
+  GrDiffBuilderImage.prototype._emitImagePair = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._baseImage, 'left'));
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
+    var td = this._createElement('td', className);
+    if (image) {
+      var imageEl = this._createElement('img');
+      imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
+      image._height = imageEl.naturalHeight;
+      image._width = imageEl.naturalWidth;
+      imageEl.addEventListener('error', function(e) {
+        imageEl.remove();
+        td.textContent = '[Image failed to load]';
+      });
+      td.appendChild(imageEl);
+    }
+    return td;
+  };
+
+  GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    var td = this._createElement('td', 'left');
+    var label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._baseImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    tr.appendChild(this._createElement('td'));
+    td = this._createElement('td', 'right');
+    label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._revisionImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._getImageLabel = function(image) {
+    if (image) {
+      var type = image.type || image._expectedType;
+      if (image._width && image._height) {
+        return image._width + '⨉' + image._height + ' ' + type;
+      } else {
+        return type;
+      }
+    }
+    return 'No image';
+  };
+
+  window.GrDiffBuilderImage = GrDiffBuilderImage;
+})(window, GrDiffBuilderSideBySide);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index c2197eb..f55037c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -15,6 +15,7 @@
   'use strict';
 
   function GrDiffBuilder(diff, comments, prefs, outputEl) {
+    this._diff = diff;
     this._comments = comments;
     this._prefs = prefs;
     this._outputEl = outputEl;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index e4603b8..826a21e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -43,6 +43,17 @@
       .section {
         background-color: #eee;
       }
+      .image-diff .gr-diff {
+        text-align: center;
+      }
+      .image-diff img {
+        max-width: 50em;
+        outline: 1px solid #ccc;
+      }
+      .image-diff label {
+        font-family: var(--font-family);
+        font-style: italic;
+      }
       .diff-row.target-row.target-side-left .lineNum.left,
       .diff-row.target-row.target-side-right .lineNum.right,
       .diff-row.target-row.unified .lineNum {
@@ -151,5 +162,6 @@
   <script src="gr-diff-builder.js"></script>
   <script src="gr-diff-builder-side-by-side.js"></script>
   <script src="gr-diff-builder-unified.js"></script>
+  <script src="gr-diff-builder-image.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 660b86f..4cbe10f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -42,6 +42,13 @@
         type: Object,
         observer: '_projectConfigChanged',
       },
+      project: String,
+      commit: String,
+      isImageDiff: {
+        type: Boolean,
+        computed: '_computeIsImageDiff(_diff)',
+        notify: true,
+      },
 
       _loggedIn: {
         type: Boolean,
@@ -82,6 +89,7 @@
 
       promises.push(this._getDiff().then(function(diff) {
         this._diff = diff;
+        return this._loadDiffAssets();
       }.bind(this)));
 
       promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
@@ -414,8 +422,40 @@
       return this.$.restAPI.getLoggedIn();
     },
 
+    _computeIsImageDiff: function() {
+      if (!this._diff) { return false; }
+
+      var isA = this._diff.meta_a &&
+          this._diff.meta_a.content_type.indexOf('image/') === 0;
+      var isB = this._diff.meta_b &&
+          this._diff.meta_b.content_type.indexOf('image/') === 0;
+
+      return this._diff.binary && (isA || isB);
+    },
+
+    _loadDiffAssets: function() {
+      if (this.isImageDiff) {
+        return this._getImages().then(function(images) {
+          this._baseImage = images.baseImage;
+          this._revisionImage = images.revisionImage;
+        }.bind(this));
+      } else {
+        this._baseImage = null;
+        this._revisionImage = null;
+        return Promise.resolve();
+      }
+    },
+
+    _getImages: function() {
+      return this.$.restAPI.getImagesForDiff(this.project, this.commit,
+          this.changeNum, this._diff, this.patchRange);
+    },
+
     _getDiffBuilder: function(diff, comments, prefs) {
-      if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      if (this.isImageDiff) {
+        return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable,
+            this._baseImage, this._revisionImage);
+      } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
         return new GrDiffBuilderSideBySide(diff, comments, prefs,
             this.$.diffTable);
       } else if (this.viewMode === DiffViewMode.UNIFIED) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f02b861..cb091c9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -20,6 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-diff.html">
@@ -242,5 +243,105 @@
         ],
       }));
     });
+
+    test('renders image diffs', function(done) {
+      var mockDiff = {
+        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+        meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+        intraline_status: 'OK',
+        change_type: 'MODIFIED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot.jpg',
+          'index 2adc47d..f9c2f2c 100644',
+          '--- a/carrot.jpg',
+          '+++ b/carrot.jpg',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      var mockFile1 = {
+        body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsAAA' +
+            'AAAAAAAAAAAAAA/w==',
+        type: 'image/bmp',
+      };
+      var mockFile2 = {
+        body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsAAA' +
+            'AAAAAAAAAA/////w==',
+        type: 'image/bmp'
+      };
+      var mockCommit = {
+        commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
+        parents: [{
+          commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
+          subject: 'Added a carrot',
+        }],
+        author: {
+          name: 'Wyatt Allen',
+          email: 'wyatta@google.com',
+          date: '2016-05-23 21:44:51.000000000',
+          tz: -420,
+        },
+        committer: {
+          name: 'Wyatt Allen',
+          email: 'wyatta@google.com',
+          date: '2016-05-25 00:25:41.000000000',
+          tz: -420,
+        },
+        subject: 'Updated the carrot',
+        message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
+      };
+      var mockComments = {baseComments: [], comments: []};
+
+      var stubs = [];
+      stubs.push(sinon.stub(element, '_getDiff',
+          function() { return Promise.resolve(mockDiff); }));
+      stubs.push(sinon.stub(element.$.restAPI, 'getCommitInfo',
+          function() { return Promise.resolve(mockCommit); }));
+      stubs.push(sinon.stub(element.$.restAPI,
+          'getCommitFileContents',
+          function() { return Promise.resolve(mockFile1); }));
+      stubs.push(sinon.stub(element.$.restAPI,
+          'getChangeFileContents',
+          function() { return Promise.resolve(mockFile2); }));
+      stubs.push(sinon.stub(element.$.restAPI, '_getDiffComments',
+          function() { return Promise.resolve(mockComments); }));
+      stubs.push(sinon.stub(element.$.restAPI, 'getDiffDrafts',
+          function() { return Promise.resolve(mockComments); }));
+
+      element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+
+      var rendered = function() {
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(element._getDiffBuilder(element._diff,
+            element._comments, element.prefs), GrDiffBuilderImage);
+
+        // The left image rendered with the parent commit's version of the file.
+        var leftInmage = element.$.diffTable.querySelector('td.left img');
+        assert.isOk(leftInmage);
+        assert.equal(leftInmage.getAttribute('src'),
+            'data:image/bmp;base64, ' + mockFile1.body);
+
+        // The right image rendered with this change's revision of the image.
+        var rightInmage = element.$.diffTable.querySelector('td.right img');
+        assert.isOk(rightInmage);
+        assert.equal(rightInmage.getAttribute('src'),
+            'data:image/bmp;base64, ' + mockFile2.body);
+
+        // Cleanup.
+        element.removeEventListener('render', rendered);
+        stubs.forEach(function(stub) { stub.restore(); });
+
+        done();
+      };
+
+      element.addEventListener('render', rendered);
+
+      element.$.restAPI.getDiffPreferences().then(function(prefs) {
+        element.prefs = prefs;
+        element.reload();
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 10c8a29..72ae4e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -18,7 +18,6 @@
 <script src="../../../bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
-  <template></template>
   <script src="gr-rest-api-interface.js"></script>
 </dom-module>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index e681bf9..9604ded 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -682,5 +682,84 @@
       return '';
     },
 
+    getCommitInfo: function(project, commit) {
+      return this.fetchJSON(
+          '/projects/' + encodeURIComponent(project) +
+          '/commits/' + encodeURIComponent(commit));
+    },
+
+    _fetchB64File: function(url) {
+      return fetch(url).then(function(response) {
+        var type = response.headers.get('X-FYI-Content-Type');
+        return response.text()
+          .then(function(text) {
+            return {body: text, type: type};
+          });
+      });
+    },
+
+    getChangeFileContents: function(changeId, patchNum, path) {
+      return this._fetchB64File(
+          '/changes/' + encodeURIComponent(changeId) +
+          '/revisions/' + encodeURIComponent(patchNum) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getCommitFileContents: function(projectName, commit, path) {
+      return this._fetchB64File(
+          '/projects/' + encodeURIComponent(projectName) +
+          '/commits/' + encodeURIComponent(commit) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getImagesForDiff: function(project, commit, changeNum, diff, patchRange) {
+      var promiseA;
+      var promiseB;
+
+      if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) {
+        if (patchRange.basePatchNum === 'PARENT') {
+          // Need the commit info know the parent SHA.
+          promiseA = this.getCommitInfo(project, commit).then(function(info) {
+            if (info.parents.length !== 1) {
+              return Promise.reject('Change commit has multiple parents.');
+            }
+            var parent = info.parents[0].commit;
+            return this.getCommitFileContents(project, parent,
+                diff.meta_a.name);
+          }.bind(this));
+
+        } else {
+          promiseA = this.getChangeFileContents(changeNum,
+              patchRange.basePatchNum, diff.meta_a.name);
+        }
+      } else {
+        promiseA = Promise.resolve(null);
+      }
+
+      if (diff.meta_b && diff.meta_b.content_type.indexOf('image/') === 0) {
+        promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum,
+            diff.meta_b.name);
+      } else {
+        promiseB = Promise.resolve(null);
+      }
+
+      return Promise.all([promiseA, promiseB])
+        .then(function(results) {
+          var baseImage = results[0];
+          var revisionImage = results[1];
+
+          // Sometimes the server doesn't send back the content type.
+          if (baseImage) {
+            baseImage._expectedType = diff.meta_a.content_type;
+          }
+          if (revisionImage) {
+            revisionImage._expectedType = diff.meta_b.content_type;
+          }
+
+          return {baseImage: baseImage, revisionImage: revisionImage};
+        }.bind(this));
+    },
   });
 })();