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
18
+ from tempfile import TemporaryFile
13
19
14
20
import numpy as np
15
21
19
25
from matplotlib import _png
20
26
from matplotlib import _get_cachedir
21
27
from matplotlib import cbook
22
- from distutils import version
23
28
24
29
__all__ = ['compare_float' , 'compare_images' , 'comparable_formats' ]
25
30
@@ -128,6 +133,110 @@ def convert(old, new):
128
133
return convert
129
134
130
135
136
+ # Modified from https://bugs.python.org/issue25567.
137
+ _find_unsafe_bytes = re .compile (br'[^a-zA-Z0-9_@%+=:,./-]' ).search
138
+
139
+
140
+ def _shlex_quote_bytes (b ):
141
+ return (b if _find_unsafe_bytes (b ) is None
142
+ else b"'" + b .replace (b"'" , b"'\" '\" '" ) + b"'" )
143
+
144
+
145
+ class _SVGConverter (object ):
146
+ def __init__ (self ):
147
+ self ._proc = None
148
+ # We cannot rely on the GC to trigger `__del__` at exit because
149
+ # other modules (e.g. `subprocess`) may already have their globals
150
+ # set to `None`, which make `proc.communicate` or `proc.terminate`
151
+ # fail. By relying on `atexit` we ensure the destructor runs before
152
+ # `None`-setting occurs.
153
+ atexit .register (self .__del__ )
154
+
155
+ def _read_to_prompt (self ):
156
+ """Did Inkscape reach the prompt without crashing?
157
+ """
158
+ stream = iter (functools .partial (self ._proc .stdout .read , 1 ), b"" )
159
+ prompt = (b"\n " , b">" )
160
+ n = len (prompt )
161
+ its = itertools .tee (stream , n )
162
+ for i , it in enumerate (its ):
163
+ next (itertools .islice (it , i , i ), None ) # Advance `it` by `i`.
164
+ while True :
165
+ window = tuple (map (next , its ))
166
+ if len (window ) != n :
167
+ # Ran out of data -- one of the `next(it)` raised
168
+ # StopIteration, so the tuple is shorter.
169
+ return False
170
+ if self ._proc .poll () is not None :
171
+ # Inkscape exited.
172
+ return False
173
+ if window == prompt :
174
+ # Successfully read until prompt.
175
+ return True
176
+
177
+ def __call__ (self , orig , dest ):
178
+ if (not self ._proc # First run.
179
+ or self ._proc .poll () is not None ): # Inkscape terminated.
180
+ env = os .environ .copy ()
181
+ # If one passes e.g. a png file to Inkscape, it will try to
182
+ # query the user for conversion options via a GUI (even with
183
+ # `--without-gui`). Unsetting `DISPLAY` prevents this (and causes
184
+ # GTK to crash and Inkscape to terminate, but that'll just be
185
+ # reported as a regular exception below).
186
+ env .pop ("DISPLAY" , None ) # May already be unset.
187
+ # Do not load any user options.
188
+ # `os.environ` needs native strings on Py2+Windows.
189
+ env [str ("INKSCAPE_PROFILE_DIR" )] = os .devnull
190
+ # Old versions of Inkscape (0.48.3.1, used on Travis as of now)
191
+ # seem to sometimes deadlock when stderr is redirected to a pipe,
192
+ # so we redirect it to a temporary file instead. This is not
193
+ # necessary anymore as of Inkscape 0.92.1.
194
+ self ._stderr = TemporaryFile ()
195
+ self ._proc = subprocess .Popen (
196
+ [str ("inkscape" ), "--without-gui" , "--shell" ],
197
+ stdin = subprocess .PIPE , stdout = subprocess .PIPE ,
198
+ stderr = self ._stderr , env = env )
199
+ if not self ._read_to_prompt ():
200
+ raise OSError ("Failed to start Inkscape" )
201
+
202
+ try :
203
+ fsencode = os .fsencode
204
+ except AttributeError : # Py2.
205
+ def fsencode (s ):
206
+ return s .encode (sys .getfilesystemencoding ())
207
+
208
+ # Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
209
+ # behavior across platforms, so we can just use `shlex.quote`.
210
+ orig_b , dest_b = map (_shlex_quote_bytes , map (fsencode , [orig , dest ]))
211
+ if b"\n " in orig_b or b"\n " in dest_b :
212
+ # Who knows whether the current folder name has a newline, or if
213
+ # our encoding is even ASCII compatible... Just fall back on the
214
+ # slow solution (Inkscape uses `fgets` so it will always stop at a
215
+ # newline).
216
+ return make_external_conversion_command (lambda old , new : [
217
+ str ('inkscape' ), '-z' , old , '--export-png' , new ])(orig , dest )
218
+ self ._proc .stdin .write (orig_b + b" --export-png=" + dest_b + b"\n " )
219
+ self ._proc .stdin .flush ()
220
+ if not self ._read_to_prompt ():
221
+ # Inkscape's output is not localized but gtk's is, so the
222
+ # output stream probably has a mixed encoding. Using
223
+ # `getfilesystemencoding` should at least get the filenames
224
+ # right...
225
+ self ._stderr .seek (0 )
226
+ raise ImageComparisonFailure (
227
+ self ._stderr .read ().decode (
228
+ sys .getfilesystemencoding (), "replace" ))
229
+
230
+ def __del__ (self ):
231
+ if self ._proc :
232
+ if self ._proc .poll () is None : # Not exited yet.
233
+ self ._proc .communicate (b"quit\n " )
234
+ self ._proc .wait ()
235
+ self ._proc .stdin .close ()
236
+ self ._proc .stdout .close ()
237
+ self ._stderr .close ()
238
+
239
+
131
240
def _update_converter ():
132
241
gs , gs_v = matplotlib .checkdep_ghostscript ()
133
242
if gs_v is not None :
@@ -138,9 +247,7 @@ def cmd(old, new):
138
247
converter ['eps' ] = make_external_conversion_command (cmd )
139
248
140
249
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 )
250
+ converter ['svg' ] = _SVGConverter ()
144
251
145
252
146
253
#: A dictionary that maps filename extensions to functions which
@@ -363,9 +470,8 @@ def save_diff_image(expected, actual, output):
363
470
actual , actualImage , expected , expectedImage )
364
471
expectedImage = np .array (expectedImage ).astype (float )
365
472
actualImage = np .array (actualImage ).astype (float )
366
- assert expectedImage .ndim == actualImage .ndim
367
473
assert expectedImage .shape == actualImage .shape
368
- absDiffImage = abs (expectedImage - actualImage )
474
+ absDiffImage = np . abs (expectedImage - actualImage )
369
475
370
476
# expand differences in luminance domain
371
477
absDiffImage *= 255 * 10
0 commit comments