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.py309
1 files changed, 309 insertions, 0 deletions
diff --git a/share/extensions/inkex/command.py b/share/extensions/inkex/command.py
new file mode 100644
index 0000000..8f9abbc
--- /dev/null
+++ b/share/extensions/inkex/command.py
@@ -0,0 +1,309 @@
+# 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
+import sys
+from shutil import which as warlock
+
+from subprocess import Popen, PIPE
+from tempfile import TemporaryDirectory
+from typing import List
+from lxml.etree import ElementTree
+
+from .elements import SvgDocumentElement
+
+INKSCAPE_EXECUTABLE_NAME = os.environ.get("INKSCAPE_COMMAND")
+if INKSCAPE_EXECUTABLE_NAME is None:
+ if sys.platform == "win32":
+ # prefer inkscape.exe over inkscape.com which spawns a command window
+ INKSCAPE_EXECUTABLE_NAME = "inkscape.exe"
+ else:
+ INKSCAPE_EXECUTABLE_NAME = "inkscape"
+
+
+class CommandNotFound(IOError):
+ """Command is not found"""
+
+
+class ProgramRunError(ValueError):
+ """A specialized ValueError that is raised when a call to an external command fails.
+ It stores additional information about a failed call to an external program.
+
+ If only the ``program`` parameter is given, it is interpreted as the error message.
+ Otherwise, the error message is compiled from all constructor parameters."""
+
+ program: str
+ """The absolute path to the called executable"""
+
+ returncode: int
+ """Return code of the program call"""
+
+ stderr: str
+ """stderr stream output of the call"""
+
+ stdout: str
+ """stdout stream output of the call"""
+
+ arguments: List
+ """Arguments of the call"""
+
+ def __init__(self, program, returncode=None, stderr=None, stdout=None, args=None):
+ self.program = program
+ self.returncode = returncode
+ self.stderr = stderr
+ self.stdout = stdout
+ self.arguments = args
+ super().__init__(str(self))
+
+ def __str__(self):
+ if self.returncode is None:
+ return self.program
+ return (
+ f"Return Code: {self.returncode}: {self.stderr}\n{self.stdout}"
+ f"\nargs: {self.args}"
+ )
+
+
+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
+ # On Windows, shutil.which may give preference to .py files in the current directory
+ # (such as pdflatex.py), e.g. if .PY is in pathext, because the current directory is
+ # prepended to PATH. This can be suppressed by explicitly appending the current
+ # directory.
+
+ try:
+ if sys.platform == "win32":
+ prog = warlock(program, path=os.environ["PATH"] + ";" + os.curdir)
+ if prog:
+ return prog
+ except ImportError:
+ pass
+
+ try:
+ # Python3 only version of which
+ 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(f"Can not find the command: '{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 f"{arg}={str(val)}"
+ return str(arg)
+
+
+def to_args(prog, *positionals, **arguments):
+ """Compile arguments and keyword arguments into a list of strings which Popen will
+ understand.
+
+ :param prog:
+ Program executable prepended to the output.
+ :type first: ``str``
+
+ :Arguments:
+ * (``str``) -- String added as given
+ * (``tuple``) -- Ordered version of Kwyward Arguments, see below
+
+ :Keyword Arguments:
+ * *name* (``str``) --
+ Becomes ``--name="val"``
+ * *name* (``bool``) --
+ Becomes ``--name``
+ * *name* (``list``) --
+ Becomes ``--name="val1"`` ...
+ * *n* (``str``) --
+ Becomes ``-n=val``
+ * *n* (``bool``) --
+ Becomes ``-n``
+
+ :return: Returns a list of compiled arguments ready for Popen.
+ :rtype: ``list[str]``
+ """
+ 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 to_args_sorted(prog, *positionals, **arguments):
+ """same as :func:`to_args`, but keyword arguments are sorted beforehand
+
+ .. versionadded:: 1.2"""
+ return to_args(prog, *positionals, **dict(sorted(arguments.items())))
+
+
+def _call(program, *args, **kwargs):
+ stdin = kwargs.pop("stdin", None)
+ if isinstance(stdin, str):
+ stdin = stdin.encode("utf-8")
+ inpipe = PIPE if stdin else None
+
+ args = to_args(which(program), *args, **kwargs)
+
+ kwargs = {}
+ if sys.platform == "win32":
+ kwargs["creationflags"] = 0x08000000 # create no console window
+
+ with 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
+ **kwargs,
+ ) as process:
+ (stdout, stderr) = process.communicate(input=stdin)
+ if process.returncode == 0:
+ return stdout
+ raise ProgramRunError(program, 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 :class:`ProgramRunError` if return code is not 0.
+
+ Keyword arguments:
+ return_binary: Should stdout return raw bytes (default: False)
+
+ .. versionadded:: 1.1
+ stdin: The string or bytes containing the stdin (default: None)
+
+ All other arguments converted using :func:`to_args` function.
+ """
+ # We use this long input because it's less likely to conflict with --binary=
+ binary = kwargs.pop("return_binary", False)
+ stdout = _call(program, *args, **kwargs)
+ # Convert binary to string when we wish to have strings we do this here
+ # so the mock tests will also run the conversion (always returns bytes)
+ if not binary and isinstance(stdout, bytes):
+ return stdout.decode(sys.stdout.encoding or "utf-8")
+ return stdout
+
+
+def inkscape(svg_file, *args, **kwargs):
+ """
+ Call Inkscape with the given svg_file and the given arguments, see call()
+ """
+ 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 tmpdir:
+ svg_file = write_svg(svg, tmpdir, "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