summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/utils.py
blob: 2d1dd94a90372c2c6a203adae616640a48dd61fd (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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# coding=utf-8
#
# Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
#
# 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-1301, USA.
#
"""
Basic common utility functions for calculated things
"""
import os
import sys
import random
import re
import math
from argparse import ArgumentTypeError
from itertools import tee

ABORT_STATUS = -5

(X, Y) = range(2)
PY3 = sys.version_info[0] == 3

# pylint: disable=line-too-long
# Taken from https://www.w3.org/Graphics/SVG/1.1/paths.html#PathDataBNF
DIGIT_REX_PART = r"[0-9]"
DIGIT_SEQUENCE_REX_PART = rf"(?:{DIGIT_REX_PART}+)"
INTEGER_CONSTANT_REX_PART = DIGIT_SEQUENCE_REX_PART
SIGN_REX_PART = r"[+-]"
EXPONENT_REX_PART = rf"(?:[eE]{SIGN_REX_PART}?{DIGIT_SEQUENCE_REX_PART})"
FRACTIONAL_CONSTANT_REX_PART = rf"(?:{DIGIT_SEQUENCE_REX_PART}?\.{DIGIT_SEQUENCE_REX_PART}|{DIGIT_SEQUENCE_REX_PART}\.)"
FLOATING_POINT_CONSTANT_REX_PART = rf"(?:{FRACTIONAL_CONSTANT_REX_PART}{EXPONENT_REX_PART}?|{DIGIT_SEQUENCE_REX_PART}{EXPONENT_REX_PART})"
NUMBER_REX = re.compile(
    rf"(?:{SIGN_REX_PART}?{FLOATING_POINT_CONSTANT_REX_PART}|{SIGN_REX_PART}?{INTEGER_CONSTANT_REX_PART})"
)
# pylint: enable=line-too-long


def _pythonpath():
    for pth in os.environ.get("PYTHONPATH", "").split(":"):
        if os.path.isdir(pth):
            yield pth


def get_user_directory():
    """Return the user directory where extensions are stored.

    .. versionadded:: 1.1"""
    if "INKSCAPE_PROFILE_DIR" in os.environ:
        return os.path.abspath(
            os.path.expanduser(
                os.path.join(os.environ["INKSCAPE_PROFILE_DIR"], "extensions")
            )
        )

    home = os.path.expanduser("~")
    for pth in _pythonpath():
        if pth.startswith(home):
            return pth
    return None


def get_inkscape_directory():
    """Return the system directory where inkscape's core is.

    .. versionadded:: 1.1"""
    for pth in _pythonpath():
        if os.path.isdir(os.path.join(pth, "inkex")):
            return pth
    raise ValueError("Unable to determine the location of Inkscape")


class KeyDict(dict):
    """
    A normal dictionary, except asking for anything not in the dictionary
    always returns the key itself. This is used for translation dictionaries.
    """

    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            return key


def parse_percent(val: str):
    """Parse strings that are either values (i.e., '3.14159') or percentages
    (i.e. '75%') to a float.

    .. versionadded:: 1.2"""
    val = val.strip()
    if val.endswith("%"):
        return float(val[:-1]) / 100
    return float(val)


def Boolean(value):
    """ArgParser function to turn a boolean string into a python boolean"""
    if value.upper() == "TRUE":
        return True
    if value.upper() == "FALSE":
        return False
    return None


def to_bytes(content):
    """Ensures the content is bytes

    .. versionadded:: 1.1"""
    if isinstance(content, bytes):
        return content
    return str(content).encode("utf8")


def debug(what):
    """Print debug message if debugging is switched on"""
    errormsg(what)
    return what


def do_nothing(*args, **kwargs):  # pylint: disable=unused-argument
    """A blank function to do nothing

    .. versionadded:: 1.1"""


def errormsg(msg):
    """Intended for end-user-visible error messages.

    (Currently just writes to stderr with an appended newline, but could do
    something better in future: e.g. could add markup to distinguish error
    messages from status messages or debugging output.)

    Note that this should always be combined with translation::

      import inkex
      ...
      inkex.errormsg(_("This extension requires two selected paths."))
    """
    try:
        sys.stderr.write(msg)
    except TypeError:
        sys.stderr.write(str(msg))
    except UnicodeEncodeError:
        # Python 2:
        # Fallback for cases where sys.stderr.encoding is not Unicode.
        # Python 3:
        # This will not work as write() does not accept byte strings, but AFAIK
        # we should never reach this point as the default error handler is
        # 'backslashreplace'.

        # This will be None by default if stderr is piped, so use ASCII as a
        # last resort.
        encoding = sys.stderr.encoding or "ascii"
        sys.stderr.write(msg.encode(encoding, "backslashreplace"))

    # Write '\n' separately to avoid dealing with different string types.
    sys.stderr.write("\n")


class AbortExtension(Exception):
    """Raised to print a message to the user without backtrace"""


class DependencyError(NotImplementedError):
    """Raised when we need an external python module that isn't available"""


class FragmentError(Exception):
    """Raised when trying to do rooty things on an xml fragment"""


def to(kind):  # pylint: disable=invalid-name
    """
    Decorator which will turn a generator into a list, tuple or other object type.
    """

    def _inner(call):
        def _outer(*args, **kw):
            return kind(call(*args, **kw))

        return _outer

    return _inner


def strargs(string, kind=float):
    """Returns a list of floats from a string

    .. versionchanged:: 1.1
        also splits at -(minus) signs by adding a space in front of the - sign

    .. versionchanged:: 1.2
        Full support for the `SVG Path data BNF
        <https://www.w3.org/Graphics/SVG/1.1/paths.html#PathDataBNF>`_
    """
    return [kind(val) for val in NUMBER_REX.findall(string)]


class classproperty:  # pylint: disable=invalid-name, too-few-public-methods
    """Combine classmethod and property decorators"""

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, owner):
        return self.func(owner)


def filename_arg(name):
    """Existing file to read or option used in script arguments"""
    filename = os.path.abspath(os.path.expanduser(name))
    if not os.path.isfile(filename):
        raise ArgumentTypeError(f"File not found: {name}")
    return filename


def pairwise(iterable, start=True):
    "Iterate over a list with overlapping pairs (see itertools recipes)"
    first, then = tee(iterable)
    starter = [(None, next(then, None))]
    if not start:
        starter = []
    return starter + list(zip(first, then))


EVAL_GLOBALS = {}
EVAL_GLOBALS.update(random.__dict__)
EVAL_GLOBALS.update(math.__dict__)


def math_eval(function, variable="x"):
    """Interpret a function string. All functions from math and random may be used.

    .. versionadded:: 1.1

    Returns:
        a lambda expression if sucessful; otherwise None.
    """
    try:
        if function != "":
            return eval(
                f"lambda {variable}: " + (function.strip('"') or "t"), EVAL_GLOBALS, {}
            )
    # handle incomplete/invalid function gracefully
    except SyntaxError:
        pass
    return None


def is_number(string):
    """Checks if a value is a number

    .. versionadded:: 1.2"""
    try:
        float(string)
        return True
    except ValueError:
        return False