summaryrefslogtreecommitdiff
path: root/lib/extensions/utils/inkex_command.py
blob: 20cfc0a5899a77556e90c0622ca981d950edf086 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# A fix to call inkscape commands for Linux when packaged with pyinstaller
# Code taken from inkex/command.py

import os
import re
import sys
from shutil import which as warlock
from subprocess import PIPE, Popen

from inkex.command import CommandNotFound, ProgramRunError

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"


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 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 Keyword 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 _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

    '''
    This is the fix for the linux build
    Setup Linux environment variable
    '''
    if sys.platform == "linux":
        env = dict(os.environ)  # make a copy of the environment
        lp_key = 'LD_LIBRARY_PATH'  # for GNU/Linux and *BSD.
        lp_orig = env.get(lp_key + '_ORIG')
        if lp_orig is not None:
            env[lp_key] = lp_orig  # restore the original, unmodified value
        else:
            # This happens when LD_LIBRARY_PATH was not set.
            # Remove the env var as a last resort:
            env.pop(lp_key, None)
        with Popen(
            args,
            env=env,
            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)
    else:
        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().

    Returns the stdout of the call.

    .. versionchanged:: 1.3
        If the "actions" kwargs parameter is passed, it is checked whether the length of
        the action string might lead to issues with the Windows CLI call character
        limit. In this case, Inkscape is called in `--shell`
        mode and the actions are fed in via stdin. This avoids violating the character
        limit for command line arguments on Windows, which results in errors like this:
        `[WinError 206] The filename or extension is too long`.
        This workaround is also possible when calling Inkscape with long arguments
        to `--export-id` and `--query-id`, by converting the call to the appropriate
        action sequence. The stdout is cleaned to resemble non-interactive mode.
    """
    os.environ["SELF_CALL"] = "true"  # needed for inkscape versions 1.3 and 1.3.1
    actions = kwargs.get("actions", None)
    strip_stdout = False
    # Keep some safe margin to the 8191 character limit.
    if actions is not None and len(actions) > 7000:
        args = args + ("--shell",)
        kwargs["stdin"] = actions
        kwargs.pop("actions")
        strip_stdout = True
    stdout = call(INKSCAPE_EXECUTABLE_NAME, svg_file, *args, **kwargs)
    if strip_stdout:
        split = re.split(r"\n> ", stdout)
        if len(split) > 1:
            if "\n" in split[1]:
                stdout = "\n".join(split[1].split("\n")[1:])
            else:
                stdout = ""
    return stdout