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

Skip to content

Batch ghostscript converter. #9454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def checkdep_ghostscript():
for gs_exec in gs_execs:
try:
s = subprocess.Popen(
[str(gs_exec), '--version'], stdout=subprocess.PIPE,
[gs_exec, '--version'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
if s.returncode == 0:
Expand Down
147 changes: 86 additions & 61 deletions lib/matplotlib/testing/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
Provides a collection of utilities for comparing (image) results.

"""
from __future__ import absolute_import, division, print_function

import six

import atexit
import functools
import hashlib
import itertools
import os
from pathlib import Path
import re
Expand Down Expand Up @@ -141,38 +137,81 @@ def _shlex_quote_bytes(b):
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")


class _SVGConverter(object):
class _ConverterError(Exception):
pass


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

def _read_to_prompt(self):
"""Did Inkscape reach the prompt without crashing?
"""
stream = iter(functools.partial(self._proc.stdout.read, 1), b"")
prompt = (b"\n", b">")
n = len(prompt)
its = itertools.tee(stream, n)
for i, it in enumerate(its):
next(itertools.islice(it, i, i), None) # Advance `it` by `i`.
def __del__(self):
if self._proc:
self._proc.kill()
self._proc.wait()
for stream in filter(None, [self._proc.stdin,
self._proc.stdout,
self._proc.stderr]):
stream.close()
self._proc = None

def _read_until(self, terminator):
"""Read until the prompt is reached."""
buf = bytearray()
while True:
window = tuple(map(next, its))
if len(window) != n:
# Ran out of data -- one of the `next(it)` raised
# StopIteration, so the tuple is shorter.
return False
if self._proc.poll() is not None:
# Inkscape exited.
return False
if window == prompt:
# Successfully read until prompt.
return True
c = self._proc.stdout.read(1)
if not c:
raise _ConverterError
buf.extend(c)
if buf.endswith(terminator):
return bytes(buf[:-len(terminator)])


class _GSConverter(_Converter):
def __call__(self, orig, dest):
if not self._proc:
self._stdout = TemporaryFile()
self._proc = subprocess.Popen(
[matplotlib.checkdep_ghostscript.executable,
"-dNOPAUSE", "-sDEVICE=png16m"],
# As far as I can see, ghostscript never outputs to stderr.
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
try:
self._read_until(b"\nGS")
except _ConverterError:
raise OSError("Failed to start Ghostscript")

def encode_and_escape(name):
return (os.fsencode(name)
.replace(b"\\", b"\\\\")
.replace(b"(", br"\(")
.replace(b")", br"\)"))

self._proc.stdin.write(
b"<< /OutputFile ("
+ encode_and_escape(dest)
+ b") >> setpagedevice ("
+ encode_and_escape(orig)
+ b") run flush\n")
self._proc.stdin.flush()
# GS> if nothing left on the stack; GS<n> if n items left on the stack.
err = self._read_until(b"GS")
stack = self._read_until(b">")
if stack or not os.path.exists(dest):
stack_size = int(stack[1:]) if stack else 0
self._proc.stdin.write(b"pop\n" * stack_size)
# Using the systemencoding should at least get the filenames right.
raise ImageComparisonFailure(
(err + b"GS" + stack + b">")
.decode(sys.getfilesystemencoding(), "replace"))


class _SVGConverter(_Converter):
def __call__(self, orig, dest):
if (not self._proc # First run.
or self._proc.poll() is not None): # Inkscape terminated.
Expand All @@ -190,23 +229,22 @@ def __call__(self, orig, dest):
# seem to sometimes deadlock when stderr is redirected to a pipe,
# so we redirect it to a temporary file instead. This is not
# necessary anymore as of Inkscape 0.92.1.
self._stderr = TemporaryFile()
stderr = TemporaryFile()
self._proc = subprocess.Popen(
[str("inkscape"), "--without-gui", "--shell"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=self._stderr, env=env)
if not self._read_to_prompt():
raise OSError("Failed to start Inkscape")

try:
fsencode = os.fsencode
except AttributeError: # Py2.
def fsencode(s):
return s.encode(sys.getfilesystemencoding())
stderr=stderr, env=env)
# Slight abuse, but makes shutdown handling easier.
self._proc.stderr = stderr
try:
self._read_until(b"\n>")
except _ConverterError:
raise OSError("Failed to start Inkscape in interactive mode")

# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent
# behavior across platforms, so we can just use `shlex.quote`.
orig_b, dest_b = map(_shlex_quote_bytes, map(fsencode, [orig, dest]))
orig_b, dest_b = map(_shlex_quote_bytes,
map(os.fsencode, [orig, dest]))
if b"\n" in orig_b or b"\n" in dest_b:
# Who knows whether the current folder name has a newline, or if
# our encoding is even ASCII compatible... Just fall back on the
Expand All @@ -216,35 +254,22 @@ def fsencode(s):
str('inkscape'), '-z', old, '--export-png', new])(orig, dest)
self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n")
self._proc.stdin.flush()
if not self._read_to_prompt():
# Inkscape's output is not localized but gtk's is, so the
# output stream probably has a mixed encoding. Using
# `getfilesystemencoding` should at least get the filenames
# right...
try:
self._read_until(b"\n>")
except _ConverterError:
# Inkscape's output is not localized but gtk's is, so the output
# stream probably has a mixed encoding. Using the filesystem
# encoding should at least get the filenames right...
self._stderr.seek(0)
raise ImageComparisonFailure(
self._stderr.read().decode(
sys.getfilesystemencoding(), "replace"))

def __del__(self):
if self._proc:
if self._proc.poll() is None: # Not exited yet.
self._proc.communicate(b"quit\n")
self._proc.wait()
self._proc.stdin.close()
self._proc.stdout.close()
self._stderr.close()


def _update_converter():
gs, gs_v = matplotlib.checkdep_ghostscript()
if gs_v is not None:
def cmd(old, new):
return [str(gs), '-q', '-sDEVICE=png16m', '-dNOPAUSE', '-dBATCH',
'-sOutputFile=' + new, old]
converter['pdf'] = make_external_conversion_command(cmd)
converter['eps'] = make_external_conversion_command(cmd)

converter['pdf'] = converter['eps'] = _GSConverter()
if matplotlib.checkdep_inkscape() is not None:
converter['svg'] = _SVGConverter()

Expand Down