summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/command.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/inkex/command.py')
-rw-r--r--share/extensions/inkex/command.py222
1 files changed, 222 insertions, 0 deletions
diff --git a/share/extensions/inkex/command.py b/share/extensions/inkex/command.py
new file mode 100644
index 0000000..0377002
--- /dev/null
+++ b/share/extensions/inkex/command.py
@@ -0,0 +1,222 @@
+# coding=utf-8
+#
+# Copyright (C) 2019 Martin Owens
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA.
+#
+"""
+This API provides methods for calling Inkscape to execute a given
+Inkscape command. This may be needed for various compiling options
+(e.g., png), running other extensions or performing other options only
+available via the shell API.
+
+Best practice is to avoid using this API except when absolutely necessary,
+since it is resource-intensive to invoke a new Inkscape instance.
+
+However, in any circumstance when it is necessary to call Inkscape, it
+is strongly recommended that you do so through this API, rather than calling
+it yourself, to take advantage of the security settings and testing functions.
+
+"""
+
+import os
+from subprocess import Popen, PIPE
+from lxml.etree import ElementTree
+
+from .utils import TemporaryDirectory, PY3
+from .elements import SvgDocumentElement
+
+INKSCAPE_EXECUTABLE_NAME = os.environ.get('INKSCAPE_COMMAND', 'inkscape')
+
+class CommandNotFound(IOError):
+ """Command is not found"""
+ pass
+
+class ProgramRunError(ValueError):
+ """Command returned non-zero output"""
+ pass
+
+def which(program):
+ """
+ Attempt different methods of trying to find if the program exists.
+ """
+ if os.path.isabs(program) and os.path.isfile(program):
+ return program
+ try:
+ # Python2 and python3, but must have distutils and may not always
+ # work on windows versions (depending on the version)
+ from distutils.spawn import find_executable
+ prog = find_executable(program)
+ if prog:
+ return prog
+ except ImportError:
+ pass
+
+ try:
+ # Python3 only version of which
+ from shutil import which as warlock
+ prog = warlock(program)
+ if prog:
+ return prog
+ except ImportError:
+ pass # python2
+
+ # There may be other methods for doing a `which` command for other
+ # operating systems; These should go here as they are discovered.
+
+ raise CommandNotFound("Can not find the command: '{}'".format(program))
+
+def write_svg(svg, *filename):
+ """Writes an svg to the given filename"""
+ filename = os.path.join(*filename)
+ if os.path.isfile(filename):
+ return filename
+ with open(filename, 'wb') as fhl:
+ if isinstance(svg, SvgDocumentElement):
+ svg = ElementTree(svg)
+ if hasattr(svg, 'write'):
+ # XML document
+ svg.write(fhl)
+ elif isinstance(svg, bytes):
+ fhl.write(svg)
+ else:
+ raise ValueError("Not sure what type of SVG data this is.")
+ return filename
+
+
+def to_arg(arg, oldie=False):
+ """Convert a python argument to a command line argument"""
+ if isinstance(arg, (tuple, list)):
+ (arg, val) = arg
+ arg = '-' + arg
+ if len(arg) > 2 and not oldie:
+ arg = '-' + arg
+ if val is True:
+ return arg
+ if val is False:
+ return None
+ return '{}={}'.format(arg, str(val))
+ return str(arg)
+
+def to_args(prog, *positionals, **arguments):
+ """
+ Convert positional arguments and key word arguments
+ into a list of strings which Popen will understand.
+
+ Values can be:
+
+ args = *[
+ 'strait_up_string',
+ '--or_manual_kwarg=1',
+ ('ordered list', 'version of kwargs (as below)'),
+ ...
+ ]
+ kwargs = **{
+ 'name': 'val', # --name="val"'
+ 'name': ['foo', 'bar'], # --name=foo --name=bar
+ 'name': True, # --name
+ 'n': 'v', # -n=v
+ 'n': True, # -n
+ }
+
+ All args appear after the kwargs, so if you need args before,
+ use the ordered list tuple and don't use kwargs.
+ """
+ args = [prog]
+ oldie = arguments.pop('oldie', False)
+ for arg, value in arguments.items():
+ arg = arg.replace('_', '-').strip()
+
+ if isinstance(value, tuple):
+ value = list(value)
+ elif not isinstance(value, list):
+ value = [value]
+
+ for val in value:
+ args.append(to_arg((arg, val), oldie))
+
+ args += [to_arg(pos, oldie) for pos in positionals if pos is not None]
+ # Filter out empty non-arguments
+ return [arg for arg in args if arg is not None]
+
+def _call(program, *args, **kwargs):
+ stdin = kwargs.pop('stdin', None)
+ if PY3 and isinstance(stdin, str):
+ stdin = stdin.encode('utf-8')
+ inpipe = PIPE if stdin else None
+
+ args = to_args(which(program), *args, **kwargs)
+ process = Popen(
+ args,
+ shell=False, # Never have shell=True
+ stdin=inpipe, # StdIn not used (yet)
+ stdout=PIPE, # Grab any output (return it)
+ stderr=PIPE, # Take all errors, just incase
+ )
+ (stdout, stderr) = process.communicate(input=stdin)
+ if process.returncode == 0:
+ return stdout
+ raise ProgramRunError("Return Code: {}: {}\n{}\nargs: {}".format(
+ process.returncode, stderr, stdout, args))
+
+def call(program, *args, **kwargs):
+ """
+ Generic caller to open any program and return its stdout.
+
+ stdout = call('executable', arg1, arg2, dash_dash_arg='foo', d=True, ...)
+
+ Will raise ProgramRunError() if return code is not 0.
+ """
+ return _call(program, *args, **kwargs)
+
+def inkscape(svg_file, *args, **kwargs):
+ """
+ Call Inkscape with the given svg_file and the given arguments
+ """
+ return call(INKSCAPE_EXECUTABLE_NAME, svg_file, *args, **kwargs)
+
+def inkscape_command(svg, select=None, verbs=()):
+ """
+ Executes a list of commands, a mixture of verbs, selects etc.
+
+ inkscape_command('<svg...>', ('verb', 'VerbName'), ...)
+ """
+ with TemporaryDirectory(prefix='inkscape-command') as dirname:
+ svg_file = write_svg(svg, dirname, 'input.svg')
+ select = ('select', select) if select else None
+ verbs += ('FileSave', 'FileQuit')
+ inkscape(svg_file, select, batch_process=True, verb=';'.join(verbs))
+ with open(svg_file, 'rb') as fhl:
+ return fhl.read()
+
+def take_snapshot(svg, dirname, name='snapshot', ext='png', dpi=96, **kwargs):
+ """
+ Take a snapshot of the given svg file.
+
+ Resulting filename is yielded back, after generator finishes, the
+ file is deleted so you must deal with the file inside the for loop.
+ """
+ svg_file = write_svg(svg, dirname, name + '.svg')
+ ext_file = os.path.join(dirname, name + '.' + str(ext).lower())
+ inkscape(svg_file, export_dpi=dpi, export_filename=ext_file, export_type=ext, **kwargs)
+ return ext_file
+
+
+def is_inkscape_available():
+ """Return true if the Inkscape executable is available."""
+ try:
+ return bool(which(INKSCAPE_EXECUTABLE_NAME))
+ except CommandNotFound:
+ return False