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

Skip to content

Inkscape shell mode. #8248

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
Mar 15, 2017
Merged
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
118 changes: 112 additions & 6 deletions lib/matplotlib/testing/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@

import six

import atexit
import functools
import hashlib
import itertools
import os
import re
import shutil
import sys
from tempfile import TemporaryFile

import numpy as np

Expand All @@ -19,7 +25,6 @@
from matplotlib import _png
from matplotlib import _get_cachedir
from matplotlib import cbook
from distutils import version

__all__ = ['compare_float', 'compare_images', 'comparable_formats']

Expand Down Expand Up @@ -128,6 +133,110 @@ def convert(old, new):
return convert


# Modified from https://bugs.python.org/issue25567.
_find_unsafe_bytes = re.compile(br'[^a-zA-Z0-9_@%+=:,./-]').search


def _shlex_quote_bytes(b):
return (b if _find_unsafe_bytes(b) is None
else b"'" + b.replace(b"'", b"'\"'\"'") + b"'")


class _SVGConverter(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.
atexit.register(self.__del__)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would __del__ not be called a second time when this object is deleted as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but __del__ can safely be executed twice (a file stream can be closed multiple times).


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`.
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

def __call__(self, orig, dest):
if (not self._proc # First run.
or self._proc.poll() is not None): # Inkscape terminated.
env = os.environ.copy()
# If one passes e.g. a png file to Inkscape, it will try to
# query the user for conversion options via a GUI (even with
# `--without-gui`). Unsetting `DISPLAY` prevents this (and causes
# GTK to crash and Inkscape to terminate, but that'll just be
# reported as a regular exception below).
env.pop("DISPLAY", None) # May already be unset.
# Do not load any user options.
# `os.environ` needs native strings on Py2+Windows.
env[str("INKSCAPE_PROFILE_DIR")] = os.devnull
# Old versions of Inkscape (0.48.3.1, used on Travis as of now)
# 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()
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())

# 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]))
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
# slow solution (Inkscape uses `fgets` so it will always stop at a
# newline).
return make_external_conversion_command(lambda old, new: [
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...
self._stderr.seek(0)
raise ImageComparisonFailure(
self._stderr.read().decode(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine as long as you've tried this out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In [1]: from matplotlib.testing.compare import _SVGConverter; _SVGConverter()("/tmp/quux.svg", "/tmp/foo.png")
---------------------------------------------------------------------------
ImageComparisonFailure                    Traceback (most recent call last)
<ipython-input-2-c896ab68b2ce> in <module>()
----> 1 from matplotlib.testing.compare import _SVGConverter; _SVGConverter()("/tmp/quux.svg", "/tmp/foo.png")

/home/antony/src/extern/matplotlib/lib/matplotlib/testing/compare.py in __call__(self, orig, dest)
    225             raise ImageComparisonFailure(
    226                 self._stderr.read().decode(
--> 227                     sys.getfilesystemencoding(), "replace"))
    228 
    229     def __del__(self):

ImageComparisonFailure: ** Message: /dev/null is not a valid directory.
** Message: Inkscape will run with default settings, and new settings will not be saved. 

** (inkscape:25211): WARNING **: Could not create extension error log file '/dev/null/extension-errors.log'

** (inkscape:25211): WARNING **: Can't open file: /tmp/quux.svg (doesn't exist)

** (inkscape:25211): WARNING **: Can't open file: /tmp/quux.svg (doesn't exist)

** (inkscape:25211): WARNING **: Specified document /tmp/quux.svg cannot be opened (does not exist or not a valid SVG file)

The first few warnings should be spurious (due to intentionally setting INKSCAPE_PROFILE_DIR to a nonexistent folder) and I could technically crop them out, but I'd rather have the full info available just in case...

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:
Expand All @@ -138,9 +247,7 @@ def cmd(old, new):
converter['eps'] = make_external_conversion_command(cmd)

if matplotlib.checkdep_inkscape() is not None:
def cmd(old, new):
return [str('inkscape'), '-z', old, '--export-png', new]
converter['svg'] = make_external_conversion_command(cmd)
converter['svg'] = _SVGConverter()


#: A dictionary that maps filename extensions to functions which
Expand Down Expand Up @@ -363,9 +470,8 @@ def save_diff_image(expected, actual, output):
actual, actualImage, expected, expectedImage)
expectedImage = np.array(expectedImage).astype(float)
actualImage = np.array(actualImage).astype(float)
assert expectedImage.ndim == actualImage.ndim
assert expectedImage.shape == actualImage.shape
absDiffImage = abs(expectedImage - actualImage)
absDiffImage = np.abs(expectedImage - actualImage)

# expand differences in luminance domain
absDiffImage *= 255 * 10
Expand Down