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

Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to