Commit 7a067c82 authored by Maria Glukhova's avatar Maria Glukhova

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.
parent caad6d1c
......@@ -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,
]
@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
@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
@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,
......
......@@ -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
......
......@@ -42,6 +42,10 @@ EXTERNAL_TOOLS = {
'debian': 'diffutils',
'arch': 'diffutils',
},
'compare': {
'debian': 'imagemagick',
'arch': 'imagemagick',
},
'cpio': {
'debian': 'cpio',
'arch': 'cpio',
......
......@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment