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 5d8e1e0

Browse filesBrowse files
committed
Batch ghostscript converter.
1 parent 4937314 commit 5d8e1e0
Copy full SHA for 5d8e1e0

File tree

Expand file treeCollapse file tree

2 files changed

+95
-60
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+95
-60
lines changed

‎lib/matplotlib/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/__init__.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,10 @@ def checkdep_ghostscript():
334334
gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs']
335335
else:
336336
gs_execs = ['gs']
337-
for gs_exec in gs_execs:
337+
for gs_exec in map(str, gs_execs):
338338
try:
339339
s = subprocess.Popen(
340-
[str(gs_exec), '--version'], stdout=subprocess.PIPE,
340+
[gs_exec, '--version'], stdout=subprocess.PIPE,
341341
stderr=subprocess.PIPE)
342342
stdout, stderr = s.communicate()
343343
if s.returncode == 0:

‎lib/matplotlib/testing/compare.py

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/compare.py
+93-58Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import atexit
1111
import functools
1212
import hashlib
13-
import itertools
1413
import os
1514
import re
1615
import shutil
@@ -133,6 +132,13 @@ def convert(old, new):
133132
return convert
134133

135134

135+
try:
136+
_fsencode = os.fsencode
137+
except AttributeError: # Py2.
138+
def _fsencode(s):
139+
return s.encode(sys.getfilesystemencoding())
140+
141+
136142
# Modified from https://bugs.python.org/issue25567.
137143
_find_unsafe_bytes = re.compile(br'[^a-zA-Z0-9_@%+=:,./-]').search
138144

@@ -142,38 +148,82 @@ def _shlex_quote_bytes(b):
142148
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")
143149

144150

145-
class _SVGConverter(object):
151+
class _ConverterError(Exception):
152+
pass
153+
154+
155+
class _Converter(object):
146156
def __init__(self):
147157
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.
158+
# Explicitly register deletion from an atexit handler because if we
159+
# wait until the object is GC'd (which occurs later), then some module
160+
# globals (e.g. signal.SIGKILL) has already been set to None, and
161+
# kill() doesn't work anymore...
153162
atexit.register(self.__del__)
154163

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+
def __del__(self):
165+
if self._proc:
166+
self._proc.kill()
167+
self._proc.wait()
168+
for stream in filter(None, [self._proc.stdin,
169+
self._proc.stdout,
170+
self._proc.stderr]):
171+
stream.close()
172+
self._proc = None
173+
174+
def _read_until(self, terminator):
175+
"""Read until the prompt is reached."""
176+
terminator = [six.int2byte(c) for c in six.iterbytes(terminator)]
177+
buf = []
164178
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
179+
c = self._proc.stdout.read(1)
180+
if not c:
181+
raise _ConverterError
182+
buf.append(c)
183+
if buf[-len(terminator):] == terminator:
184+
return b"".join(buf[:-len(terminator)])
185+
186+
187+
class _GSConverter(_Converter):
188+
def __call__(self, orig, dest):
189+
if not self._proc:
190+
self._stdout = TemporaryFile()
191+
self._proc = subprocess.Popen(
192+
[matplotlib.checkdep_ghostscript.executable,
193+
"-dNOPAUSE", "-sDEVICE=png16m"],
194+
# As far as I can see, ghostscript never outputs to stderr.
195+
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
196+
try:
197+
self._read_until(b"\nGS")
198+
except _ConverterError:
199+
raise OSError("Failed to start Ghostscript")
200+
201+
def encode_and_escape(name):
202+
return (_fsencode(name)
203+
.replace(b"(", br"\(")
204+
.replace(b")", br"\)")
205+
.replace(b"\\", b"\\\\"))
206+
207+
self._proc.stdin.write(
208+
b"<< /OutputFile ("
209+
+ encode_and_escape(dest)
210+
+ b") >> setpagedevice ("
211+
+ encode_and_escape(orig)
212+
+ b") run flush\n")
213+
self._proc.stdin.flush()
214+
# GS> if nothing left on the stack; GS<n> if n items left on the stack.
215+
err = self._read_until(b"GS")
216+
stack = self._read_until(b">")
217+
if stack or not os.path.exists(dest):
218+
if stack:
219+
self._proc.stdin.write(b"pop\n" * int(stack[1:]))
220+
# Using the systemencoding should at least get the filenames right.
221+
raise ImageComparisonFailure(
222+
(err + b"GS" + stack + b">")
223+
.decode(sys.getfilesystemencoding(), "replace"))
224+
176225

226+
class _SVGConverter(_Converter):
177227
def __call__(self, orig, dest):
178228
if (not self._proc # First run.
179229
or self._proc.poll() is not None): # Inkscape terminated.
@@ -191,23 +241,21 @@ def __call__(self, orig, dest):
191241
# seem to sometimes deadlock when stderr is redirected to a pipe,
192242
# so we redirect it to a temporary file instead. This is not
193243
# necessary anymore as of Inkscape 0.92.1.
194-
self._stderr = TemporaryFile()
244+
stderr = TemporaryFile()
195245
self._proc = subprocess.Popen(
196246
[str("inkscape"), "--without-gui", "--shell"],
197247
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())
248+
stderr=stderr, env=env)
249+
# Slight abuse, but makes shutdown handling easier.
250+
self._proc.stderr = stderr
251+
try:
252+
self._read_until(b"\n>")
253+
except _ConverterError:
254+
raise OSError("Failed to start Inkscape in interactive mode")
207255

208256
# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
209257
# behavior across platforms, so we can just use `shlex.quote`.
210-
orig_b, dest_b = map(_shlex_quote_bytes, map(fsencode, [orig, dest]))
258+
orig_b, dest_b = map(_shlex_quote_bytes, map(_fsencode, [orig, dest]))
211259
if b"\n" in orig_b or b"\n" in dest_b:
212260
# Who knows whether the current folder name has a newline, or if
213261
# our encoding is even ASCII compatible... Just fall back on the
@@ -217,35 +265,22 @@ def fsencode(s):
217265
str('inkscape'), '-z', old, '--export-png', new])(orig, dest)
218266
self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n")
219267
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...
268+
try:
269+
self._read_until(b"\n>")
270+
except _ConverterError:
271+
# Inkscape's output is not localized but gtk's is, so the output
272+
# stream probably has a mixed encoding. Using the filesystem
273+
# encoding should at least get the filenames right...
225274
self._stderr.seek(0)
226275
raise ImageComparisonFailure(
227276
self._stderr.read().decode(
228277
sys.getfilesystemencoding(), "replace"))
229278

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-
239279

240280
def _update_converter():
241281
gs, gs_v = matplotlib.checkdep_ghostscript()
242282
if gs_v is not None:
243-
def cmd(old, new):
244-
return [str(gs), '-q', '-sDEVICE=png16m', '-dNOPAUSE', '-dBATCH',
245-
'-sOutputFile=' + new, old]
246-
converter['pdf'] = make_external_conversion_command(cmd)
247-
converter['eps'] = make_external_conversion_command(cmd)
248-
283+
converter['pdf'] = converter['eps'] = _GSConverter()
249284
if matplotlib.checkdep_inkscape() is not None:
250285
converter['svg'] = _SVGConverter()
251286

0 commit comments

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