diff options
Diffstat (limited to '')
-rw-r--r-- | share/extensions/inkex/utils.py | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/share/extensions/inkex/utils.py b/share/extensions/inkex/utils.py new file mode 100644 index 0000000..2d1dd94 --- /dev/null +++ b/share/extensions/inkex/utils.py @@ -0,0 +1,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 |