Hi, I believe the attached patch provides the requested feature. I would be very grateful for any review or general advice on how to make it better.
From 7bdcf310284a11e788fe761f86b1d1dbb51cf968 Mon Sep 17 00:00:00 2001 From: Maria Glukhova <siamez...@gmail.com> Date: Sat, 1 Apr 2017 22:07:54 +0300 Subject: [PATCH] Add visual difference between images in HTML output. (Closes: #851359)
When comparing JPEG, ICO, PNG or static GIF images with HTML output, construct visual diffrence images using ImageMagick. Two types of visual difference images are constructed, one with all the different pixels outlined (pixel difference) and one being the animation formed from the compared images (flicker difference). The constructed images are converted to base64 and stored in VisualDifference object together with their type. They are then printed in the HTML output using data URI. If none of the HTML output options is specified, visual difference is not computed. --- diffoscope/comparators/gif.py | 33 +++++++++++++++- diffoscope/comparators/image.py | 74 +++++++++++++++++++++++++++++++++-- diffoscope/comparators/png.py | 24 +++++++++++- diffoscope/config.py | 1 + diffoscope/difference.py | 29 ++++++++++++++ diffoscope/external_tools.py | 4 ++ diffoscope/main.py | 2 + diffoscope/presenters/html/html.py | 21 ++++++++++ tests/comparators/test_gif.py | 19 +++++++++ tests/comparators/test_ico_image.py | 10 +++++ tests/comparators/test_jpeg_image.py | 9 +++++ tests/comparators/test_png.py | 10 +++++ tests/data/test3.gif | Bin 0 -> 854 bytes tests/data/test4.gif | Bin 0 -> 2094 bytes tests/test_presenters.py | 27 +++++++++++++ 15 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 tests/data/test3.gif create mode 100644 tests/data/test4.gif diff --git a/diffoscope/comparators/gif.py b/diffoscope/comparators/gif.py index d01d7aa..fb60416 100644 --- a/diffoscope/comparators/gif.py +++ b/diffoscope/comparators/gif.py @@ -18,12 +18,18 @@ # along with diffoscope. If not, see <https://www.gnu.org/licenses/>. import re +import subprocess +import logging from diffoscope.tools import tool_required from diffoscope.difference import Difference +from diffoscope.config import Config from .utils.file import File from .utils.command import Command +from .image import pixel_difference, flicker_difference, get_image_size + +logger = logging.getLogger(__name__) class Gifbuild(Command): @@ -41,14 +47,37 @@ class Gifbuild(Command): return b"" return line +@tool_required('identify') +def is_image_static(image_path): + return subprocess.check_output(('identify', '-format', + '%n', image_path)) == b'1' class GifFile(File): RE_FILE_TYPE = re.compile(r'^GIF image data\b') def compare_details(self, other, source=None): - return [Difference.from_command( + gifbuild_diff = Difference.from_command( Gifbuild, self.path, other.path, source='gifbuild', - )] + ) + differences = [gifbuild_diff] + if (gifbuild_diff is not None) and Config().html_output and \ + (get_image_size(self.path) == get_image_size(other.path)): + try: + own_size = get_image_size(self.path) + other_size = get_image_size(other.path) + self_static = is_image_static(self.path) + other_static = is_image_static(other.path) + if (own_size == other_size) and self_static and other_static: + logger.debug('Generating visual difference for %s and %s', + self.path, other.path) + content_diff = Difference(None, self.path, other.path, + source='Image content') + content_diff.add_visuals([pixel_difference(self.path, other.path), + flicker_difference(self.path, other.path)]) + differences.append(content_diff) + except subprocess.CalledProcessError: #noqa + pass + return differences diff --git a/diffoscope/comparators/image.py b/diffoscope/comparators/image.py index 2e78c98..6ba7353 100644 --- a/diffoscope/comparators/image.py +++ b/diffoscope/comparators/image.py @@ -19,16 +19,21 @@ import re import subprocess +import base64 +import logging 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 diffoscope.config import Config from .utils.file import File from .utils.command import Command re_ansi_escapes = re.compile(r'\x1b[^m]*m') +logger = logging.getLogger(__name__) + class Img2Txt(Command): @tool_required('img2txt') @@ -77,12 +82,60 @@ 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) and Config().html_output: + try: + own_size = get_image_size(self.path) + other_size = get_image_size(other.path) + if own_size == other_size: + logger.debug('Generating visual difference for %s and %s', + self.path, other.path) + 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 +156,22 @@ 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) and Config().html_output: + try: + own_size = get_image_size(self.path) + other_size = get_image_size(other.path) + if own_size == other_size: + logger.debug('Generating visual difference for %s and %s', + self.path, other.path) + 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/comparators/png.py b/diffoscope/comparators/png.py index f3b3fdb..a24a8f4 100644 --- a/diffoscope/comparators/png.py +++ b/diffoscope/comparators/png.py @@ -19,12 +19,18 @@ import re import functools +import subprocess +import logging from diffoscope.tools import tool_required from diffoscope.difference import Difference +from diffoscope.config import Config from .utils.file import File from .utils.command import Command +from .image import pixel_difference, flicker_difference, get_image_size + +logger = logging.getLogger(__name__) class Sng(Command): @@ -42,4 +48,20 @@ class PngFile(File): RE_FILE_TYPE = re.compile(r'^PNG image data\b') def compare_details(self, other, source=None): - return [Difference.from_command(Sng, self.path, other.path, source='sng')] + sng_diff = Difference.from_command(Sng, self.path, other.path, source='sng') + differences = [sng_diff] + if (sng_diff is not None) and Config().html_output: + try: + own_size = get_image_size(self.path) + other_size = get_image_size(other.path) + if own_size == other_size: + logger.debug('Generating visual difference for %s and %s', + self.path, other.path) + content_diff = Difference(None, self.path, other.path, + source='Image content') + content_diff.add_visuals([pixel_difference(self.path, other.path), + flicker_difference(self.path, other.path)]) + differences.append(content_diff) + except subprocess.CalledProcessError: #noqa + pass + return differences diff --git a/diffoscope/config.py b/diffoscope/config.py index 025790d..7962683 100644 --- a/diffoscope/config.py +++ b/diffoscope/config.py @@ -34,6 +34,7 @@ class Config(object): fuzzy_threshold = 60 enforce_constraints = True excludes = () + html_output = False _singleton = {} 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 cb40989..d684bf8 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/main.py b/diffoscope/main.py index 532117d..186bfb0 100644 --- a/diffoscope/main.py +++ b/diffoscope/main.py @@ -285,6 +285,8 @@ def run_diffoscope(parsed_args): Config().fuzzy_threshold = parsed_args.fuzzy_threshold Config().new_file = parsed_args.new_file Config().excludes = parsed_args.excludes + Config().html_output = any((parsed_args.html_output, + parsed_args.html_output_directory)) set_path() set_locale() logger.debug('Starting comparison') diff --git a/diffoscope/presenters/html/html.py b/diffoscope/presenters/html/html.py index bf8e049..3378cff 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('including image 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,6 +479,9 @@ 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 len(difference.visuals) > 0: + for visual in difference.visuals: + output_visual(print_func, visual, sources) if difference.unified_diff: output_unified_diff(print_func, css_url, directory, difference.unified_diff, difference.has_internal_linenos) for detail in difference.details: diff --git a/tests/comparators/test_gif.py b/tests/comparators/test_gif.py index bd1915e..bb898c7 100644 --- a/tests/comparators/test_gif.py +++ b/tests/comparators/test_gif.py @@ -20,6 +20,7 @@ import pytest from diffoscope.comparators.gif import GifFile +from diffoscope.config import Config from utils.data import load_fixture, get_data from utils.tools import skip_unless_tools_exist @@ -27,6 +28,8 @@ from utils.nonexisting import assert_non_existing gif1 = load_fixture('test1.gif') gif2 = load_fixture('test2.gif') +gif3 = load_fixture('test3.gif') +gif4 = load_fixture('test4.gif') def test_identification(gif1): @@ -48,3 +51,19 @@ def test_diff(differences): @skip_unless_tools_exist('gifbuild') def test_compare_non_existing(monkeypatch, gif1): assert_non_existing(monkeypatch, gif1, has_null_source=False) + +@skip_unless_tools_exist('gifbuild', 'compose', 'convert', 'identify') +def test_has_visuals(monkeypatch, gif3, gif4): + monkeypatch.setattr(Config(), 'html_output', True) + gif_diff = gif3.compare(gif4) + assert len(gif_diff.details) == 2 + assert len(gif_diff.details[1].visuals) == 2 + assert gif_diff.details[1].visuals[0].data_type == 'image/png;base64' + assert gif_diff.details[1].visuals[1].data_type == 'image/gif;base64' + +@skip_unless_tools_exist('gifbuild', 'compose', 'convert', 'identify') +def test_no_visuals_different_size(monkeypatch, gif1, gif2): + monkeypatch.setattr(Config(), 'html_output', True) + gif_diff = gif1.compare(gif2) + assert len(gif_diff.details) == 1 + assert len(gif_diff.details[0].visuals) == 0 diff --git a/tests/comparators/test_ico_image.py b/tests/comparators/test_ico_image.py index 7543adf..aa1eddb 100644 --- a/tests/comparators/test_ico_image.py +++ b/tests/comparators/test_ico_image.py @@ -20,6 +20,7 @@ import pytest from diffoscope.comparators.image import ICOImageFile +from diffoscope.config import Config from utils.data import load_fixture, get_data from utils.tools import skip_unless_tools_exist, skip_unless_tool_is_at_least @@ -56,3 +57,12 @@ def differences_meta(image1_meta, image2_meta): def test_diff_meta(differences_meta): expected_diff = get_data('ico_image_meta_expected_diff') assert differences_meta[-1].unified_diff == expected_diff + +@skip_unless_tools_exist('img2txt', 'compose', 'convert', 'identify') +def test_has_visuals(monkeypatch, image1, image2): + monkeypatch.setattr(Config(), 'html_output', True) + ico_diff = image1.compare(image2) + assert len(ico_diff.details) == 2 + assert len(ico_diff.details[0].visuals) == 2 + assert ico_diff.details[0].visuals[0].data_type == 'image/png;base64' + assert ico_diff.details[0].visuals[1].data_type == 'image/gif;base64' diff --git a/tests/comparators/test_jpeg_image.py b/tests/comparators/test_jpeg_image.py index af405e4..8dd7e5d 100644 --- a/tests/comparators/test_jpeg_image.py +++ b/tests/comparators/test_jpeg_image.py @@ -73,3 +73,12 @@ def differences_meta(image1_meta, image2_meta): def test_diff_meta(differences_meta): expected_diff = get_data('jpeg_image_meta_expected_diff') assert differences_meta[-1].unified_diff == expected_diff + +@skip_unless_tools_exist('img2txt', 'compose', 'convert', 'identify') +def test_has_visuals(monkeypatch, image1, image2): + monkeypatch.setattr(Config(), 'html_output', True) + jpg_diff = image1.compare(image2) + assert len(jpg_diff.details) == 2 + assert len(jpg_diff.details[0].visuals) == 2 + assert jpg_diff.details[0].visuals[0].data_type == 'image/png;base64' + assert jpg_diff.details[0].visuals[1].data_type == 'image/gif;base64' diff --git a/tests/comparators/test_png.py b/tests/comparators/test_png.py index 8e8ee45..76378b7 100644 --- a/tests/comparators/test_png.py +++ b/tests/comparators/test_png.py @@ -20,6 +20,7 @@ import pytest from diffoscope.comparators.png import PngFile +from diffoscope.config import Config from utils.data import load_fixture, get_data from utils.tools import skip_unless_tools_exist @@ -48,3 +49,12 @@ def test_diff(differences): @skip_unless_tools_exist('sng') def test_compare_non_existing(monkeypatch, png1): assert_non_existing(monkeypatch, png1, has_null_source=False) + +@skip_unless_tools_exist('sng', 'compose', 'convert', 'identify') +def test_has_visuals(monkeypatch, png1, png2): + monkeypatch.setattr(Config(), 'html_output', True) + png_diff = png1.compare(png2) + assert len(png_diff.details) == 2 + assert len(png_diff.details[1].visuals) == 2 + assert png_diff.details[1].visuals[0].data_type == 'image/png;base64' + assert png_diff.details[1].visuals[1].data_type == 'image/gif;base64' diff --git a/tests/data/test3.gif b/tests/data/test3.gif new file mode 100644 index 0000000000000000000000000000000000000000..d10963db2a77bd7141fbb918094cacce68807917 GIT binary patch literal 854 zcmV-c1F8H+Nk%w1VPpVg0QEfp000020s;gC1O^5M2nYxk78Vy57bYeqE-o%GFfcJO zF+M&%K|w)AMn+3ZOHEBpPft%!P*7!MWpHqCadB~Wc6N7ncYl9>fPjF3fq{dAgNllZ zj*gC&mX?>7m!6)UprD|mqobsxq^PK<t*x!Hva-Fsy}!S|!NI}B#l^?R$H~db%gf8r z(b3b>)7IA3+uPgT-rnHg;N|7z=;-M1@bLBZ_4fAm`T6<!`uh9(`~Cg>{{H^||Nj60 z00000A^8LV00000EC2ui0Av7U000L5z@BhOEE<o<q;kn@I-k(uEoSme9EidoFeo4# z6OSo&i<-~qA+01K0N(JpJWi05Xz9Fu$a`S|bAf_$0T?eldx=go9D|OJavXSyltnEN zkeQkgEtQlk3Ywyt3M`&^Fbt!snhY?fQ8pH=w3-$+uuUknyqYMvNjMa}#E=v?z(q0z z#m$ZdGRZ<G&ee`5$spF-f*`pY+~IQ@rx)VqaTk>u=<RPBiXHCr9eXA7_$7HR`28+u zHTnsxapr-*eh<uuI4DpAlo<>U&Un%wqQC}DOjtZ<f{Bie40S9C0rFr7B{)hlM22L9 z%7Y`2>=5u$pnx4mRMb4E;t2kxo&6vf2?%sx5fViOk|?quX}<+TY&aE&OUR0;{Z<Ug zfE8bcAqTq7J0K*7SbRBpKs@ViVhXnIQuO!$x7~*x73!j^&?AE0a}jzZ_$w|!4+Dh5 z8R+3A@iqcI06Df6fbnE&kP%;Yws`R7W`_NKE*3cNXkvWnp6+$`?P^|f*S@AT(d=wm zX?ujdJ&LRluDnHkwHjQMRv}W0XR0cssd7h9i6DJGXekk*)B_PL@-urJp+<CikJHo0 z%<*q-wnPsgCX$rsSt23n{iKqQ-bF6aNWEi|ippW+glxf0^u$8JITVFKw<Tl-cCT$` z20yR;lg2%#>7$1{pLNxf2s@X-Ly0+#spAPagpmV_HhOsj3^j0JBh4~mK_kr&!eDjG zjYaH|)Gs_5A<Is;1eruCCAnfG6QD#?%92l95>zKpM!`rsl2n;SAAeL+$RS!PvPK)U g)Nx0cz)Vra6<KW2#TQ|WQ3jfH)@kRRL<9i<JAJc-y8r+H literal 0 HcmV?d00001 diff --git a/tests/data/test4.gif b/tests/data/test4.gif new file mode 100644 index 0000000000000000000000000000000000000000..b429d6ac9b8a842d70ef7937b715a4828aa98280 GIT binary patch literal 2094 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 literal 0 HcmV?d00001 diff --git a/tests/test_presenters.py b/tests/test_presenters.py index 5a0f874..e267eb8 100644 --- a/tests/test_presenters.py +++ b/tests/test_presenters.py @@ -44,6 +44,23 @@ def run(capsys, *args): return out +def run_images(capsys, *args): + with pytest.raises(SystemExit) as exc: + prev = os.getcwd() + os.chdir(DATA_DIR) + + try: + main(args + ('test1.png', 'test2.png')) + finally: + os.chdir(prev) + + out, err = capsys.readouterr() + + assert err == '' + assert exc.value.code == 1 + + return out + def data(filename): with open(os.path.join(DATA_DIR, filename), encoding='utf-8') as f: return f.read() @@ -115,6 +132,16 @@ def test_html_option_with_file(tmpdir, capsys): with open(report_path, 'r', encoding='utf-8') as f: assert extract_body(f.read()) == extract_body(data('output.html')) +def test_html_visuals(tmpdir, capsys): + report_path = str(tmpdir.join('report.html')) + + out = run_images(capsys, '--html', report_path) + + assert out == '' + body = extract_body(open(report_path, 'r', encoding='utf-8').read()) + assert '<img src="data:image/png;base64' in body + assert '<img src="data:image/gif;base64' in body + def test_htmldir_option(tmpdir, capsys): html_dir = os.path.join(str(tmpdir), 'target') -- 2.11.0
signature.asc
Description: OpenPGP digital signature