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 8a244e4

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

File tree

Expand file treeCollapse file tree

2 files changed

+87
-59
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+87
-59
lines changed

‎lib/matplotlib/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/__init__.py
+1-1Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/compare.py
+86-58Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.