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

Skip to content

Commit 5b0360d

Browse files
committed
Batch ghostscript converter.
1 parent af4830e commit 5b0360d

File tree

2 files changed

+87
-62
lines changed

2 files changed

+87
-62
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: 86 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
Provides a collection of utilities for comparing (image) results.
33
44
"""
5-
from __future__ import absolute_import, division, print_function
6-
7-
import six
85

96
import atexit
107
import functools
118
import hashlib
12-
import itertools
139
import os
1410
from pathlib import Path
1511
import re
@@ -141,38 +137,81 @@ def _shlex_quote_bytes(b):
141137
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")
142138

143139

144-
class _SVGConverter(object):
140+
class _ConverterError(Exception):
141+
pass
142+
143+
144+
class _Converter(object):
145145
def __init__(self):
146146
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.
147+
# Explicitly register deletion from an atexit handler because if we
148+
# wait until the object is GC'd (which occurs later), then some module
149+
# globals (e.g. signal.SIGKILL) has already been set to None, and
150+
# kill() doesn't work anymore...
152151
atexit.register(self.__del__)
153152

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`.
153+
def __del__(self):
154+
if self._proc:
155+
self._proc.kill()
156+
self._proc.wait()
157+
for stream in filter(None, [self._proc.stdin,
158+
self._proc.stdout,
159+
self._proc.stderr]):
160+
stream.close()
161+
self._proc = None
162+
163+
def _read_until(self, terminator):
164+
"""Read until the prompt is reached."""
165+
buf = bytearray()
163166
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
167+
c = self._proc.stdout.read(1)
168+
if not c:
169+
raise _ConverterError
170+
buf.extend(c)
171+
if buf.endswith(terminator):
172+
return bytes(buf[-len(terminator):])
175173

174+
175+
class _GSConverter(_Converter):
176+
def __call__(self, orig, dest):
177+
if not self._proc:
178+
self._stdout = TemporaryFile()
179+
self._proc = subprocess.Popen(
180+
[matplotlib.checkdep_ghostscript.executable,
181+
"-dNOPAUSE", "-sDEVICE=png16m"],
182+
# As far as I can see, ghostscript never outputs to stderr.
183+
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
184+
try:
185+
self._read_until(b"\nGS")
186+
except _ConverterError:
187+
raise OSError("Failed to start Ghostscript")
188+
189+
def encode_and_escape(name):
190+
return (os.fsencode(name)
191+
.replace(b"(", br"\(")
192+
.replace(b")", br"\)")
193+
.replace(b"\\", b"\\\\"))
194+
195+
self._proc.stdin.write(
196+
b"<< /OutputFile ("
197+
+ encode_and_escape(dest)
198+
+ b") >> setpagedevice ("
199+
+ encode_and_escape(orig)
200+
+ b") run flush\n")
201+
self._proc.stdin.flush()
202+
# GS> if nothing left on the stack; GS<n> if n items left on the stack.
203+
err = self._read_until(b"GS")
204+
stack = self._read_until(b">")
205+
if stack or not os.path.exists(dest):
206+
stack_size = int(stack[1:]) if stack else 0
207+
self._proc.stdin.write(b"pop\n" * stack_size)
208+
# Using the systemencoding should at least get the filenames right.
209+
raise ImageComparisonFailure(
210+
(err + b"GS" + stack + b">")
211+
.decode(sys.getfilesystemencoding(), "replace"))
212+
213+
214+
class _SVGConverter(_Converter):
176215
def __call__(self, orig, dest):
177216
if (not self._proc # First run.
178217
or self._proc.poll() is not None): # Inkscape terminated.
@@ -190,23 +229,22 @@ def __call__(self, orig, dest):
190229
# seem to sometimes deadlock when stderr is redirected to a pipe,
191230
# so we redirect it to a temporary file instead. This is not
192231
# necessary anymore as of Inkscape 0.92.1.
193-
self._stderr = TemporaryFile()
232+
stderr = TemporaryFile()
194233
self._proc = subprocess.Popen(
195234
[str("inkscape"), "--without-gui", "--shell"],
196235
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())
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")
206243

207244
# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
208245
# behavior across platforms, so we can just use `shlex.quote`.
209-
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]))
210248
if b"\n" in orig_b or b"\n" in dest_b:
211249
# Who knows whether the current folder name has a newline, or if
212250
# our encoding is even ASCII compatible... Just fall back on the
@@ -216,35 +254,22 @@ def fsencode(s):
216254
str('inkscape'), '-z', old, '--export-png', new])(orig, dest)
217255
self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n")
218256
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...
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...
224263
self._stderr.seek(0)
225264
raise ImageComparisonFailure(
226265
self._stderr.read().decode(
227266
sys.getfilesystemencoding(), "replace"))
228267

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

239269
def _update_converter():
240270
gs, gs_v = matplotlib.checkdep_ghostscript()
241271
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-
272+
converter['pdf'] = converter['eps'] = _GSConverter()
248273
if matplotlib.checkdep_inkscape() is not None:
249274
converter['svg'] = _SVGConverter()
250275

0 commit comments

Comments
 (0)