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 5b0360d

Browse filesBrowse files
committed
Batch ghostscript converter.
1 parent af4830e commit 5b0360d
Copy full SHA for 5b0360d

File tree

Expand file treeCollapse file tree

2 files changed

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

2 files changed

+87
-62
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
@@ -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

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