7
7
8
8
import six
9
9
10
+ import atexit
11
+ import functools
10
12
import hashlib
13
+ import itertools
11
14
import os
15
+ import re
12
16
import shutil
17
+ import sys
13
18
14
19
import numpy as np
15
20
19
24
from matplotlib import _png
20
25
from matplotlib import _get_cachedir
21
26
from matplotlib import cbook
22
- from distutils import version
23
27
24
28
__all__ = ['compare_float' , 'compare_images' , 'comparable_formats' ]
25
29
@@ -128,6 +132,96 @@ def convert(old, new):
128
132
return convert
129
133
130
134
135
+ # Copied from https://bugs.python.org/issue25567
136
+ _find_unsafe_bytes = re .compile (rb'[^\w@%+=:,./-]' , re .ASCII ).search
137
+
138
+
139
+ def _shlex_quote_bytes (b ):
140
+ return (b if _find_unsafe_bytes (b ) is None
141
+ else b"'" + b .replace (b"'" , b"'\" '\" '" ) + b"'" )
142
+
143
+
144
+ class _SVGConverter (object ):
145
+ def __init__ (self ):
146
+ self ._proc = None
147
+ # We cannot rely on the GC to trigger `__del__` at exit because
148
+ # other modules (e.g. `subprocess`) may already have their globals
149
+ # set to `None`, which make `proc.communicate` or `proc.terminate`
150
+ # fail. By relying on `atexit` we ensure the destructor runs before
151
+ # `None`-setting occurs.
152
+ atexit .register (self .__del__ )
153
+
154
+ def _read_to_prompt (self ):
155
+ """Did Inkscape reach the prompt without crashing?
156
+ """
157
+ stream = iter (functools .partial (self ._proc .stdout .read , 1 ), b"" )
158
+ prompt = (b"\n " , b">" )
159
+ n = len (prompt )
160
+ its = itertools .tee (stream , n )
161
+ for i , it in enumerate (its ):
162
+ next (itertools .islice (it , i , i ), None ) # Advance `it` by `i`.
163
+ while True :
164
+ window = tuple (map (next , its ))
165
+ if len (window ) != n :
166
+ # Ran out of data -- one of the `next(it)` raised
167
+ # StopIteration, so the tuple is shorter.
168
+ return False
169
+ if self ._proc .poll () is not None :
170
+ # Inkscape exited.
171
+ return False
172
+ if window == prompt :
173
+ # Successfully read until prompt.
174
+ return True
175
+
176
+ def __call__ (self , orig , dest ):
177
+ if (not self ._proc # First run.
178
+ or self ._proc .poll () is not None ): # Inkscape terminated.
179
+ env = os .environ .copy ()
180
+ # If one passes e.g. a png file to Inkscape, it will try to
181
+ # query the user for conversion options via a GUI (even with
182
+ # `--without-gui`). Unsetting `DISPLAY` prevents this (and causes
183
+ # GTK to crash and Inkscape to terminate, but that'll just be
184
+ # reported as a regular exception below).
185
+ env ["DISPLAY" ] = ""
186
+ # Do not load any user options.
187
+ env ["INKSCAPE_PROFILE_DIR" ] = os .devnull
188
+ self ._proc = subprocess .Popen (
189
+ [str ("inkscape" ), "--without-gui" , "--shell" ],
190
+ env = env , stdin = subprocess .PIPE ,
191
+ stdout = subprocess .PIPE , stderr = subprocess .PIPE )
192
+ if not self ._read_to_prompt ():
193
+ raise OSError ("Failed to start Inkscape" )
194
+
195
+ # Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
196
+ # behavior across platforms, so we can just use `shlex.quote`.
197
+ try :
198
+ fsencode = os .fsencode
199
+ except AttributeError : # Py2.
200
+ def fsencode (s ):
201
+ return s .encode (sys .getfilesystemencoding ())
202
+
203
+ orig , dest = map (_shlex_quote_bytes , map (fsencode , [orig , dest ]))
204
+ self ._proc .stdin .write (orig + b" --export-png=" + dest + b"\n " )
205
+ self ._proc .stdin .flush ()
206
+ if not self ._read_to_prompt ():
207
+ # I *guess* we should technically decode the stream using
208
+ # `getdefaultencoding` *except* for filenames which should be
209
+ # decoded using `getfilesystemencoding` but that would be a bit
210
+ # overkill.
211
+ raise ImageComparisonFailure (
212
+ self ._proc .stderr .read ().decode (
213
+ sys .getfilesystemencoding (), "replace" ))
214
+
215
+ def __del__ (self ):
216
+ if self ._proc :
217
+ if self ._proc .poll () is None : # Not exited yet.
218
+ self ._proc .communicate (b"quit\n " )
219
+ self ._proc .wait ()
220
+ self ._proc .stdin .close ()
221
+ self ._proc .stdout .close ()
222
+ self ._proc .stderr .close ()
223
+
224
+
131
225
def _update_converter ():
132
226
gs , gs_v = matplotlib .checkdep_ghostscript ()
133
227
if gs_v is not None :
@@ -138,9 +232,7 @@ def cmd(old, new):
138
232
converter ['eps' ] = make_external_conversion_command (cmd )
139
233
140
234
if matplotlib .checkdep_inkscape () is not None :
141
- def cmd (old , new ):
142
- return [str ('inkscape' ), '-z' , old , '--export-png' , new ]
143
- converter ['svg' ] = make_external_conversion_command (cmd )
235
+ converter ['svg' ] = _SVGConverter ()
144
236
145
237
146
238
#: A dictionary that maps filename extensions to functions which
@@ -363,9 +455,8 @@ def save_diff_image(expected, actual, output):
363
455
actual , actualImage , expected , expectedImage )
364
456
expectedImage = np .array (expectedImage ).astype (float )
365
457
actualImage = np .array (actualImage ).astype (float )
366
- assert expectedImage .ndim == actualImage .ndim
367
458
assert expectedImage .shape == actualImage .shape
368
- absDiffImage = abs (expectedImage - actualImage )
459
+ absDiffImage = np . abs (expectedImage - actualImage )
369
460
370
461
# expand differences in luminance domain
371
462
absDiffImage *= 255 * 10
0 commit comments