diff --git a/reviewboard/attachments/admin.py b/reviewboard/attachments/admin.py
index 9b1ba3be827ec07d3361c73611aeec5e81d5facd..56ca552e9c0b10c0a553fd7697ee4652d76d7818 100644
--- a/reviewboard/attachments/admin.py
+++ b/reviewboard/attachments/admin.py
@@ -9,6 +9,7 @@ class FileAttachmentAdmin(admin.ModelAdmin):
                     'review_request_id')
     list_display_links = ('file', 'caption')
     search_fields = ('caption', 'mimetype')
+    raw_id_fields = ('added_in_filediff',)
 
     def review_request_id(self, obj):
         return obj.review_request.get().id
diff --git a/reviewboard/attachments/evolutions/__init__.py b/reviewboard/attachments/evolutions/__init__.py
index 9f21e3a1cd7a0d04ed0dc1bc86d9b4aa60ead8f2..39b54524a5ef6f121d568259c2d715e53b092b6c 100644
--- a/reviewboard/attachments/evolutions/__init__.py
+++ b/reviewboard/attachments/evolutions/__init__.py
@@ -1,4 +1,5 @@
 SEQUENCE = [
     'file_attachment_orig_filename',
     'file_attachment_file_max_length_512',
+    'file_attachment_repo_info',
 ]
diff --git a/reviewboard/attachments/evolutions/file_attachment_repo_info.py b/reviewboard/attachments/evolutions/file_attachment_repo_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..a68d4418f4cabcab8b2b0c180234fa82eba5c2af
--- /dev/null
+++ b/reviewboard/attachments/evolutions/file_attachment_repo_info.py
@@ -0,0 +1,14 @@
+from django_evolution.mutations import AddField
+from django.db import models
+
+
+MUTATIONS = [
+    AddField('FileAttachment', 'repository', models.ForeignKey, null=True,
+             related_model='scmtools.Repository'),
+    AddField('FileAttachment', 'repo_revision', models.CharField,
+             max_length=512, null=True, db_index=True),
+    AddField('FileAttachment', 'repo_path', models.CharField,
+             max_length=1024, null=True, db_index=True),
+    AddField('FileAttachment', 'added_in_filediff', models.ForeignKey,
+             null=True, related_model='diffviewer.FileDiff'),
+]
diff --git a/reviewboard/attachments/managers.py b/reviewboard/attachments/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7e4f812b3b27f1648910ee8c0d47f0ce7ff782
--- /dev/null
+++ b/reviewboard/attachments/managers.py
@@ -0,0 +1,58 @@
+from django.db.models import Manager
+
+
+class FileAttachmentManager(Manager):
+    """Manages FileAttachment objects.
+
+    Adds utility functions for looking up FileAttachments based on other
+    objects.
+    """
+    def create_from_filediff(self, filediff, from_modified=True, **kwargs):
+        """Creates a new FileAttachment for a FileDiff.
+
+        FileAttachments created from a FileDiff are used to represent changes
+        to binary files which would otherwise not be displayed with the diff.
+
+        An individual FileAttachment can represent either the original or
+        modified copy of the file. If 'from_modified' is True, then the
+        FileAttachment will be created using the information (filename,
+        revision, etc.) for the modified version. If it is False, the
+        FileAttachment will be created using the information for the original
+        version.
+        """
+        if filediff.is_new:
+            assert from_modified
+
+            return self.create(added_in_filediff=filediff, **kwargs)
+        elif from_modified:
+            return self.create(repo_path=filediff.dest_file,
+                               repo_revision=filediff.dest_detail,
+                               repository=filediff.diffset.repository,
+                               **kwargs)
+        else:
+            return self.create(repo_path=filediff.source_file,
+                               repo_revision=filediff.source_revision,
+                               repository=filediff.diffset.repository,
+                               **kwargs)
+
+    def get_for_filediff(self, filediff, modified=True):
+        """Returns the FileAttachment matching a DiffSet.
+
+        The FileAttachment associated with the path, revision and repository
+        matching the DiffSet will be returned, if it exists.
+
+        It is up to the caller to check for errors.
+        """
+        if filediff.is_new:
+            if modified:
+                return self.get(added_in_filediff=filediff)
+            else:
+                return None
+        elif modified:
+            return self.get(repo_path=filediff.dest_file,
+                            repo_revision=filediff.dest_detail,
+                            repository=filediff.diffset.repository)
+        else:
+            return self.get(repo_path=filediff.source_file,
+                            repo_revision=filediff.source_revision,
+                            repository=filediff.diffset.repository)
diff --git a/reviewboard/attachments/models.py b/reviewboard/attachments/models.py
index db0cdff5da8c8a7e6efdbac744ee0deb89d913dc..09479396aec932ae64c2169af7281fba2ed002b5 100644
--- a/reviewboard/attachments/models.py
+++ b/reviewboard/attachments/models.py
@@ -4,7 +4,10 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
+from reviewboard.attachments.managers import FileAttachmentManager
 from reviewboard.attachments.mimetypes import MimetypeHandler
+from reviewboard.diffviewer.models import FileDiff
+from reviewboard.scmtools.models import Repository
 
 
 class FileAttachment(models.Model):
@@ -24,9 +27,38 @@ class FileAttachment(models.Model):
                                                    '%Y', '%m', '%d'))
     mimetype = models.CharField(_('mimetype'), max_length=256, blank=True)
 
+    # repo_path, repo_revision, and repository are used to identify
+    # FileAttachments associated with committed binary files in a source tree.
+    # They are not used for new files that don't yet have a revision.
+    #
+    # For new files, the added_in_filediff association is used.
+    repo_path = models.CharField(_('repository file path'),
+                                 max_length=1024,
+                                 blank=True,
+                                 null=True,
+                                 db_index=True)
+    repo_revision = models.CharField(_('repository file revision'),
+                                     max_length=512,
+                                     blank=True,
+                                     null=True,
+                                     db_index=True)
+    repository = models.ForeignKey(Repository,
+                                   blank=True,
+                                   null=True,
+                                   related_name='file_attachments')
+    added_in_filediff = models.ForeignKey(FileDiff,
+                                          blank=True,
+                                          null=True,
+                                          related_name='added_attachments')
+
+    objects = FileAttachmentManager()
+
     @property
     def mimetype_handler(self):
-        return MimetypeHandler.for_type(self)
+        if not hasattr(self, '_thumbnail'):
+            self._thumbnail = MimetypeHandler.for_type(self)
+
+        return self._thumbnail
 
     @property
     def review_ui(self):
@@ -67,6 +99,12 @@ class FileAttachment(models.Model):
         """Returns the icon URL for this file."""
         return self.mimetype_handler.get_icon_url()
 
+    @property
+    def is_from_diff(self):
+        """Returns if this file attachment is associated with a diff."""
+        return (self.repository_id is not None or
+                self.added_in_filediff_id is not None)
+
     def __unicode__(self):
         return self.caption
 
diff --git a/reviewboard/attachments/templatetags/attachments.py b/reviewboard/attachments/templatetags/attachments.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a8eb2f868c3e177efe4f7b4db52fadeafea4cd6
--- /dev/null
+++ b/reviewboard/attachments/templatetags/attachments.py
@@ -0,0 +1,41 @@
+import logging
+
+from django import template
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
+
+from reviewboard.attachments.models import FileAttachment
+
+
+register = template.Library()
+
+
+@register.assignment_tag(takes_context=True)
+def get_diff_file_attachment(context, filediff, use_modified=True):
+    """Fetch the FileAttachment associated with a FileDiff.
+
+    This will query for the FileAttachment based on the provided filediff,
+    and set the retrieved diff file attachment to a variable whose name is
+    provided as an argument to this tag.
+
+    If 'use_modified' is True, the FileAttachment returned will be from the
+    modified version of the new file. Otherwise, it's the original file that's
+    being modified.
+
+    If no matching FileAttachment is found or if there is more than one
+    FileAttachment associated with one FileDiff, None is returned. An error
+    is logged in the latter case.
+    """
+    if not filediff:
+        return None
+
+    try:
+        return FileAttachment.objects.get_for_filediff(filediff, use_modified)
+    except ObjectDoesNotExist:
+        return None
+    except MultipleObjectsReturned:
+        # Only one FileAttachment should be associated with a FileDiff
+        logging.error('More than one FileAttachments associated with '
+                      'FileDiff %s',
+                      filediff.pk,
+                      exc_info=1)
+        return None
diff --git a/reviewboard/attachments/tests.py b/reviewboard/attachments/tests.py
index af34921d3636c4d6f733abe7dc8e56b18a064937..1e13a76d6b6e092ae25ee6cf98d280d331da9967 100644
--- a/reviewboard/attachments/tests.py
+++ b/reviewboard/attachments/tests.py
@@ -2,32 +2,72 @@ import mimeparse
 import os
 
 from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.cache import cache
 from django.core.files.uploadedfile import SimpleUploadedFile
-from django.test import TestCase
+from djblets.testing.decorators import add_fixtures
+from djblets.testing.testcases import TestCase
 
 from reviewboard.attachments.forms import UploadFileForm
 from reviewboard.attachments.mimetypes import (MimetypeHandler,
                                                register_mimetype_handler,
                                                unregister_mimetype_handler)
+from reviewboard.attachments.models import FileAttachment
+from reviewboard.diffviewer.models import DiffSet, DiffSetHistory, FileDiff
 from reviewboard.reviews.models import ReviewRequest
+from reviewboard.scmtools.core import PRE_CREATION
+from reviewboard.scmtools.models import Repository
 
 
-class FileAttachmentTests(TestCase):
-    fixtures = ['test_users', 'test_reviewrequests', 'test_scmtools']
-
-    def test_upload_file(self):
-        """Testing uploading a file attachment."""
+class BaseFileAttachmentTestCase(TestCase):
+    def make_uploaded_file(self):
         filename = os.path.join(settings.STATIC_ROOT,
                                 'rb', 'images', 'trophy.png')
         f = open(filename, 'r')
-        file = SimpleUploadedFile(f.name, f.read(), content_type='image/png')
+        uploaded_file = SimpleUploadedFile(f.name, f.read(),
+                                           content_type='image/png')
         f.close()
 
+        return uploaded_file
+
+    def make_filediff(self, is_new=False, diffset_history=None,
+                      diffset_revision=1, source_filename='file1',
+                      dest_filename='file2'):
+        if is_new:
+            source_revision = PRE_CREATION
+            dest_revision = ''
+        else:
+            source_revision = '1'
+            dest_revision = '2'
+
+        repository = Repository.objects.get(pk=1)
+
+        if not diffset_history:
+            diffset_history = DiffSetHistory.objects.create(name='testhistory')
+
+        diffset = DiffSet.objects.create(name='test',
+                                         revision=diffset_revision,
+                                         repository=repository,
+                                         history=diffset_history)
+        filediff = FileDiff(source_file=source_filename,
+                            source_revision=source_revision,
+                            dest_file=dest_filename,
+                            dest_detail=dest_revision,
+                            diffset=diffset,
+                            binary=True)
+        filediff.save()
+
+        return filediff
+
+
+class FileAttachmentTests(BaseFileAttachmentTestCase):
+    @add_fixtures(['test_users', 'test_reviewrequests', 'test_scmtools'])
+    def test_upload_file(self):
+        """Testing uploading a file attachment"""
+        file = self.make_uploaded_file()
         form = UploadFileForm(files={
             'path': file,
         })
-        form.is_valid()
-        print form.errors
         self.assertTrue(form.is_valid())
 
         review_request = ReviewRequest.objects.get(pk=1)
@@ -36,6 +76,28 @@ class FileAttachmentTests(TestCase):
             '__trophy.png'))
         self.assertEqual(file_attachment.mimetype, 'image/png')
 
+    def test_is_from_diff_with_no_association(self):
+        """Testing FileAttachment.is_from_diff with standard attachment"""
+        file_attachment = FileAttachment()
+
+        self.assertFalse(file_attachment.is_from_diff)
+
+    @add_fixtures(['test_scmtools'])
+    def test_is_from_diff_with_repository(self):
+        """Testing FileAttachment.is_from_diff with repository association"""
+        repository = Repository.objects.get(pk=1)
+        file_attachment = FileAttachment(repository=repository)
+
+        self.assertTrue(file_attachment.is_from_diff)
+
+    @add_fixtures(['test_scmtools'])
+    def test_is_from_diff_with_filediff(self):
+        """Testing FileAttachment.is_from_diff with filediff association"""
+        filediff = self.make_filediff()
+        file_attachment = FileAttachment(added_in_filediff=filediff)
+
+        self.assertTrue(file_attachment.is_from_diff)
+
 
 class MimetypeTest(MimetypeHandler):
     supported_mimetypes = ['test/*']
@@ -106,7 +168,7 @@ class MimetypeHandlerTests(TestCase):
         return handler
 
     def test_handler_factory(self):
-        """Testing matching of factory method for mimetype handlers."""
+        """Testing matching of factory method for mimetype handlers"""
         # Exact Match
         self.assertEqual(self._handler_for("test/abc"), TestAbcMimetype)
         self.assertEqual(self._handler_for("test2/abc+xml"),
@@ -119,7 +181,7 @@ class MimetypeHandlerTests(TestCase):
         self.assertEqual(self._handler_for("foo/bar"), StarDefMimetype)
 
     def test_handler_factory_precedence(self):
-        """Testing precedence of factory method for mimetype handlers."""
+        """Testing precedence of factory method for mimetype handlers"""
         self.assertEqual(self._handler_for("test2/def"), StarDefMimetype)
         self.assertEqual(self._handler_for("test3/abc+xml"),
                          Test3AbcXmlMimetype)
@@ -128,3 +190,212 @@ class MimetypeHandlerTests(TestCase):
         self.assertEqual(self._handler_for("foo/def"), StarDefMimetype)
         # Left match and Wildcard should trump Left Wildcard and match
         self.assertEqual(self._handler_for("test/def"), MimetypeTest)
+
+
+class FileAttachmentManagerTests(BaseFileAttachmentTestCase):
+    """Tests for FileAttachmentManager"""
+    fixtures = ['test_scmtools']
+
+    def test_create_from_filediff_with_new_and_modified_true(self):
+        """Testing FileAttachmentManager.create_from_filediff with new FileDiff and modified=True"""
+        filediff = self.make_filediff(is_new=True)
+        self.assertTrue(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+        self.assertEqual(file_attachment.repository_id, None)
+        self.assertEqual(file_attachment.repo_path, None)
+        self.assertEqual(file_attachment.repo_revision, None)
+        self.assertEqual(file_attachment.added_in_filediff, filediff)
+
+    def test_create_from_filediff_with_new_and_modified_false(self):
+        """Testing FileAttachmentManager.create_from_filediff with new FileDiff and modified=False"""
+        filediff = self.make_filediff(is_new=True)
+        self.assertTrue(filediff.is_new)
+
+        self.assertRaises(
+            AssertionError,
+            FileAttachment.objects.create_from_filediff,
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png',
+            from_modified=False)
+
+    def test_create_from_filediff_with_existing_and_modified_true(self):
+        """Testing FileAttachmentManager.create_from_filediff with existing FileDiff and modified=True"""
+        filediff = self.make_filediff()
+        self.assertFalse(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+        self.assertEqual(file_attachment.repository,
+                         filediff.diffset.repository)
+        self.assertEqual(file_attachment.repo_path, filediff.dest_file)
+        self.assertEqual(file_attachment.repo_revision, filediff.dest_detail)
+        self.assertEqual(file_attachment.added_in_filediff_id, None)
+
+    def test_create_from_filediff_with_existing_and_modified_false(self):
+        """Testing FileAttachmentManager.create_from_filediff with existing FileDiff and modified=False"""
+        filediff = self.make_filediff()
+        self.assertFalse(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png',
+            from_modified=False)
+        self.assertEqual(file_attachment.repository,
+                         filediff.diffset.repository)
+        self.assertEqual(file_attachment.repo_path, filediff.source_file)
+        self.assertEqual(file_attachment.repo_revision,
+                         filediff.source_revision)
+        self.assertEqual(file_attachment.added_in_filediff_id, None)
+
+    def test_get_for_filediff_with_new_and_modified_true(self):
+        """Testing FileAttachmentManager.get_for_filediff with new FileDiff and modified=True"""
+        filediff = self.make_filediff(is_new=True)
+        self.assertTrue(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+
+        self.assertEqual(
+            FileAttachment.objects.get_for_filediff(filediff, modified=True),
+            file_attachment)
+
+    def test_get_for_filediff_with_new_and_modified_false(self):
+        """Testing FileAttachmentManager.get_for_filediff with new FileDiff and modified=False"""
+        filediff = self.make_filediff(is_new=True)
+        self.assertTrue(filediff.is_new)
+
+        FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+
+        self.assertEqual(
+            FileAttachment.objects.get_for_filediff(filediff, modified=False),
+            None)
+
+    def test_get_for_filediff_with_existing_and_modified_true(self):
+        """Testing FileAttachmentManager.get_for_filediff with existing FileDiff and modified=True"""
+        filediff = self.make_filediff()
+        self.assertFalse(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+
+        self.assertEqual(
+            FileAttachment.objects.get_for_filediff(filediff, modified=True),
+            file_attachment)
+
+    def test_get_for_filediff_with_existing_and_modified_false(self):
+        """Testing FileAttachmentManager.get_for_filediff with existing FileDiff and modified=False"""
+        filediff = self.make_filediff()
+        self.assertFalse(filediff.is_new)
+
+        file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            file=self.make_uploaded_file(),
+            mimetype='image/png',
+            from_modified=False)
+
+        self.assertEqual(
+            FileAttachment.objects.get_for_filediff(filediff, modified=False),
+            file_attachment)
+
+
+class DiffViewerFileAttachmentTests(BaseFileAttachmentTestCase):
+    """Tests for inline diff file attachments in the diff viewer."""
+    fixtures = ['test_users', 'test_reviewrequests', 'test_scmtools',
+                'test_site']
+
+    def setUp(self):
+        # The diff viewer's caching breaks the result of these tests,
+        # so be sure we clear before each one.
+        cache.clear()
+
+    def test_added_file(self):
+        """Testing inline diff file attachments with newly added files"""
+        # Set up the initial state.
+        user = User.objects.get(username='doc')
+        review_request = ReviewRequest.objects.create(user, None)
+        filediff = self.make_filediff(
+            is_new=True,
+            diffset_history=review_request.diffset_history)
+
+        # Create a diff file attachment to be displayed inline.
+        diff_file_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            filename='my-file',
+            file=self.make_uploaded_file(),
+            mimetype='image/png')
+        review_request.file_attachments.add(diff_file_attachment)
+        review_request.publish(user)
+
+        # Load the diff viewer.
+        self.client.login(username='doc', password='doc')
+        response = self.client.get('/r/%d/diff/' % review_request.pk)
+        self.assertEqual(response.status_code, 200)
+
+        # This file attachment should not appear in the list of standard
+        # file attachments.
+        self.assertEqual(len(response.context['file_attachments']), 0)
+
+        # The file attachment should appear as the right-hand side
+        # file attachment in the diff viewer.
+        self.assertFalse('orig_diff_file_attachment' in response.context)
+        self.assertEqual(response.context['modified_diff_file_attachment'],
+                         diff_file_attachment)
+
+    def test_modified_file(self):
+        """Testing inline diff file attachments with modified files"""
+        # Set up the initial state.
+        user = User.objects.get(username='doc')
+        review_request = ReviewRequest.objects.create(user, None)
+        filediff = self.make_filediff(
+            is_new=False,
+            diffset_history=review_request.diffset_history)
+        self.assertFalse(filediff.is_new)
+
+        # Create diff file attachments to be displayed inline.
+        uploaded_file = self.make_uploaded_file()
+
+        orig_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            filename='my-file',
+            file=uploaded_file,
+            mimetype='image/png',
+            from_modified=False)
+        modified_attachment = FileAttachment.objects.create_from_filediff(
+            filediff,
+            filename='my-file',
+            file=uploaded_file,
+            mimetype='image/png')
+        review_request.file_attachments.add(orig_attachment)
+        review_request.file_attachments.add(modified_attachment)
+        review_request.publish(user)
+
+        # Load the diff viewer.
+        self.client.login(username='doc', password='doc')
+        response = self.client.get('/r/%d/diff/' % review_request.pk)
+        self.assertEqual(response.status_code, 200)
+
+        # This file attachment should not appear in the list of standard
+        # file attachments.
+        self.assertEqual(len(response.context['file_attachments']), 0)
+
+        # The file attachment should appear as the right-hand side
+        # file attachment in the diff viewer.
+        self.assertEqual(response.context['orig_diff_file_attachment'],
+                         orig_attachment)
+        self.assertEqual(response.context['modified_diff_file_attachment'],
+                         modified_attachment)
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index f97c0e8df78aa033803bad6e03c131e275b97558..d0cd2ba60ad07a2f8f57df6afbc17b9a9d58410f 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -710,7 +710,10 @@ def review_detail(request,
         'PRE_CREATION': PRE_CREATION,
         'issues': issues,
         'has_diffs': (draft and draft.diffset) or len(diffsets) > 0,
-        'file_attachments': file_attachments,
+        'file_attachments': [file_attachment
+                             for file_attachment in file_attachments
+                             if not file_attachment.is_from_diff],
+        'all_file_attachments': file_attachments,
         'screenshots': screenshots,
     })
 
@@ -1037,7 +1040,10 @@ class ReviewsDiffViewerView(DiffViewerView):
             'is_draft_interdiff': is_draft_interdiff,
             'num_diffs': num_diffs,
             'last_activity_time': last_activity_time,
-            'file_attachments': file_attachments,
+            'file_attachments': [file_attachment
+                                 for file_attachment in file_attachments
+                                 if not file_attachment.is_from_diff],
+            'all_file_attachments': file_attachments,
             'screenshots': screenshots,
             'comments': comments,
         })
diff --git a/reviewboard/scmtools/git.py b/reviewboard/scmtools/git.py
index 2db48719b5c6b96f308b95bc41584235a8059965..da8e2bc820046f5afa0dfbc83f57dece4f13be54 100644
--- a/reviewboard/scmtools/git.py
+++ b/reviewboard/scmtools/git.py
@@ -231,12 +231,6 @@ class GitDiffParser(DiffParser):
             linenum += 3
             file_info.moved = True
 
-        # Only show interesting empty changes. Basically, deletions.
-        # It's likely a binary file if we're at this point, and so we want
-        # to process the rest of it.
-        if empty_change and not file_info.deleted:
-            return empty_change_linenum, None
-
         if self._is_index_range_line(linenum):
             index_range = self.lines[linenum].split(None, 2)[1]
 
diff --git a/reviewboard/static/rb/css/diffviewer.less b/reviewboard/static/rb/css/diffviewer.less
index dbd0f35fc71d2f0a5ca7f2c1bf54181963d37b1d..3998a44bdf209026799868a10d6be5b487c91ba1 100644
--- a/reviewboard/static/rb/css/diffviewer.less
+++ b/reviewboard/static/rb/css/diffviewer.less
@@ -2,6 +2,7 @@
 
 @diff-file-color: #F0F0F0;
 @diff-border-color: #A0A0A0;
+@diff-file-border-color: darken(@diff-file-color, 15%);
 @diff-line-border-color: #D0D0D0;
 
 // Diff headers
@@ -53,6 +54,9 @@
 @paginate-bg-color: #417690;
 @paginate-text-color: #f4f379;
 
+// Binary Files
+@inline-actions-bg: darken(@diff-file-color, 3%);
+@inline-actions-hover-bg: darken(@inline-actions-bg, 5%);
 
 /*
  * The .diff-changes-* rules are used only within JavaScript code to
@@ -151,6 +155,10 @@
     }
   }
 
+  img.header-file-icon {
+    vertical-align: middle;
+  }
+
   pre {
     .pre-wrap;
     font-size: 8pt;
@@ -247,11 +255,6 @@
       display: none;
     }
 
-    &.binary td {
-      background: #dbebff;
-      padding: 1em;
-    }
-
     &.whitespace-file td {
       background: @diff-replace-color;
       padding: 1em;
@@ -266,6 +269,71 @@
       padding: 1em;
     }
 
+    &.binary {
+      .inline-actions-header {
+        background: @inline-actions-bg;
+        border-bottom: 1px @diff-file-border-color solid;
+
+        td {
+          padding: 0;
+
+          &:first-child {
+            border-right: 1px @diff-line-border-color solid;
+          }
+        }
+      }
+
+      .inline-actions-right, .inline-actions-left {
+        li {
+          .border-radius(0 0 0 0);
+
+          &:hover {
+            background-color: @inline-actions-hover-bg;
+          }
+        }
+      }
+
+      .inline-actions-right {
+        float: right;
+
+        a {
+          border-left: 1px @diff-file-border-color solid;
+        }
+      }
+
+      .inline-actions-left {
+        float: left;
+
+        a {
+          border-right: 1px @diff-file-border-color solid;
+          border-left: 0px;
+        }
+      }
+
+      .inline-files-container {
+        td:first-child {
+          border-right: 1px @diff-line-border-color solid;
+        }
+      }
+
+      .file-thumbnail-container {
+        margin: 0;
+        overflow: hidden;
+        padding: 1em;
+        text-align: center;
+        white-space: nowrap;
+      }
+
+      p {
+        margin: 0;
+        padding: 4px;
+      }
+
+      td {
+        padding: 1em;
+      }
+    }
+
     &.delete {
       tr {
         &.selected * { background: @diff-delete-selected-color; }
@@ -376,10 +444,10 @@
     }
 
     .revision-row th {
-      border-bottom: 1px darken(@diff-file-color, 15%) solid;
+      border-bottom: 1px @diff-file-border-color solid;
       font-size: 100%;
       font-weight: normal;
-      padding: 8px 4px;
+      padding: 8px 0;
     }
   }
 
@@ -517,6 +585,7 @@
         background-image: url("../images/spinner.gif");
         background-position: center center;
         background-repeat: no-repeat;
+        min-width: 20px;
         width: 20px;
         height: 24px;
       }
diff --git a/reviewboard/static/rb/css/reviews.less b/reviewboard/static/rb/css/reviews.less
index 2c96da1f3711fb6f22bb7da688ea0a0d53bec071..685fbf91fd188f090f7c27f0a89e006a5ade5608 100644
--- a/reviewboard/static/rb/css/reviews.less
+++ b/reviewboard/static/rb/css/reviews.less
@@ -2,6 +2,113 @@
 
 
 /****************************************************************************
+ * Actions
+ ****************************************************************************/
+.actions-container {
+  background: @review-request-actions-bg;
+  border-bottom: 1px @review-request-actions-border-color solid;
+  float: right;
+  width: 100%;
+  .border-radius(@box-inner-border-radius @box-inner-border-radius 0 0);
+
+  &>ul {
+    min-height: 2em;
+  }
+}
+
+
+.actions {
+  float: right;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  white-space: nowrap;
+
+  a {
+    color: black;
+    cursor: pointer;
+    display: block;
+    text-decoration: none;
+    padding: 6px 10px;
+
+    border-left: 1px @review-request-action-border-color solid;
+  }
+
+  img {
+    vertical-align: middle;
+  }
+
+  li {
+    float: left;
+    margin: 0;
+    padding: 0;
+
+    &:hover {
+      background-color: @review-request-action-hover-bg;
+    }
+
+    &:active {
+      background-color: @review-request-action-active-bg;
+    }
+
+    &.primary {
+      background-color: @review-request-action-primary-bg;
+    }
+
+    &.primary:hover {
+      background-color: @review-request-action-primary-hover-bg;
+    }
+
+    &.primary:active {
+      background-color: @review-request-action-primary-active-bg;
+    }
+  }
+
+  .menu {
+    background: @review-request-action-menu-bg;
+    border: 1px @review-request-action-menu-border-color solid;
+    float: none;
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    position: absolute;
+    z-index: @z-index-menu;
+    .border-radius(0 0 @box-border-radius @box-border-radius);
+    .box-shadow(@box-shadow);
+
+    li {
+      background: @review-request-action-bg;
+      border: 0;
+      float: none;
+      margin: 0;
+      padding: 0;
+
+      &:last-child {
+        .border-radius(0 0 @box-border-radius @box-border-radius);
+      }
+
+      &:hover {
+        background-color: @review-request-action-menu-item-hover-bg;
+      }
+
+      a {
+        color: black;
+        border: 0;
+        display: block;
+        margin: 0;
+        padding: 8px 10px;
+        text-decoration: none;
+      }
+    }
+  }
+}
+
+.actions>li:last-child {
+  .border-radius(0 @box-inner-border-radius 0 0);
+}
+
+
+/****************************************************************************
  * Review Request Box
  ****************************************************************************/
 .inline-editor-form {
@@ -215,109 +322,9 @@
   }
 
   .actions-container {
-    background: @review-request-actions-bg;
-    border-bottom: 1px @review-request-actions-border-color solid;
-    float: right;
-    width: 100%;
-    .border-radius(@box-inner-border-radius @box-inner-border-radius 0 0);
-
     .star {
       margin: 5px 0 0 5px;
     }
-
-    &>ul {
-      min-height: 2em;
-    }
-  }
-
-  .actions {
-    float: right;
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    white-space: nowrap;
-
-    a {
-      color: black;
-      cursor: pointer;
-      display: block;
-      text-decoration: none;
-      padding: 6px 10px;
-
-      border-left: 1px @review-request-action-border-color solid;
-    }
-
-    img {
-      vertical-align: middle;
-    }
-
-    li {
-      float: left;
-      margin: 0;
-      padding: 0;
-
-      &:hover {
-        background-color: @review-request-action-hover-bg;
-      }
-
-      &:active {
-        background-color: @review-request-action-active-bg;
-      }
-
-      &.primary {
-        background-color: @review-request-action-primary-bg;
-      }
-
-      &.primary:hover {
-        background-color: @review-request-action-primary-hover-bg;
-      }
-
-      &.primary:active {
-        background-color: @review-request-action-primary-active-bg;
-      }
-    }
-
-    .menu {
-      background: @review-request-action-menu-bg;
-      border: 1px @review-request-action-menu-border-color solid;
-      float: none;
-      list-style: none;
-      margin: 0;
-      padding: 0;
-      position: absolute;
-      z-index: @z-index-menu;
-      .border-radius(0 0 @box-border-radius @box-border-radius);
-      .box-shadow(@box-shadow);
-
-      li {
-        background: @review-request-action-bg;
-        border: 0;
-        float: none;
-        margin: 0;
-        padding: 0;
-
-        &:last-child {
-          .border-radius(0 0 @box-border-radius @box-border-radius);
-        }
-
-        &:hover {
-          background-color: @review-request-action-menu-item-hover-bg;
-        }
-
-        a {
-          color: black;
-          border: 0;
-          display: block;
-          margin: 0;
-          padding: 8px 10px;
-          text-decoration: none;
-        }
-      }
-    }
-  }
-
-  .actions>li:last-child {
-    .border-radius(0 @box-inner-border-radius 0 0);
   }
 
   #updated_time, #created_time {
diff --git a/reviewboard/static/rb/js/diffviewer/views/diffReviewableView.js b/reviewboard/static/rb/js/diffviewer/views/diffReviewableView.js
index b60535a07ca33d468adba4b325ecf013c8b3e8f7..77dc9b5f3652072d2062aedb749378f7597611f2 100644
--- a/reviewboard/static/rb/js/diffviewer/views/diffReviewableView.js
+++ b/reviewboard/static/rb/js/diffviewer/views/diffReviewableView.js
@@ -460,6 +460,20 @@ RB.DiffReviewableView = RB.AbstractReviewableView.extend({
 
         this._selector.render();
 
+        _.each(this.$el.children('tbody.binary'), function(thumbnailEl) {
+            var $thumbnail = $(thumbnailEl),
+                id = $thumbnail.data('file-id'),
+                $caption = $thumbnail.find('.file-caption .edit'),
+                reviewRequest = this.model.get('reviewRequest'),
+                fileAttachment = reviewRequest.createFileAttachment({
+                    id: id
+                });
+
+            if (!$caption.hasClass('empty-caption')) {
+                fileAttachment.set('caption', $caption.text());
+            }
+        }, this);
+
         this._$window.on('scroll resize', this._updateCollapseButtonPos);
 
         return this;
diff --git a/reviewboard/static/rb/js/pages/views/diffViewerPageView.js b/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
index b94ca48dc6c5497eb620eaa1c002d79e775b1a5b..8372f955a680e03c9583c7e4de3ff7a0726546e3 100644
--- a/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
+++ b/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
@@ -94,6 +94,8 @@ var DiffFileIndexView = Backbone.View.extend({
             numInserts = 1;
         } else if (fileDeleted) {
             numDeletes = 1;
+        } else if ($item.hasClass('binary-file')) {
+            numReplaces = 1;
         } else {
             _.each($table.children('tbody'), function(chunk) {
                 var numRows = chunk.rows.length,
diff --git a/reviewboard/static/rb/js/views/reviewRequestEditorView.js b/reviewboard/static/rb/js/views/reviewRequestEditorView.js
index 7e246717f91f0a318e9ed3cb1795b51238d68db0..ce1e8f3e9d5b29b27c98796ed2f6aa8d44de2732 100644
--- a/reviewboard/static/rb/js/views/reviewRequestEditorView.js
+++ b/reviewboard/static/rb/js/views/reviewRequestEditorView.js
@@ -248,6 +248,9 @@ RB.ReviewRequestEditorView = Backbone.View.extend({
         _.each(this._$attachments.find('.file-container'),
                this._importFileAttachmentThumbnail,
                this);
+        _.each($('.binary'),
+               this._importFileAttachmentThumbnail,
+               this);
 
         /*
          * Set up editors for every registered field.
diff --git a/reviewboard/templates/diffviewer/diff_file_fragment.html b/reviewboard/templates/diffviewer/diff_file_fragment.html
index dbbbc4e6c60496436a572eb0fa3707f2f4db848d..454f2eceac2a233fbf78f2734b7b18c80731a962 100644
--- a/reviewboard/templates/diffviewer/diff_file_fragment.html
+++ b/reviewboard/templates/diffviewer/diff_file_fragment.html
@@ -1,8 +1,6 @@
-{% load i18n %}
-{% load difftags %}
-{% load djblets_deco %}
-{% load djblets_utils %}
-{% load static %}
+{% load attachments difftags i18n djblets_deco djblets_utils %}
+{% load reviewtags static %}
+
 {% if standalone and error %}
 {{error}}
 {% endif %}
@@ -45,6 +43,17 @@
 {% enddefinevar %}
 
 {% if file.changed_chunk_indexes or file.binary or file.deleted or file.moved %}
+{%  if file.binary %}
+{%   if file.force_interdiff %}
+{%    get_diff_file_attachment file.interfilediff as modified_diff_file_attachment %}
+{%    get_diff_file_attachment file.filediff as orig_diff_file_attachment %}
+{%   else %}
+{%    if not is_new_file %}
+{%     get_diff_file_attachment file.filediff False as orig_diff_file_attachment %}
+{%    endif %}
+{%    get_diff_file_attachment file.filediff as modified_diff_file_attachment %}
+{%   endif %}
+{%  endif %}
 {%  if not standalone %}
 <table id="file{{file.filediff.id}}" class="{% spaceless %}
   sidebyside
@@ -64,7 +73,17 @@
  <thead>
   <tr class="filename-row">
 {%   if file.dest_filename == file.depot_filename %}
-   <th colspan="4"><a name="{{file.index}}" class="file-anchor"></a>{{ file.depot_filename }}</th>
+   <th colspan="4">
+    <a name="{{file.index}}" class="file-anchor"></a>
+{%    if file.binary %}
+{%     if modified_diff_file_attachment %}
+    <img class="header-file-icon" src="{{modified_diff_file_attachment.icon_url}}" />
+{%     elif orig_diff_file_attachment %}
+    <img class="header-file-icon" src="{{orig_diff_file_attachment.icon_url}}" />
+{%     endif %}
+{%    endif %}
+    {{file.depot_filename}}
+  </th>
 {%   else %}
 {%    if not is_new_file %}
    <th colspan="2"><a name="{{file.index}}" class="file-anchor"></a>{{ file.depot_filename }}</th>
@@ -87,10 +106,59 @@
  </thead>
 {%  endif %}{# not standalone #}
 {%  if file.binary %}
- <tbody class="binary">
-  <tr>
-   <td colspan="{% if is_new_file %}2{% else %}4{% endif %}">
-    {% trans "This is a binary file. The content cannot be displayed." %}
+ <tbody class="binary" data-file-id="{{modified_diff_file_attachment.id}}">
+{%   if orig_diff_file_attachment or modified_diff_file_attachment %}
+  <tr class="inline-actions-header">
+{%    if file.moved and file.num_changes == 0 or file.newfile and not orig_diff_file_attachment %}
+   <td colspan="4">
+{%    else %}
+   <td colspan="2">
+    <div class="inline-actions-container clearfix">
+     <ul class="actions inline-actions-left">
+{%     if orig_diff_file_attachment %}
+      <li class="download"><a href="{{orig_diff_file_attachment.get_absolute_url}}">{% trans "Download" %}</a></li>
+{%     endif %}
+     </ul>
+    </div>
+   </td>
+   <td colspan="2">
+{%    endif %}
+    <div class="inline-actions-container clearfix">
+     <ul class="actions inline-actions-right">
+{%   if modified_diff_file_attachment %}
+      <li><a href="{{modified_diff_file_attachment.get_absolute_url}}">{% trans "Download" %}</a></li>
+{%    if modified_diff_file_attachment.review_ui %}
+      <li class="{% if file.review_ui.allow_inline %}file-review-inline %}{% else %}file-review{% endif %}"><a href="{% url file_attachment modified_diff_file_attachment.get_review_request.display_id modified_diff_file_attachment.pk %}">{% trans "Review" %}</a></li>
+{%    else %}
+      <li class="file-add-comment"><a href="#">{% trans "Add Comment" %}</a></li>
+{%    endif %}
+{%   endif %}
+     </ul>
+    </div>
+  </tr>
+{%   endif %}
+  <tr class="inline-files-container">
+{%   if file.moved and file.num_changes == 0 or file.newfile and not orig_diff_file_attachment %}
+   <td colspan="4">
+{%   else %}
+   <td colspan="2">
+{%    if not orig_diff_file_attachment %}
+{%     trans "This is a binary file. The content cannot be displayed." %}
+{%    elif orig_diff_file_attachment.thumbnail %}
+    <div class="file-thumbnail-container">{{orig_diff_file_attachment.thumbnail}}</div>
+{%    else %}
+{%     trans "No preview available." %}
+{%    endif %}
+   </td>
+   <td colspan="2">
+{%   endif %}
+{%   if not modified_diff_file_attachment %}
+{%    trans "This is a binary file. The content cannot be displayed." %}
+{%   elif modified_diff_file_attachment.thumbnail %}
+    <div class="file-thumbnail-container">{{modified_diff_file_attachment.thumbnail}}</div>
+{%   else %}
+{%    trans "No preview available." %}
+{%   endif %}
    </td>
   </tr>
  </tbody>
diff --git a/reviewboard/templates/reviews/reviewable_page_data.js b/reviewboard/templates/reviews/reviewable_page_data.js
index e7246285a8cc612e7a7bc01569c7077887c3e1a3..6738f3870e7fa20a1d612dd95955a831bb133c2d 100644
--- a/reviewboard/templates/reviews/reviewable_page_data.js
+++ b/reviewboard/templates/reviews/reviewable_page_data.js
@@ -33,8 +33,8 @@
         editorData: {
             editable: {% if review_request.status == 'P' %}true{% else %}false{% endif %},
             fileAttachmentComments: {
-{% if file_attachments %}
-{%  for file_attachment in file_attachments %}
+{% if all_file_attachments %}
+{%  for file_attachment in all_file_attachments %}
                 {{file_attachment.id}}: {% file_attachment_comments file_attachment %}{% if not forloop.last %},{% endif %}
 {%  endfor %}
 {% endif %}
