diff options
Diffstat (limited to '')
-rw-r--r-- | share/extensions/inkex/utils.py | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/share/extensions/inkex/utils.py b/share/extensions/inkex/utils.py new file mode 100644 index 0000000..dea5308 --- /dev/null +++ b/share/extensions/inkex/utils.py @@ -0,0 +1,290 @@ +# 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 +""" +from __future__ import absolute_import, print_function, unicode_literals + +import re +import os +import sys +import shutil + +from itertools import tee +from collections import defaultdict +from argparse import ArgumentTypeError + +# When python2 support is gone, enable tempfile's version +# from tempfile import TemporaryDirectory + +# All the names that get added to the inkex API itself. +__all__ = ('AbortExtension', 'DependencyError', 'Boolean', 'errormsg', 'addNS', 'NSS') + +ABORT_STATUS = -5 + +(X, Y) = range(2) +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str # pylint: disable=redefined-builtin,invalid-name + +# a dictionary of all of the xmlns prefixes in a standard inkscape doc +NSS = { + 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'cc': 'http://creativecommons.org/ns#', + 'ccOLD': 'http://web.resource.org/cc/', + 'svg': 'http://www.w3.org/2000/svg', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', + 'xlink': 'http://www.w3.org/1999/xlink', + 'xml': 'http://www.w3.org/XML/1998/namespace' +} +SSN = dict((b, a) for (a, b) in NSS.items()) + +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(KeyDict, self).__getitem__(key) + except KeyError: + return key + +class TemporaryDirectory(object): # pylint: disable=too-few-public-methods + """Tiny replacement for python3's version.""" + def __init__(self, suffix="", prefix="tmp"): + self.suffix = suffix + self.prefix = prefix + self.path = None + def __enter__(self): + from tempfile import mkdtemp + self.path = mkdtemp(self.suffix, self.prefix, None) + return self.path + def __exit__(self, exc, value, traceback): + if os.path.isdir(self.path): + shutil.rmtree(self.path) + +def Boolean(value): + """ArgParser function to turn a boolean string into a python boolean""" + if value.upper() == 'TRUE': + return True + elif value.upper() == 'FALSE': + return False + return None + +def to_bytes(content): + """Ensures the content is bytes""" + 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""" + pass + +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(unicode(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""" + + def __init__(self, message=""): + self.message = message + + def write(self): + """write the error message out to the user""" + errormsg(self.message) + + +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""" + +class InitSubClassPy3(type): + """Provide a poly-fill for python3 __init_subclass__()""" + def __init__(cls, name, bases, dct): + if '__metaclass__' not in cls.__dict__: + if hasattr(cls, '__init_subclass__'): + cls.__init_subclass__() + super(InitSubClassPy3, cls).__init__(name, bases, dct) + +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 with commas or space separators, + also splits at -(minus) signs by adding a space in front of the - sign + """ + return [kind(val) for val in string.replace(',', ' ').replace('-', ' -').replace('e ', 'e').split()] + + +def addNS(tag, ns=None): # pylint: disable=invalid-name + """Add a known namespace to a name for use with lxml""" + if tag.startswith('{') and ns: + _, tag = removeNS(tag) + if not tag.startswith('{'): + tag = tag.replace('__', ':') + if ':' in tag: + (ns, tag) = tag.rsplit(':', 1) + if ns in NSS: + ns = NSS[ns] + if ns is not None: + return "{%s}%s" % (ns, tag) + return tag + + +def removeNS(name): # pylint: disable=invalid-name + """The reverse of addNS, finds any namespace and returns tuple (ns, tag)""" + if name[0] == '{': + (url, tag) = name[1:].split('}', 1) + return SSN.get(url, 'svg'), tag + if ':' in name: + return name.rsplit(':', 1) + return 'svg', name + +def splitNS(name): # pylint: disable=invalid-name + """Like removeNS, but returns a url instead of a prefix""" + (prefix, tag) = removeNS(name) + return (NSS[prefix], tag) + +class classproperty(object): # 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("File not found: {}".format(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)) + +class CloningVat(object): + """ + When modifying defs, sometimes we want to know if every backlink would have + needed changing, or it was just some of them. + + This tracks the def elements, their promises and creates clones if needed. + """ + def __init__(self, svg): + self.svg = svg + self.tracks = defaultdict(set) + self.set_ids = defaultdict(list) + + def track(self, elem, parent, set_id=None, **kwargs): + """Track the element and connected parent""" + elem_id = elem.get('id') + parent_id = parent.get('id') + self.tracks[elem_id].add(parent_id) + self.set_ids[elem_id].append((set_id, kwargs)) + + def process(self, process, types=(), make_clones=True, **kwargs): + """ + Process each tracked item if the backlinks match the parents + + Optionally make clones, process the clone and set the new id. + """ + for elem_id in list(self.tracks): + parents = self.tracks[elem_id] + elem = self.svg.getElementById(elem_id) + backlinks = set([blk.get('id') for blk in elem.backlinks(*types)]) + if backlinks == parents: + # No need to clone, we're processing on-behalf of all parents + process(elem, **kwargs) + elif make_clones: + clone = elem.copy() + elem.getparent().append(clone) + clone.set_random_id() + for update, upkw in self.set_ids.get(elem_id, ()): + update(elem.get('id'), clone.get('id'), **upkw) + process(clone, **kwargs) + +def fullmatch(regex, string, flags=0): + """Emulate python-3.4 re.fullmatch().""" + if hasattr(regex, 'fullmatch'): + return regex.fullmatch(string, flags) + if hasattr(regex, 'pattern'): + regex = regex.pattern + return re.match("(?:" + regex + r")\Z", string, flags=flags) |