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

Skip to content

Commit 6c875d2

Browse files
committed
Batch ghostscript converter.
1 parent af4830e commit 6c875d2

File tree

2 files changed

+88
-59
lines changed

2 files changed

+88
-59
lines changed

lib/matplotlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def checkdep_ghostscript():
444444
for gs_exec in gs_execs:
445445
try:
446446
s = subprocess.Popen(
447-
[str(gs_exec), '--version'], stdout=subprocess.PIPE,
447+
[gs_exec, '--version'], stdout=subprocess.PIPE,
448448
stderr=subprocess.PIPE)
449449
stdout, stderr = s.communicate()
450450
if s.returncode == 0:

lib/matplotlib/testing/compare.py

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import atexit
1010
import functools
1111
import hashlib
12-
import itertools
1312
import os
1413
from pathlib import Path
1514
import re
@@ -141,38 +140,82 @@ def _shlex_quote_bytes(b):
141140
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")
142141

143142

144-
class _SVGConverter(object):
143+
class _ConverterError(Exception):
144+
pass
145+
146+
147+
class _Converter(object):
145148
def __init__(self):
146149
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.
150+
# Explicitly register deletion from an atexit handler because if we
151+
# wait until the object is GC'd (which occurs later), then some module
152+
# globals (e.g. signal.SIGKILL) has already been set to None, and
153+
# kill() doesn't work anymore...
152154
atexit.register(self.__del__)
153155

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

218+
class _SVGConverter(_Converter):
176219
def __call__(self, orig, dest):
177220
if (not self._proc # First run.
178221
or self._proc.poll() is not None): # Inkscape terminated.
@@ -190,23 +233,22 @@ def __call__(self, orig, dest):
190233
# seem to sometimes deadlock when stderr is redirected to a pipe,
191234
# so we redirect it to a temporary file instead. This is not
192235
# necessary anymore as of Inkscape 0.92.1.
193-
self._stderr = TemporaryFile()
236+
stderr = TemporaryFile()
194237
self._proc = subprocess.Popen(
195238
[str("inkscape"), "--without-gui", "--shell"],
196239
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
197-
stderr=self._stderr, env=env)
198-
if not self._read_to_prompt():
199-
raise OSError("Failed to start Inkscape")
200-
201-
try:
202-
fsencode = os.fsencode
203-
except AttributeError: # Py2.
204-
def fsencode(s):
205-
return s.encode(sys.getfilesystemencoding())
240+
stderr=stderr, env=env)
241+
# Slight abuse, but makes shutdown handling easier.
242+
self._proc.stderr = stderr
243+
try:
244+
self._read_until(b"\n>")
245+
except _ConverterError:
246+
raise OSError("Failed to start Inkscape in interactive mode")
206247

207248
# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
208249
# behavior across platforms, so we can just use `shlex.quote`.
209-
orig_b, dest_b = map(_shlex_quote_bytes, map(fsencode, [orig, dest]))
250+
orig_b, dest_b = map(_shlex_quote_bytes,
251+
map(os.fsencode, [orig, dest]))
210252
if b"\n" in orig_b or b"\n" in dest_b:
211253
# Who knows whether the current folder name has a newline, or if
212254
# our encoding is even ASCII compatible... Just fall back on the
@@ -216,35 +258,22 @@ def fsencode(s):
216258
str('inkscape'), '-z', old, '--export-png', new])(orig, dest)
217259
self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n")
218260
self._proc.stdin.flush()
219-
if not self._read_to_prompt():
220-
# Inkscape's output is not localized but gtk's is, so the
221-
# output stream probably has a mixed encoding. Using
222-
# `getfilesystemencoding` should at least get the filenames
223-
# right...
261+
try:
262+
self._read_until(b"\n>")
263+
except _ConverterError:
264+
# Inkscape's output is not localized but gtk's is, so the output
265+
# stream probably has a mixed encoding. Using the filesystem
266+
# encoding should at least get the filenames right...
224267
self._stderr.seek(0)
225268
raise ImageComparisonFailure(
226269
self._stderr.read().decode(
227270
sys.getfilesystemencoding(), "replace"))
228271

229-
def __del__(self):
230-
if self._proc:
231-
if self._proc.poll() is None: # Not exited yet.
232-
self._proc.communicate(b"quit\n")
233-
self._proc.wait()
234-
self._proc.stdin.close()
235-
self._proc.stdout.close()
236-
self._stderr.close()
237-
238272

239273
def _update_converter():
240274
gs, gs_v = matplotlib.checkdep_ghostscript()
241275
if gs_v is not None:
242-
def cmd(old, new):
243-
return [str(gs), '-q', '-sDEVICE=png16m', '-dNOPAUSE', '-dBATCH',
244-
'-sOutputFile=' + new, old]
245-
converter['pdf'] = make_external_conversion_command(cmd)
246-
converter['eps'] = make_external_conversion_command(cmd)
247-
276+
converter['pdf'] = converter['eps'] = _GSConverter()
248277
if matplotlib.checkdep_inkscape() is not None:
249278
converter['svg'] = _SVGConverter()
250279

0 commit comments

Comments
 (0)