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
12
15
import shutil
16
+ import sys
17
+ try :
18
+ from shlex import quote as _shlex_quote
19
+ except ImportError :
20
+ from pipes import quote as _shlex_quote
13
21
14
22
import numpy as np
15
23
@@ -128,6 +136,74 @@ def convert(old, new):
128
136
return convert
129
137
130
138
139
+ class _SVGConverter (object ):
140
+ def __init__ (self ):
141
+ self ._proc = None
142
+ # We cannot rely on the GC to trigger `__del__` at exit because
143
+ # other modules (e.g. `subprocess`) may already have their globals
144
+ # set to `None`, which make `proc.communicate` or `proc.terminate`
145
+ # fail. By relying on `atexit` we ensure the destructor runs before
146
+ # `None`-setting occurs.
147
+ atexit .register (self .__del__ )
148
+
149
+ def _read_to_prompt (self ):
150
+ """Did Inkscape reach the prompt without crashing?
151
+ """
152
+ stream = iter (functools .partial (self ._proc .stdout .read , 1 ), "" )
153
+ prompt = tuple ("\n >" )
154
+ n = len (prompt )
155
+ its = itertools .tee (stream , n )
156
+ for i , it in enumerate (its ):
157
+ next (itertools .islice (it , i , i ), None ) # Advance `it` by `i`.
158
+ while True :
159
+ window = tuple (map (next , its ))
160
+ if len (window ) != n :
161
+ # Ran out of data -- one of the `next(it)` raised
162
+ # StopIteration, so the tuple is shorter.
163
+ return False
164
+ if self ._proc .poll ():
165
+ # Inkscape exited.
166
+ return False
167
+ if window == prompt :
168
+ # Successfully read until prompt.
169
+ return True
170
+
171
+ def __call__ (self , orig , dest ):
172
+ if (not self ._proc # First run.
173
+ or self ._proc .poll ()): # Inkscape terminated, e.g. crashed.
174
+ env = os .environ .copy ()
175
+ # If one passes e.g. a png file to Inkscape, it will try to
176
+ # query the user for conversion options via a GUI (even with
177
+ # `--without-gui`). Unsetting `DISPLAY` prevents this (and causes
178
+ # GTK to crash and Inkscape to terminate, but that'll just be
179
+ # reported as a regular exception below).
180
+ env ["DISPLAY" ] = ""
181
+ # Do not load any user options.
182
+ env ["INKSCAPE_PROFILE_DIR" ] = os .devnull
183
+ self ._proc = subprocess .Popen (
184
+ [str ("inkscape" ), "--without-gui" , "--shell" ],
185
+ env = env , stdin = subprocess .PIPE ,
186
+ stdout = subprocess .PIPE , stderr = subprocess .PIPE ,
187
+ universal_newlines = True )
188
+ if not self ._read_to_prompt ():
189
+ raise OSError ("Failed to start Inkscape" )
190
+ # Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
191
+ # behavior across platforms.
192
+ self ._proc .stdin .write (
193
+ "{} --export-png={}\n " .format (* map (_shlex_quote , [orig , dest ])))
194
+ self ._proc .stdin .flush ()
195
+ if not self ._read_to_prompt ():
196
+ raise ImageComparisonFailure (self ._proc .stderr .read ())
197
+
198
+ def __del__ (self ):
199
+ if self ._proc :
200
+ if self ._proc .poll () is None : # Not exited yet.
201
+ self ._proc .communicate ("quit\n " )
202
+ self ._proc .wait ()
203
+ self ._proc .stdin .close ()
204
+ self ._proc .stdout .close ()
205
+
206
+
131
207
def _update_converter ():
132
208
gs , gs_v = matplotlib .checkdep_ghostscript ()
133
209
if gs_v is not None :
@@ -138,9 +214,7 @@ def cmd(old, new):
138
214
converter ['eps' ] = make_external_conversion_command (cmd )
139
215
140
216
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 )
217
+ converter ['svg' ] = _SVGConverter ()
144
218
145
219
146
220
#: A dictionary that maps filename extensions to functions which
@@ -363,9 +437,8 @@ def save_diff_image(expected, actual, output):
363
437
actual , actualImage , expected , expectedImage )
364
438
expectedImage = np .array (expectedImage ).astype (float )
365
439
actualImage = np .array (actualImage ).astype (float )
366
- assert expectedImage .ndim == actualImage .ndim
367
440
assert expectedImage .shape == actualImage .shape
368
- absDiffImage = abs (expectedImage - actualImage )
441
+ absDiffImage = np . abs (expectedImage - actualImage )
369
442
370
443
# expand differences in luminance domain
371
444
absDiffImage *= 255 * 10
0 commit comments