Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bd7865c

Browse files
committed
Inkscape shell mode.
1 parent 8a77cfb commit bd7865c

File tree

1 file changed

+97
-6
lines changed

1 file changed

+97
-6
lines changed

lib/matplotlib/testing/compare.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77

88
import six
99

10+
import atexit
11+
import functools
1012
import hashlib
13+
import itertools
1114
import os
15+
import re
1216
import shutil
17+
import sys
1318

1419
import numpy as np
1520

@@ -19,7 +24,6 @@
1924
from matplotlib import _png
2025
from matplotlib import _get_cachedir
2126
from matplotlib import cbook
22-
from distutils import version
2327

2428
__all__ = ['compare_float', 'compare_images', 'comparable_formats']
2529

@@ -128,6 +132,96 @@ def convert(old, new):
128132
return convert
129133

130134

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+
131225
def _update_converter():
132226
gs, gs_v = matplotlib.checkdep_ghostscript()
133227
if gs_v is not None:
@@ -138,9 +232,7 @@ def cmd(old, new):
138232
converter['eps'] = make_external_conversion_command(cmd)
139233

140234
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()
144236

145237

146238
#: A dictionary that maps filename extensions to functions which
@@ -363,9 +455,8 @@ def save_diff_image(expected, actual, output):
363455
actual, actualImage, expected, expectedImage)
364456
expectedImage = np.array(expectedImage).astype(float)
365457
actualImage = np.array(actualImage).astype(float)
366-
assert expectedImage.ndim == actualImage.ndim
367458
assert expectedImage.shape == actualImage.shape
368-
absDiffImage = abs(expectedImage - actualImage)
459+
absDiffImage = np.abs(expectedImage - actualImage)
369460

370461
# expand differences in luminance domain
371462
absDiffImage *= 255 * 10

0 commit comments

Comments
 (0)