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

Skip to content

Commit 8a244e4

Browse files
authored
Merge pull request #9454 from anntzer/gsbatch
Batch ghostscript converter.
2 parents b01bdf4 + 2a99a0a commit 8a244e4

File tree

2 files changed

+87
-59
lines changed

2 files changed

+87
-59
lines changed

lib/matplotlib/__init__.py

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

lib/matplotlib/testing/compare.py

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import atexit
77
import functools
88
import hashlib
9-
import itertools
109
import os
1110
from pathlib import Path
1211
import re
@@ -139,38 +138,81 @@ def _shlex_quote_bytes(b):
139138
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")
140139

141140

142-
class _SVGConverter(object):
141+
class _ConverterError(Exception):
142+
pass
143+
144+
145+
class _Converter(object):
143146
def __init__(self):
144147
self._proc = None
145-
# We cannot rely on the GC to trigger `__del__` at exit because
146-
# other modules (e.g. `subprocess`) may already have their globals
147-
# set to `None`, which make `proc.communicate` or `proc.terminate`
148-
# fail. By relying on `atexit` we ensure the destructor runs before
149-
# `None`-setting occurs.
148+
# Explicitly register deletion from an atexit handler because if we
149+
# wait until the object is GC'd (which occurs later), then some module
150+
# globals (e.g. signal.SIGKILL) has already been set to None, and
151+
# kill() doesn't work anymore...
150152
atexit.register(self.__del__)
151153

152-
def _read_to_prompt(self):
153-
"""Did Inkscape reach the prompt without crashing?
154-
"""
155-
stream = iter(functools.partial(self._proc.stdout.read, 1), b"")
156-
prompt = (b"\n", b">")
157-
n = len(prompt)
158-
its = itertools.tee(stream, n)
159-
for i, it in enumerate(its):
160-
next(itertools.islice(it, i, i), None) # Advance `it` by `i`.
154+
def __del__(self):
155+
if self._proc:
156+
self._proc.kill()
157+
self._proc.wait()
158+
for stream in filter(None, [self._proc.stdin,
159+
self._proc.stdout,
160+
self._proc.stderr]):
161+
stream.close()
162+
self._proc = None
163+
164+
def _read_until(self, terminator):
165+
"""Read until the prompt is reached."""
166+
buf = bytearray()
161167
while True:
162-
window = tuple(map(next, its))
163-
if len(window) != n:
164-
# Ran out of data -- one of the `next(it)` raised
165-
# StopIteration, so the tuple is shorter.
166-
return False
167-
if self._proc.poll() is not None:
168-
# Inkscape exited.
169-
return False
170-
if window == prompt:
171-
# Successfully read until prompt.
172-
return True
168+
c = self._proc.stdout.read(1)
169+
if not c:
170+
raise _ConverterError
171+
buf.extend(c)
172+
if buf.endswith(terminator):
173+
return bytes(buf[:-len(terminator)])
174+
175+
176+
class _GSConverter(_Converter):
177+
def __call__(self, orig, dest):
178+
if not self._proc:
179+
self._stdout = TemporaryFile()
180+
self._proc = subprocess.Popen(
181+
[matplotlib.checkdep_ghostscript.executable,
182+
"-dNOPAUSE", "-sDEVICE=png16m"],
183+
# As far as I can see, ghostscript never outputs to stderr.
184+
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
185+
try:
186+
self._read_until(b"\nGS")
187+
except _ConverterError:
188+
raise OSError("Failed to start Ghostscript")
189+
190+
def encode_and_escape(name):
191+
return (os.fsencode(name)
192+
.replace(b"\\", b"\\\\")
193+
.replace(b"(", br"\(")
194+
.replace(b")", br"\)"))
195+
196+
self._proc.stdin.write(
197+
b"<< /OutputFile ("
198+
+ encode_and_escape(dest)
199+
+ b") >> setpagedevice ("
200+
+ encode_and_escape(orig)
201+
+ b") run flush\n")
202+
self._proc.stdin.flush()
203+
# GS> if nothing left on the stack; GS<n> if n items left on the stack.
204+
err = self._read_until(b"GS")
205+
stack = self._read_until(b">")
206+
if stack or not os.path.exists(dest):
207+
stack_size = int(stack[1:]) if stack else 0
208+
self._proc.stdin.write(b"pop\n" * stack_size)
209+
# Using the systemencoding should at least get the filenames right.
210+
raise ImageComparisonFailure(
211+
(err + b"GS" + stack + b">")
212+
.decode(sys.getfilesystemencoding(), "replace"))
213+
173214

215+
class _SVGConverter(_Converter):
174216
def __call__(self, orig, dest):
175217
if (not self._proc # First run.
176218
or self._proc.poll() is not None): # Inkscape terminated.
@@ -187,23 +229,22 @@ def __call__(self, orig, dest):
187229
# seem to sometimes deadlock when stderr is redirected to a pipe,
188230
# so we redirect it to a temporary file instead. This is not
189231
# necessary anymore as of Inkscape 0.92.1.
190-
self._stderr = TemporaryFile()
232+
stderr = TemporaryFile()
191233
self._proc = subprocess.Popen(
192234
["inkscape", "--without-gui", "--shell"],
193235
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
194-
stderr=self._stderr, env=env)
195-
if not self._read_to_prompt():
196-
raise OSError("Failed to start Inkscape")
197-
198-
try:
199-
fsencode = os.fsencode
200-
except AttributeError: # Py2.
201-
def fsencode(s):
202-
return s.encode(sys.getfilesystemencoding())
236+
stderr=stderr, env=env)
237+
# Slight abuse, but makes shutdown handling easier.
238+
self._proc.stderr = stderr
239+
try:
240+
self._read_until(b"\n>")
241+
except _ConverterError:
242+
raise OSError("Failed to start Inkscape in interactive mode")
203243

204244
# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
205245
# behavior across platforms, so we can just use `shlex.quote`.
206-
orig_b, dest_b = map(_shlex_quote_bytes, map(fsencode, [orig, dest]))
246+
orig_b, dest_b = map(_shlex_quote_bytes,
247+
map(os.fsencode, [orig, dest]))
207248
if b"\n" in orig_b or b"\n" in dest_b:
208249
# Who knows whether the current folder name has a newline, or if
209250
# our encoding is even ASCII compatible... Just fall back on the
@@ -213,35 +254,22 @@ def fsencode(s):
213254
'inkscape', '-z', old, '--export-png', new])(orig, dest)
214255
self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n")
215256
self._proc.stdin.flush()
216-
if not self._read_to_prompt():
217-
# Inkscape's output is not localized but gtk's is, so the
218-
# output stream probably has a mixed encoding. Using
219-
# `getfilesystemencoding` should at least get the filenames
220-
# right...
257+
try:
258+
self._read_until(b"\n>")
259+
except _ConverterError:
260+
# Inkscape's output is not localized but gtk's is, so the output
261+
# stream probably has a mixed encoding. Using the filesystem
262+
# encoding should at least get the filenames right...
221263
self._stderr.seek(0)
222264
raise ImageComparisonFailure(
223265
self._stderr.read().decode(
224266
sys.getfilesystemencoding(), "replace"))
225267

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

236269
def _update_converter():
237270
gs, gs_v = matplotlib.checkdep_ghostscript()
238271
if gs_v is not None:
239-
def cmd(old, new):
240-
return [str(gs), '-q', '-sDEVICE=png16m', '-dNOPAUSE', '-dBATCH',
241-
'-sOutputFile=' + new, old]
242-
converter['pdf'] = make_external_conversion_command(cmd)
243-
converter['eps'] = make_external_conversion_command(cmd)
244-
272+
converter['pdf'] = converter['eps'] = _GSConverter()
245273
if matplotlib.checkdep_inkscape() is not None:
246274
converter['svg'] = _SVGConverter()
247275

0 commit comments

Comments
 (0)