Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8c66203

Browse filesBrowse files
committed
Inkscape shell mode.
1 parent 343792f commit 8c66203
Copy full SHA for 8c66203

File tree

Expand file treeCollapse file tree

1 file changed

+112
-6
lines changed
Filter options
Expand file treeCollapse file tree

1 file changed

+112
-6
lines changed

‎lib/matplotlib/testing/compare.py

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/compare.py
+112-6Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
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
18+
from tempfile import TemporaryFile
1319

1420
import numpy as np
1521

@@ -19,7 +25,6 @@
1925
from matplotlib import _png
2026
from matplotlib import _get_cachedir
2127
from matplotlib import cbook
22-
from distutils import version
2328

2429
__all__ = ['compare_float', 'compare_images', 'comparable_formats']
2530

@@ -128,6 +133,110 @@ def convert(old, new):
128133
return convert
129134

130135

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+
131240
def _update_converter():
132241
gs, gs_v = matplotlib.checkdep_ghostscript()
133242
if gs_v is not None:
@@ -138,9 +247,7 @@ def cmd(old, new):
138247
converter['eps'] = make_external_conversion_command(cmd)
139248

140249
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()
144251

145252

146253
#: A dictionary that maps filename extensions to functions which
@@ -363,9 +470,8 @@ def save_diff_image(expected, actual, output):
363470
actual, actualImage, expected, expectedImage)
364471
expectedImage = np.array(expectedImage).astype(float)
365472
actualImage = np.array(actualImage).astype(float)
366-
assert expectedImage.ndim == actualImage.ndim
367473
assert expectedImage.shape == actualImage.shape
368-
absDiffImage = abs(expectedImage - actualImage)
474+
absDiffImage = np.abs(expectedImage - actualImage)
369475

370476
# expand differences in luminance domain
371477
absDiffImage *= 255 * 10

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.