summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/utils.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--share/extensions/inkex/utils.py271
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