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

Skip to content

Commit 5d8e1e0

Browse files
committed
Batch ghostscript converter.
1 parent 4937314 commit 5d8e1e0

File tree

2 files changed

+95
-60
lines changed

2 files changed

+95
-60
lines changed

lib/matplotlib/__init__.py

Lines 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

Lines 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)