[diffoscope] 01/06: Add visual comparison for JPEG and ICO images.

Maria Glukhova siamezzze-guest at moszumanska.debian.org
Sat Apr 29 19:16:58 CEST 2017


This is an automated email from the git hooks/post-receive script.

siamezzze-guest pushed a commit to branch siamezzze/image-visual-comparison
in repository diffoscope.

commit ab33d348e0dce7b5d14bbc955c4d6cfbfb9e622b
Author: Maria Glukhova <siamezzze at gmail.com>
Date:   Sat Apr 1 22:07:54 2017 +0300

    Add visual comparison for JPEG and ICO images.
    
    Add VisualDifference class and .visuals field to Difference class.
    If these are present, HTML presenter will output them instead of
    text-based difference.
    Add pixel difference and flicker (animation) difference for JPEG and
    ICO files.
---
 diffoscope/comparators/image.py    | 66 ++++++++++++++++++++++++++++++++++++--
 diffoscope/difference.py           | 29 +++++++++++++++++
 diffoscope/external_tools.py       |  4 +++
 diffoscope/presenters/html/html.py | 23 ++++++++++++-
 4 files changed, 118 insertions(+), 4 deletions(-)

diff --git a/diffoscope/comparators/image.py b/diffoscope/comparators/image.py
index 2e78c98..68e53d9 100644
--- a/diffoscope/comparators/image.py
+++ b/diffoscope/comparators/image.py
@@ -19,10 +19,11 @@
 
 import re
 import subprocess
+import base64
 
 from diffoscope.tools import tool_required
 from diffoscope.tempfiles import get_named_temporary_file
-from diffoscope.difference import Difference
+from diffoscope.difference import Difference, VisualDifference
 
 from .utils.file import File
 from .utils.command import Command
@@ -77,12 +78,58 @@ class Identify(Command):
             self.path,
         ]
 
+ at tool_required('compare')
+def pixel_difference(image1_path, image2_path):
+    compared_filename = get_named_temporary_file(suffix='.png').name
+    try:
+        subprocess.check_call(('compare', image1_path, image2_path,
+                               '-compose', 'src', compared_filename))
+    except subprocess.CalledProcessError as e:
+        # ImageMagick's `compare` will return 1 if images are different
+        if e.returncode == 1:
+            pass
+    content = base64.b64encode(open(compared_filename, 'rb').read())
+    content = content.decode('utf8')
+    datatype = 'image/png;base64'
+    result = VisualDifference(datatype, content, "Pixel difference")
+    return result
+
+ at tool_required('convert')
+def flicker_difference(image1_path, image2_path):
+    compared_filename = get_named_temporary_file(suffix='.gif').name
+    subprocess.check_call(
+        ('convert', '-delay', '50', image1_path, image2_path,
+         '-loop', '0', '-compose', 'difference', compared_filename))
+    content = base64.b64encode(open(compared_filename, 'rb').read())
+    content = content.decode('utf8')
+    datatype = 'image/gif;base64'
+    result = VisualDifference(datatype, content, "Flicker difference")
+    return result
+
+ at tool_required('identify')
+def get_image_size(image_path):
+    return subprocess.check_output(('identify', '-format',
+                                    '%[h]x%[w]', image_path))
+
 class JPEGImageFile(File):
     RE_FILE_TYPE = re.compile(r'\bJPEG image data\b')
 
     def compare_details(self, other, source=None):
+        content_diff = Difference.from_command(Img2Txt, self.path, other.path,
+                                               source='Image content')
+        if content_diff is not None:
+            try:
+                own_size = get_image_size(self.path)
+                other_size = get_image_size(other.path)
+                if own_size == other_size:
+                    content_diff.add_visuals([
+                        pixel_difference(self.path, other.path),
+                        flicker_difference(self.path, other.path)
+                    ])
+            except subprocess.CalledProcessError:  # noqa
+                pass
         return [
-            Difference.from_command(Img2Txt, self.path, other.path),
+            content_diff,
             Difference.from_command(
                 Identify,
                 self.path,
@@ -103,7 +150,20 @@ class ICOImageFile(File):
         except subprocess.CalledProcessError:  # noqa
             pass
         else:
-            differences.append(Difference.from_command(Img2Txt, png_a, png_b))
+            content_diff = Difference.from_command(Img2Txt, png_a, png_b,
+                                                   source='Image content')
+            if content_diff is not None:
+                try:
+                    own_size = get_image_size(self.path)
+                    other_size = get_image_size(other.path)
+                    if own_size == other_size:
+                        content_diff.add_visuals([
+                            pixel_difference(self.path, other.path),
+                            flicker_difference(self.path, other.path)
+                        ])
+                except subprocess.CalledProcessError:  # noqa
+                    pass
+            differences.append(content_diff)
 
         differences.append(Difference.from_command(
             Identify,
diff --git a/diffoscope/difference.py b/diffoscope/difference.py
index 8342cc0..53e1dda 100644
--- a/diffoscope/difference.py
+++ b/diffoscope/difference.py
@@ -32,6 +32,25 @@ DIFF_CHUNK = 4096
 logger = logging.getLogger(__name__)
 
 
+class VisualDifference(object):
+    def __init__(self, data_type, content, source):
+        self._data_type = data_type
+        self._content = content
+        self._source = source
+
+    @property
+    def data_type(self):
+        return self._data_type
+
+    @property
+    def content(self):
+        return self._content
+
+    @property
+    def source(self):
+        return self._source
+
+
 class Difference(object):
     def __init__(self, unified_diff, path1, path2, source=None, comment=None, has_internal_linenos=False):
         self._comments = []
@@ -60,6 +79,7 @@ class Difference(object):
         # Whether the unified_diff already contains line numbers inside itself
         self._has_internal_linenos = has_internal_linenos
         self._details = []
+        self._visuals = []
 
     def __repr__(self):
         return '<Difference %s -- %s %s>' % (self._source1, self._source2, self._details)
@@ -160,11 +180,20 @@ class Difference(object):
     def details(self):
         return self._details
 
+    @property
+    def visuals(self):
+        return self._visuals
+
     def add_details(self, differences):
         if len([d for d in differences if type(d) is not Difference]) > 0:
             raise TypeError("'differences' must contains Difference objects'")
         self._details.extend(differences)
 
+    def add_visuals(self, visuals):
+        if any([type(v) is not VisualDifference for v in visuals]):
+            raise TypeError("'visuals' must contain VisualDifference objects'")
+        self._visuals.extend(visuals)
+
     def get_reverse(self):
         if self._unified_diff is None:
             unified_diff = None
diff --git a/diffoscope/external_tools.py b/diffoscope/external_tools.py
index fd1173e..34f1ca7 100644
--- a/diffoscope/external_tools.py
+++ b/diffoscope/external_tools.py
@@ -42,6 +42,10 @@ EXTERNAL_TOOLS = {
         'debian': 'diffutils',
         'arch': 'diffutils',
     },
+    'compare': {
+        'debian': 'imagemagick',
+        'arch': 'imagemagick',
+    },
     'cpio': {
         'debian': 'cpio',
         'arch': 'cpio',
diff --git a/diffoscope/presenters/html/html.py b/diffoscope/presenters/html/html.py
index bf8e049..a8e3804 100644
--- a/diffoscope/presenters/html/html.py
+++ b/diffoscope/presenters/html/html.py
@@ -423,6 +423,24 @@ def output_unified_diff(print_func, css_url, directory, unified_diff, has_intern
         text = "load diff (%s %s%s)" % (spl_current_page, noun, (", truncated" if truncated else ""))
         print_func(templates.UD_TABLE_FOOTER % {"filename": html.escape("%s-1.html" % mainname), "text": text}, force=True)
 
+def output_visual(print_func, visual, parents):
+    logger.debug('visual difference for %s', visual.source)
+    sources = parents + [visual.source]
+    print_func(u'<div class="difference">')
+    print_func(u'<div class="diffheader">')
+    print_func(u'<div class="diffcontrol">[−]</div>')
+    print_func(u'<div><span class="source">%s</span>'
+               % html.escape(visual.source))
+    anchor = escape_anchor('/'.join(sources[1:]))
+    print_func(
+        u' <a class="anchor" href="#%s" name="%s">\xb6</a>' % (anchor, anchor))
+    print_func(u"</div>")
+    print_func(u"</div>")
+    print_func(u'<div class="difference">'
+               u'<img src=\"data:%s,%s\" alt=\"compared images\" /></div>' %
+               (visual.data_type, visual.content))
+    print_func(u"</div>", force=True)
+
 def escape_anchor(val):
     """
     ID and NAME tokens must begin with a letter ([A-Za-z]) and may be followed
@@ -461,7 +479,10 @@ def output_difference(difference, print_func, css_url, directory, parents):
             print_func(u'<div class="comment">%s</div>'
                        % u'<br />'.join(map(html.escape, difference.comments)))
         print_func(u"</div>")
-        if difference.unified_diff:
+        if difference.unified_diff and len(difference.visuals) > 0:
+            for visual in difference.visuals:
+                output_visual(print_func, visual, sources)
+        elif difference.unified_diff:
             output_unified_diff(print_func, css_url, directory, difference.unified_diff, difference.has_internal_linenos)
         for detail in difference.details:
             output_difference(detail, print_func, css_url, directory, sources)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/diffoscope.git


More information about the diffoscope mailing list