summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/elements
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/inkex/elements')
-rw-r--r--share/extensions/inkex/elements/__init__.py19
-rw-r--r--share/extensions/inkex/elements/__pycache__/__init__.cpython-39.pycbin0 -> 1541 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_base.cpython-39.pycbin0 -> 18505 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_filters.cpython-39.pycbin0 -> 10723 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_groups.cpython-39.pycbin0 -> 3488 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_image.cpython-39.pycbin0 -> 468 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_meta.cpython-39.pycbin0 -> 5469 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_polygons.cpython-39.pycbin0 -> 9772 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_selected.cpython-39.pycbin0 -> 6660 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_svg.cpython-39.pycbin0 -> 8222 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_text.cpython-39.pycbin0 -> 6256 bytes
-rw-r--r--share/extensions/inkex/elements/__pycache__/_use.cpython-39.pycbin0 -> 2048 bytes
-rw-r--r--share/extensions/inkex/elements/_base.py514
-rw-r--r--share/extensions/inkex/elements/_filters.py276
-rw-r--r--share/extensions/inkex/elements/_groups.py112
-rw-r--r--share/extensions/inkex/elements/_image.py27
-rw-r--r--share/extensions/inkex/elements/_meta.py147
-rw-r--r--share/extensions/inkex/elements/_polygons.py231
-rw-r--r--share/extensions/inkex/elements/_selected.py159
-rw-r--r--share/extensions/inkex/elements/_svg.py211
-rw-r--r--share/extensions/inkex/elements/_text.py159
-rw-r--r--share/extensions/inkex/elements/_use.py70
22 files changed, 1925 insertions, 0 deletions
diff --git a/share/extensions/inkex/elements/__init__.py b/share/extensions/inkex/elements/__init__.py
new file mode 100644
index 0000000..f5f0d65
--- /dev/null
+++ b/share/extensions/inkex/elements/__init__.py
@@ -0,0 +1,19 @@
+"""
+Element based interface provides the bulk of features that allow you to
+interact directly with the SVG xml interface.
+
+See the documentation for each of the elements for details on how it works.
+"""
+
+from ._base import SVG_PARSER, load_svg, ShapeElement, BaseElement
+from ._svg import SvgDocumentElement
+from ._groups import Group, Layer, Anchor, Marker, ClipPath
+from ._polygons import PathElement, Polyline, Polygon, Line, Rectangle, Circle, Ellipse
+from ._text import FlowRegion, FlowRoot, FlowPara, FlowDiv, FlowSpan, TextElement, \
+ TextPath, Tspan, SVGfont, FontFace, Glyph, MissingGlyph
+from ._use import Symbol, Use
+from ._meta import Defs, StyleElement, Script, Desc, Title, NamedView, Guide, \
+ Metadata, ForeignObject, Switch, Grid
+from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, \
+ PathEffect, Stop, MeshGradient, MeshRow, MeshPatch
+from ._image import Image
diff --git a/share/extensions/inkex/elements/__pycache__/__init__.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000..718ece7
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/__init__.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_base.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_base.cpython-39.pyc
new file mode 100644
index 0000000..06dd762
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_base.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_filters.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_filters.cpython-39.pyc
new file mode 100644
index 0000000..a1d3a70
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_filters.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_groups.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_groups.cpython-39.pyc
new file mode 100644
index 0000000..35305d8
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_groups.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_image.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_image.cpython-39.pyc
new file mode 100644
index 0000000..1c2849c
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_image.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_meta.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_meta.cpython-39.pyc
new file mode 100644
index 0000000..807de7b
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_meta.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_polygons.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_polygons.cpython-39.pyc
new file mode 100644
index 0000000..270e082
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_polygons.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_selected.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_selected.cpython-39.pyc
new file mode 100644
index 0000000..6272318
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_selected.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_svg.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_svg.cpython-39.pyc
new file mode 100644
index 0000000..800e778
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_svg.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_text.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_text.cpython-39.pyc
new file mode 100644
index 0000000..1e898f2
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_text.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/__pycache__/_use.cpython-39.pyc b/share/extensions/inkex/elements/__pycache__/_use.cpython-39.pyc
new file mode 100644
index 0000000..7fb1511
--- /dev/null
+++ b/share/extensions/inkex/elements/__pycache__/_use.cpython-39.pyc
Binary files differ
diff --git a/share/extensions/inkex/elements/_base.py b/share/extensions/inkex/elements/_base.py
new file mode 100644
index 0000000..e922908
--- /dev/null
+++ b/share/extensions/inkex/elements/_base.py
@@ -0,0 +1,514 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Provide extra utility to each svg element type specific to its type.
+
+This is useful for having a common interface for each element which can
+give path, transform, and property access easily.
+"""
+
+from collections import defaultdict
+from copy import deepcopy
+from lxml import etree
+
+from ..paths import Path
+from ..styles import Style, AttrFallbackStyle, Classes
+from ..transforms import Transform, BoundingBox
+from ..utils import PY3, NSS, addNS, removeNS, splitNS, FragmentError
+from ..utils import InitSubClassPy3
+
+try:
+ from typing import overload, DefaultDict, Type, Any, List, Tuple, Union, Optional # pylint: disable=unused-import
+except ImportError:
+ overload = lambda x: x
+
+class NodeBasedLookup(etree.PythonElementClassLookup):
+ """
+ We choose what kind of Elements we should return for each element, providing useful
+ SVG based API to our extensions system.
+ """
+ # (ns,tag) -> list(cls) ; ascending priority
+ lookup_table = defaultdict(list) # type: DefaultDict[str, List[Any]]
+
+ @classmethod
+ def register_class(cls, klass):
+ """Register the given class using it's attached tag name"""
+ cls.lookup_table[splitNS(klass.tag_name)].append(klass)
+
+ def lookup(self, doc, element): # pylint: disable=unused-argument
+ """Lookup called by lxml when assigning elements their object class"""
+ try:
+ for cls in reversed(self.lookup_table[splitNS(element.tag)]):
+ if cls._is_class_element(element): # pylint: disable=protected-access
+ return cls
+ except TypeError:
+ # Handle non-element proxies case
+ # The documentation implies that it's not possible
+ # Didn't found a reliable way to check whether proxy corresponds to element or not
+ # Look like lxml issue to me.
+ # The troubling element is "<!--Comment-->"
+ return None
+ return BaseElement
+
+
+SVG_PARSER = etree.XMLParser(huge_tree=True, strip_cdata=False)
+SVG_PARSER.set_element_class_lookup(NodeBasedLookup())
+
+def load_svg(stream):
+ """Load SVG file using the SVG_PARSER"""
+ if (isinstance(stream, str) and stream.lstrip().startswith('<'))\
+ or (isinstance(stream, bytes) and stream.lstrip().startswith(b'<')):
+ return etree.ElementTree(etree.fromstring(stream, parser=SVG_PARSER))
+ return etree.parse(stream, parser=SVG_PARSER)
+
+class BaseElement(etree.ElementBase):
+ """Provide automatic namespaces to all calls"""
+ # Included for python2 support (this branch is for 1.0.x only)
+ __metaclass__ = InitSubClassPy3
+ @classmethod
+ def __init_subclass__(cls):
+ if cls.tag_name:
+ NodeBasedLookup.register_class(cls)
+
+ @classmethod
+ def _is_class_element(cls, el): # type: (etree.Element) -> bool
+ """Hook to do more restrictive check in addition to (ns,tag) match"""
+ return True
+
+ tag_name = ''
+
+ @property
+ def TAG(self): # pylint: disable=invalid-name
+ """Return the tag_name without NS"""
+ if not self.tag_name:
+ return removeNS(super(etree.ElementBase, self).tag)[-1]
+ return removeNS(self.tag_name)[-1]
+
+ @classmethod
+ def new(cls, *children, **attrs):
+ """Create a new element, converting attrs values to strings."""
+ obj = cls(*children)
+ obj.update(**attrs)
+ return obj
+
+ NAMESPACE = property(lambda self: splitNS(self.tag_name)[0])
+ PARSER = SVG_PARSER
+ WRAPPED_ATTRS = (
+ # (prop_name, [optional: attr_name], cls)
+ ('transform', Transform),
+ ('style', Style),
+ ('classes', 'class', Classes),
+ ) # type: Tuple[Tuple[Any, ...], ...]
+
+ # We do this because python2 and python3 have different ways
+ # of combining two dictionaries that are incompatible.
+ # This allows us to update these with inheritance.
+ @property
+ def wrapped_attrs(self):
+ """Map attributes to property name and wrapper class"""
+ return dict([(row[-2], (row[0], row[-1])) for row in self.WRAPPED_ATTRS])
+
+ @property
+ def wrapped_props(self):
+ """Map properties to attribute name and wrapper class"""
+ return dict([(row[0], (row[-2], row[-1])) for row in self.WRAPPED_ATTRS])
+
+ typename = property(lambda self: type(self).__name__)
+ xml_path = property(lambda self: self.getroottree().getpath(self))
+
+ def __getattr__(self, name):
+ """Get the attribute, but load it if it is not available yet"""
+ if name in self.wrapped_props:
+ (attr, cls) = self.wrapped_props[name]
+ # The reason we do this here and not in _init is because lxml
+ # is inconsistant about when elements are initialised.
+ # So we make this a lazy property.
+ def _set_attr(new_item):
+ if new_item:
+ self.set(attr, str(new_item))
+ else:
+ self.attrib.pop(attr, None) # pylint: disable=no-member
+
+ # pylint: disable=no-member
+ value = cls(self.attrib.get(attr, None), callback=_set_attr)
+ setattr(self, name, value)
+ return value
+ raise AttributeError("Can't find attribute {}.{}".format(self.typename, name))
+
+ def __setattr__(self, name, value):
+ """Set the attribute, update it if needed"""
+ if name in self.wrapped_props:
+ (attr, cls) = self.wrapped_props[name]
+ # Don't call self.set or self.get (infinate loop)
+ if value:
+ if not isinstance(value, cls):
+ value = cls(value)
+ self.attrib[attr] = str(value)
+ else:
+ self.attrib.pop(attr, None) # pylint: disable=no-member
+ else:
+ super(BaseElement, self).__setattr__(name, value)
+
+ def get(self, attr, default=None):
+ """Get element attribute named, with addNS support."""
+ if attr in self.wrapped_attrs:
+ (prop, _) = self.wrapped_attrs[attr]
+ value = getattr(self, prop, None)
+ # We check the boolean nature of the value, because empty
+ # transformations and style attributes are equiv to not-existing
+ ret = str(value) if value else (default or None)
+ return ret
+ return super(BaseElement, self).get(addNS(attr), default)
+
+ def set(self, attr, value):
+ """Set element attribute named, with addNS support"""
+ if attr in self.wrapped_attrs:
+ # Always keep the local wrapped class up to date.
+ (prop, cls) = self.wrapped_attrs[attr]
+ setattr(self, prop, cls(value))
+ value = getattr(self, prop)
+ if not value:
+ return
+ if value is None:
+ self.attrib.pop(addNS(attr), None) # pylint: disable=no-member
+ else:
+ value = str(value) if PY3 else unicode(value) # pylint: disable=undefined-variable
+ super(BaseElement, self).set(addNS(attr), value)
+
+ def update(self, **kwargs):
+ """
+ Update element attributes using keyword arguments
+
+ Note: double underscore is used as namespace separator,
+ i.e. "namespace__attr" argument name will be treated as "namespace:attr"
+
+ :param kwargs: dict with name=value pairs
+ :return: self
+ """
+ for name, value in kwargs.items():
+ self.set(name, value)
+ return self
+
+ def pop(self, attr, default=None):
+ """Delete/remove the element attribute named, with addNS support."""
+ if attr in self.wrapped_attrs:
+ # Always keep the local wrapped class up to date.
+ (prop, cls) = self.wrapped_attrs[attr]
+ value = getattr(self, prop)
+ setattr(self, prop, cls(None))
+ return value
+ return self.attrib.pop(addNS(attr), default) # pylint: disable=no-member
+
+ def add(self, *children):
+ """
+ Like append, but will do multiple children and will return
+ children or only child
+ """
+ for child in children:
+ self.append(child)
+ return children if len(children) != 1 else children[0]
+
+ def tostring(self):
+ """Return this element as it would appear in an svg document"""
+ # This kind of hack is pure maddness, but etree provides very little
+ # in the way of fragment printing, prefering to always output valid xml
+ from ..base import SvgOutputMixin
+ svg = SvgOutputMixin.get_template(width=0, height=0).getroot()
+ svg.append(self.copy())
+ return svg.tostring().split(b'>\n ', 1)[-1][:-6]
+
+ def description(self, text):
+ """Set the desc element with text"""
+ from ._meta import Desc
+ desc = self.add(Desc())
+ desc.text = text
+
+ def set_random_id(self, prefix=None, size=4, backlinks=False):
+ """Sets the id attribute if it is not already set."""
+ prefix = str(self) if prefix is None else prefix
+ self.set_id(self.root.get_unique_id(prefix, size=size), backlinks=backlinks)
+
+ def set_random_ids(self, prefix=None, levels=-1, backlinks=False):
+ """Same as set_random_id, but will apply also to children"""
+ self.set_random_id(prefix=prefix, backlinks=backlinks)
+ if levels != 0:
+ for child in self:
+ if hasattr(child, 'set_random_ids'):
+ child.set_random_ids(prefix=prefix, levels=levels-1, backlinks=backlinks)
+
+ def get_id(self):
+ """Get the id for the element, will set a new unique id if not set"""
+ if 'id' not in self.attrib:
+ self.set_random_id(self.TAG)
+ return self.get('id')
+
+ def set_id(self, new_id, backlinks=False):
+ """Set the id and update backlinks to xlink and style urls if needed"""
+ old_id = self.get('id', None)
+ self.set('id', new_id)
+ if backlinks and old_id:
+ for elem in self.root.getElementsByHref(old_id):
+ elem.href = self
+ for elem in self.root.getElementsByStyleUrl(old_id):
+ elem.style.update_urls(old_id, new_id)
+
+ @property
+ def root(self):
+ """Get the root document element from any element descendent"""
+ if self.getparent() is not None:
+ return self.getparent().root
+ from ._svg import SvgDocumentElement
+ if not isinstance(self, SvgDocumentElement):
+ raise FragmentError("Element fragment does not have a document root!")
+ return self
+
+ def get_or_create(self, xpath, nodeclass, prepend=False):
+ """Get or create the given xpath, pre/append new node if not found."""
+ node = self.findone(xpath)
+ if node is None:
+ node = nodeclass()
+ if prepend:
+ self.insert(0, node)
+ else:
+ self.append(node)
+ return node
+
+ def descendants(self):
+ """Walks the element tree and yields all elements, parent first"""
+ from ._selected import ElementList
+ return ElementList(self.root, self._descendants())
+
+ def _descendants(self):
+ yield self
+ for child in self:
+ if hasattr(child, '_descendants'):
+ for item in child._descendants(): # pylint: disable=protected-access
+ yield item
+
+ def ancestors(self, elem=None, stop_at=()):
+ """
+ Walk the parents and yield all the ancestor elements, parent first
+
+ If elem is provided, it will stop at the last common ancestor.
+ If stop_at is provided, it will stop at the first parent that is in this list.
+ """
+ from ._selected import ElementList
+ return ElementList(self.root, self._ancestors(elem=elem, stop_at=stop_at))
+
+ def _ancestors(self, elem, stop_at):
+ if isinstance(elem, BaseElement):
+ stop_at = list(elem.ancestors().values())
+ parent = self.getparent()
+ if parent is not None:
+ yield parent
+ if parent not in stop_at:
+ for item in parent._ancestors(elem=elem, stop_at=stop_at): # pylint: disable=protected-access
+ yield item
+
+ def backlinks(self, *types):
+ """Get elements which link back to this element, like ancestors but via xlinks"""
+ if not types or isinstance(self, types):
+ yield self
+ my_id = self.get('id')
+ if my_id is not None:
+ elems = list(self.root.getElementsByHref(my_id)) \
+ + list(self.root.getElementsByStyleUrl(my_id))
+ for elem in elems:
+ if hasattr(elem, 'backlinks'):
+ for child in elem.backlinks(*types):
+ yield child
+
+ def xpath(self, pattern, namespaces=NSS): # pylint: disable=dangerous-default-value
+ """Wrap xpath call and add svg namespaces"""
+ return super(BaseElement, self).xpath(pattern, namespaces=namespaces)
+
+ def findall(self, pattern, namespaces=NSS): # pylint: disable=dangerous-default-value
+ """Wrap findall call and add svg namespaces"""
+ return super(BaseElement, self).findall(pattern, namespaces=namespaces)
+
+ def findone(self, xpath):
+ """Gets a single element from the given xpath or returns None"""
+ el_list = self.xpath(xpath)
+ return el_list[0] if el_list else None
+
+ def delete(self):
+ """Delete this node from it's parent node"""
+ if self.getparent() is not None:
+ self.getparent().remove(self)
+
+ def remove_all(self, *types):
+ """Remove all children or child types"""
+ for child in self:
+ if not types or isinstance(child, types):
+ self.remove(child)
+
+ def replace_with(self, elem):
+ """Replace this element with the given element"""
+ self.addnext(elem)
+ if not elem.get('id') and self.get('id'):
+ elem.set('id', self.get('id'))
+ if not elem.label and self.label:
+ elem.label = self.label
+ self.delete()
+ return elem
+
+ def copy(self):
+ """Make a copy of the element and return it"""
+ elem = deepcopy(self)
+ elem.set('id', None)
+ return elem
+
+ def duplicate(self):
+ """Like copy(), but the copy stays in the tree and sets a random id"""
+ elem = self.copy()
+ self.addnext(elem)
+ elem.set_random_id()
+ return elem
+
+ def __str__(self):
+ # We would do more here, but lxml is VERY unpleseant when it comes to
+ # namespaces, basically over printing details and providing no
+ # supression mechanisms to turn off xml's over engineering.
+ return str(self.tag).split('}')[-1]
+
+ @property
+ def href(self):
+ """Returns the referred-to element if available"""
+ ref = self.get('xlink:href')
+ if not ref:
+ return None
+ return self.root.getElementById(ref.strip('#'))
+
+ @href.setter
+ def href(self, elem):
+ """Set the href object"""
+ if isinstance(elem, BaseElement):
+ elem = elem.get_id()
+ self.set('xlink:href', '#' + elem)
+
+ def fallback_style(self, move=False):
+ """Get styles falling back to element attributes"""
+ return AttrFallbackStyle(self, move=move)
+
+ @property
+ def label(self):
+ """Returns the inkscape label"""
+ return self.get('inkscape:label', None)
+
+ label = label.setter(lambda self, value: self.set('inkscape:label', str(value))) # type: ignore
+
+
+class ShapeElement(BaseElement):
+ """Elements which have a visible representation on the canvas"""
+ @property
+ def path(self):
+ """Gets the outline or path of the element, this may be a simple bounding box"""
+ return Path(self.get_path())
+
+ @path.setter
+ def path(self, path):
+ self.set_path(path)
+
+ @property
+ def clip(self):
+ """Gets the clip path element (if any)"""
+ ref = self.get('clip-path')
+ if not ref:
+ return None
+ return self.root.getElementById(ref)
+
+ @clip.setter
+ def clip(self, elem):
+ self.set('clip-path', 'url(#' + elem.get_id() + ')')
+
+ def get_path(self):
+ """Generate a path for this object which can inform the bounding box"""
+ raise NotImplementedError("Path should be provided by svg elem {}.".format(self.typename))
+
+ def set_path(self, path):
+ """Set the path for this object (if possible)"""
+ raise AttributeError(
+ "Path can not be set on this element: {} <- {}.".format(self.typename, path))
+
+ def to_path_element(self):
+ """Replace this element with a path element"""
+ from ._polygons import PathElement
+ elem = PathElement()
+ elem.path = self.path
+ elem.style = self.effective_style()
+ elem.transform = self.transform
+ return elem
+
+ def composed_transform(self, other=None):
+ """Calculate every transform down to the other element
+ if none specified the transform is to the root document element"""
+ parent = self.getparent()
+ if parent is not None and isinstance(parent, ShapeElement):
+ return parent.composed_transform() * self.transform
+ return self.transform
+
+ def composed_style(self):
+ """Calculate the final styles applied to this element"""
+ parent = self.getparent()
+ if parent is not None and isinstance(parent, ShapeElement):
+ return parent.composed_style() + self.style
+ return self.style
+
+ def cascaded_style(self):
+ """Add all cascaded styles, do not write to this Style object"""
+ ret = Style()
+ for style in self.root.stylesheets.lookup(self.get('id')):
+ ret += style
+ return ret + self.style
+
+ def effective_style(self):
+ """Without parent styles, what is the effective style is"""
+ return self.style
+
+ def bounding_box(self, transform=None):
+ # type: (Optional[Transform]) -> Optional[BoundingBox]
+ """BoundingBox of the shape (adjusted for its clip path if applicable)"""
+ shape_box = self.shape_box(transform)
+ clip = self.clip
+ if clip is None or shape_box is None:
+ return shape_box
+ return shape_box & clip.bounding_box(Transform(transform) * self.transform)
+
+ def shape_box(self, transform=None):
+ # type: (Optional[Transform]) -> Optional[BoundingBox]
+ """BoundingBox of the unclipped shape"""
+ path = self.path.to_absolute()
+ if transform is True:
+ path = path.transform(self.composed_transform())
+ else:
+ path = path.transform(self.transform)
+ if transform: # apply extra transformation
+ path = path.transform(transform)
+ return path.bounding_box()
+
+ def is_visible(self):
+ """Returns false if the css says this object is invisible"""
+ if self.style.get('display', '') == 'none':
+ return False
+ if not float(self.style.get('opacity', 1.0)):
+ return False
+ return True
diff --git a/share/extensions/inkex/elements/_filters.py b/share/extensions/inkex/elements/_filters.py
new file mode 100644
index 0000000..ff6883f
--- /dev/null
+++ b/share/extensions/inkex/elements/_filters.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Element interface for patterns, filters, gradients and path effects.
+"""
+
+from lxml import etree
+from copy import deepcopy
+
+from ..utils import addNS
+from ..transforms import Transform
+from ..tween import interpcoord, interp
+from ..units import convert_unit
+
+from ..styles import Style
+from ._base import BaseElement
+
+
+try:
+ from typing import overload, Iterable, List, Tuple, Union, Optional # pylint: disable=unused-import
+except ImportError:
+ overload = lambda x: x
+
+
+class Filter(BaseElement):
+ """A filter (usually in defs)"""
+ tag_name = 'filter'
+
+ def add_primitive(self, fe_type, **args):
+ """Create a filter primitive with the given arguments"""
+ elem = etree.SubElement(self, addNS(fe_type, 'svg'))
+ elem.update(**args)
+ return elem
+
+ class Primitive(BaseElement):
+ pass
+
+ class Blend(Primitive):
+ tag_name = 'feBlend'
+
+ class ColorMatrix(Primitive):
+ tag_name = 'feColorMatrix'
+
+ class ComponentTransfer(Primitive):
+ tag_name = 'feComponentTransfer'
+
+ class Composite(Primitive):
+ tag_name = 'feComposite'
+
+ class ConvolveMatrix(Primitive):
+ tag_name = 'feConvolveMatrix'
+
+ class DiffuseLighting(Primitive):
+ tag_name = 'feDiffuseLighting'
+
+ class DisplacementMap(Primitive):
+ tag_name = 'feDisplacementMap'
+
+ class Flood(Primitive):
+ tag_name = 'feFlood'
+
+ class GaussianBlur(Primitive):
+ tag_name = 'feGaussianBlur'
+
+ class Image(Primitive):
+ tag_name = 'feImage'
+
+ class Merge(Primitive):
+ tag_name = 'feMerge'
+
+ class Morphology(Primitive):
+ tag_name = 'feMorphology'
+
+ class Offset(Primitive):
+ tag_name = 'feOffset'
+
+ class SpecularLighting(Primitive):
+ tag_name = 'feSpecularLighting'
+
+ class Tile(Primitive):
+ tag_name = 'feTile'
+
+ class Turbulence(Primitive):
+ tag_name = 'feTurbulence'
+
+
+class Stop(BaseElement):
+ tag_name = 'stop'
+
+ @property
+ def offset(self):
+ # type: () -> float
+ return self.get('offset')
+
+ @offset.setter
+ def offset(self, number):
+ self.set('offset', number)
+
+ def interpolate(self, other, fraction):
+ newstop = Stop()
+ newstop.style = self.style.interpolate(other.style, fraction)
+ newstop.offset = interpcoord(float(self.offset), float(other.offset), fraction)
+ return newstop
+
+
+class Pattern(BaseElement):
+ """Pattern element which is used in the def to control repeating fills"""
+ tag_name = 'pattern'
+ WRAPPED_ATTRS = BaseElement.WRAPPED_ATTRS + (('patternTransform', Transform),)
+
+
+class Gradient(BaseElement):
+ """A gradient instruction usually in the defs"""
+ WRAPPED_ATTRS = BaseElement.WRAPPED_ATTRS + (('gradientTransform', Transform),)
+
+ orientation_attributes = () # type: Tuple[str, ...]
+
+ @property
+ def stops(self):
+ """Return an ordered list of own or linked stop nodes"""
+ gradcolor = self.href if isinstance(self.href, LinearGradient) else self
+ return sorted([child for child in gradcolor if isinstance(child, Stop)]
+ , key=lambda x: float(x.offset))
+
+ @property
+ def stop_offsets(self):
+ # type: () -> List[float]
+ """Return a list of own or linked stop offsets"""
+ return [child.offset for child in self.stops]
+
+ @property
+ def stop_styles(self): # type: () -> List[Style]
+ """Return a list of own or linked offset styles"""
+ return [child.style for child in self.stops]
+
+ def remove_orientation(self):
+ """Remove all orientation attributes from this element"""
+ for attr in self.orientation_attributes:
+ self.pop(attr)
+
+ def interpolate(self, other, fraction): # type: (LinearGradient, float) -> LinearGradient
+ """Interpolate with another gradient."""
+ if self.tag_name != other.tag_name:
+ return self
+ newgrad = self.copy()
+
+ # interpolate transforms
+ newtransform = self.gradientTransform.interpolate(other.gradientTransform, fraction)
+ newgrad.gradientTransform = newtransform
+
+ # interpolate orientation
+ for attr in self.orientation_attributes:
+ newattr = interpcoord(convert_unit(self.get(attr), 'px'), convert_unit(other.get(attr), 'px'), fraction)
+ newgrad.set(attr, newattr)
+
+ # interpolate stops
+ if self.href is not None and self.href is other.href:
+ # both gradients link to the same stops
+ pass
+ else:
+ # gradients might have different stops
+ newoffsets = sorted(self.stop_offsets + other.stop_offsets[1:-1])
+ func = lambda x,y,f: x.interpolate(y, f)
+ sstops = interp(self.stop_offsets, list(self.stops), newoffsets, func)
+ ostops = interp(other.stop_offsets, list(other.stops), newoffsets, func)
+ newstops = [s1.interpolate(s2, fraction) for s1, s2 in zip(sstops, ostops)]
+ newgrad.remove_all(Stop)
+ newgrad.add(*newstops)
+ return newgrad
+
+ def stops_and_orientation(self):
+ """Return a copy of all the stops in this gradient"""
+ stops = self.copy()
+ stops.remove_orientation()
+ orientation = self.copy()
+ orientation.remove_all(Stop)
+ return stops, orientation
+
+
+class LinearGradient(Gradient):
+ tag_name = 'linearGradient'
+ orientation_attributes = ('x1', 'y1', 'x2', 'y2')
+
+ def apply_transform(self): # type: () -> None
+ """Apply transform to orientation points and set it to identity."""
+ trans = self.pop('gradientTransform')
+ p1 = (convert_unit(self.get('x1'), 'px'), convert_unit(self.get('y1'), 'px'))
+ p2 = (convert_unit(self.get('x2'), 'px'), convert_unit(self.get('y2'), 'px'))
+ p1t = trans.apply_to_point(p1)
+ p2t = trans.apply_to_point(p2)
+ self.update(x1=p1t[0], y1=p1t[1], x2=p2t[0], y2=p2t[1])
+
+
+class RadialGradient(Gradient):
+ tag_name = 'radialGradient'
+ orientation_attributes = ('cx', 'cy', 'fx', 'fy', 'r')
+
+ def apply_transform(self): # type: () -> None
+ """Apply transform to orientation points and set it to identity."""
+ trans = self.pop('gradientTransform')
+ p1 = (convert_unit(self.get('cx'), 'px'), convert_unit(self.get('cy'), 'px'))
+ p2 = (convert_unit(self.get('fx'), 'px'), convert_unit(self.get('fy'), 'px'))
+ p1t = trans.apply_to_point(p1)
+ p2t = trans.apply_to_point(p2)
+ self.update(cx=p1t[0], cy=p1t[1], fx=p2t[0], fy=p2t[1])
+
+class PathEffect(BaseElement):
+ """Inkscape LPE element"""
+ tag_name = 'inkscape:path-effect'
+
+
+class MeshGradient(Gradient):
+ """Usable MeshGradient XML base class"""
+ tag_name = 'meshgradient'
+
+ @classmethod
+ def new_mesh(cls, pos=None, rows=1, cols=1, autocollect=True):
+ """Return skeleton of 1x1 meshgradient definition."""
+ # initial point
+ if pos is None or len(pos) != 2:
+ pos = [0.0, 0.0]
+ # create nested elements for rows x cols mesh
+ meshgradient = cls()
+ for _ in range(rows):
+ meshrow = meshgradient.add(MeshRow())
+ for _ in range(cols):
+ meshrow.append(MeshPatch())
+ # set meshgradient attributes
+ meshgradient.set('gradientUnits', 'userSpaceOnUse')
+ meshgradient.set('x', pos[0])
+ meshgradient.set('y', pos[1])
+ if autocollect:
+ meshgradient.set('inkscape:collect', 'always')
+ return meshgradient
+
+
+class MeshRow(BaseElement):
+ """Each row of a mesh gradient"""
+ tag_name = 'meshrow'
+
+class MeshPatch(BaseElement):
+ """Each column or 'patch' in a mesh gradient"""
+ tag_name = 'meshpatch'
+
+ def stops(self, edges, colors):
+ """Add or edit meshpatch stops with path and stop-color."""
+ # iterate stops based on number of edges (path data)
+ for i, edge in enumerate(edges):
+ if i < len(self):
+ stop = self[i]
+ else:
+ stop = self.add(Stop())
+
+ # set edge path data
+ stop.set('path', str(edge))
+ # set stop color
+ stop.style['stop-color'] = str(colors[i % 2])
diff --git a/share/extensions/inkex/elements/_groups.py b/share/extensions/inkex/elements/_groups.py
new file mode 100644
index 0000000..52bd073
--- /dev/null
+++ b/share/extensions/inkex/elements/_groups.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Ryan Jarvis <ryan@shopboxretail.com>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Interface for all group based elements such as Groups, Use, Markers etc.
+"""
+
+from lxml import etree # pylint: disable=unused-import
+
+from ..paths import Path
+from ..utils import addNS
+from ..transforms import Transform
+
+from ._base import ShapeElement
+
+try:
+ from typing import Optional # pylint: disable=unused-import
+except ImportError:
+ pass
+
+class GroupBase(ShapeElement):
+ """Base Group element"""
+ def get_path(self):
+ ret = Path()
+ for child in self:
+ if isinstance(child, ShapeElement):
+ ret += child.path.transform(child.transform)
+ return ret
+
+ def shape_box(self, transform=None):
+ bbox = None
+ effective_transform = Transform(transform) * self.transform
+ for child in self:
+ if isinstance(child, ShapeElement):
+ child_bbox = child.bounding_box(transform=effective_transform)
+ if child_bbox is not None:
+ bbox += child_bbox
+ return bbox
+
+
+class Group(GroupBase):
+ """Any group element (layer or regular group)"""
+ tag_name = 'g'
+
+ @classmethod
+ def new(cls, label, *children, **attrs):
+ attrs['inkscape:label'] = label
+ return super(Group, cls).new(*children, **attrs)
+
+
+ def effective_style(self):
+ """A blend of each child's style mixed together (last child wins)"""
+ style = self.style
+ for child in self:
+ style.update(child.effective_style())
+ return style
+
+ @property
+ def groupmode(self):
+ """Return the type of group this is"""
+ return self.get('inkscape:groupmode', 'group')
+
+
+class Layer(Group):
+ """Inkscape extension of svg:g"""
+
+ def _init(self):
+ self.set('inkscape:groupmode', 'layer')
+
+ @classmethod
+ def _is_class_element(cls, el):
+ # type: (etree.Element) -> bool
+ return el.attrib.get(addNS('inkscape:groupmode'), None) == "layer"
+
+
+class Anchor(GroupBase):
+ """An anchor or link tag"""
+ tag_name = 'a'
+
+ @classmethod
+ def new(cls, href, *children, **attrs):
+ attrs['xlink:href'] = href
+ return super(Anchor, cls).new(*children, **attrs)
+
+
+class ClipPath(GroupBase):
+ """A path used to clip objects"""
+ tag_name = 'clipPath'
+
+
+class Marker(GroupBase):
+ """The <marker> element defines the graphic that is to be used for drawing arrowheads
+ or polymarkers on a given <path>, <line>, <polyline> or <polygon> element."""
+ tag_name = 'marker'
diff --git a/share/extensions/inkex/elements/_image.py b/share/extensions/inkex/elements/_image.py
new file mode 100644
index 0000000..efd00d3
--- /dev/null
+++ b/share/extensions/inkex/elements/_image.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 - Martin Owens <doctormo@gmail.com>
+#
+# 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.
+#
+"""
+Image element interface.
+"""
+
+from ._polygons import RectangleBase
+
+class Image(RectangleBase):
+ """Provide a useful extension for image elements"""
+ tag_name = 'image'
diff --git a/share/extensions/inkex/elements/_meta.py b/share/extensions/inkex/elements/_meta.py
new file mode 100644
index 0000000..f8107ad
--- /dev/null
+++ b/share/extensions/inkex/elements/_meta.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Maren Hachmann <moini>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Provide extra utility to each svg element type specific to its type.
+
+This is useful for having a common interface for each element which can
+give path, transform, and property access easily.
+"""
+
+import math
+
+from lxml import etree
+
+from ..styles import StyleSheet
+from ..transforms import Vector2d
+
+from ._base import BaseElement
+
+class Defs(BaseElement):
+ """A header defs element, one per document"""
+ tag_name = 'defs'
+
+class StyleElement(BaseElement):
+ """A CSS style element containing multiple style definitions"""
+ tag_name = 'style'
+
+ def set_text(self, content):
+ """Sets the style content text as a CDATA section"""
+ self.text = etree.CDATA(str(content))
+
+ def stylesheet(self):
+ """Return the StyleSheet() object for the style tag"""
+ return StyleSheet(self.text, callback=self.set_text)
+
+class Script(BaseElement):
+ """A javascript tag in SVG"""
+ tag_name = 'script'
+
+ def set_text(self, content):
+ """Sets the style content text as a CDATA section"""
+ self.text = etree.CDATA(str(content))
+
+class Desc(BaseElement):
+ """Description element"""
+ tag_name = 'desc'
+
+class Title(BaseElement):
+ """Title element"""
+ tag_name = 'title'
+
+class NamedView(BaseElement):
+ """The NamedView element is Inkscape specific metadata about the file"""
+ tag_name = 'sodipodi:namedview'
+
+ current_layer = property(lambda self: self.get('inkscape:current-layer'))
+
+ @property
+ def center(self):
+ """Returns view_center in terms of document units"""
+ return Vector2d(self.root.unittouu(self.get('inkscape:cx') or 0),
+ self.root.unittouu(self.get('inkscape:cy') or 0))
+
+ def get_guides(self):
+ """Returns a list of guides"""
+ return self.findall('sodipodi:guide')
+
+ def new_guide(self, position, orient=True, name=None):
+ """Creates a new guide in this namedview"""
+ if orient is True:
+ elem = Guide().move_to(0, position, (0, 1))
+ elif orient is False:
+ elem = Guide().move_to(position, 0, (1, 0))
+ if name:
+ elem.set('inkscape:label', str(name))
+ return self.add(elem)
+
+
+class Guide(BaseElement):
+ """An inkscape guide"""
+ tag_name = 'sodipodi:guide'
+
+ is_horizontal = property(lambda self: self.get('orientation').startswith('0,') and not
+ self.get('orientation') == '0,0')
+ is_vertical = property(lambda self: self.get('orientation').endswith(',0'))
+ point = property(lambda self: Vector2d(self.get('position')))
+
+ @classmethod
+ def new(cls, pos_x, pos_y, angle, **attrs):
+ guide = super(Guide, cls).new(**attrs)
+ guide.move_to(pos_x, pos_y, angle=angle)
+ return guide
+
+ def move_to(self, pos_x, pos_y, angle=None):
+ """
+ Move this guide to the given x,y position,
+
+ Angle may be a float or integer, which will change the orientation. Alternately,
+ it may be a pair of numbers (tuple) which will set the orientation directly.
+ """
+ self.set('position', "{:g},{:g}".format(float(pos_x), float(pos_y)))
+ if isinstance(angle, str):
+ if ',' not in angle:
+ angle = float(angle)
+
+ if isinstance(angle, (float, int)):
+ # Generate orientation from angle
+ angle = (math.sin(math.radians(angle)), -math.cos(math.radians(angle)))
+
+ if isinstance(angle, (tuple, list)) and len(angle) == 2:
+ angle = "{:g},{:g}".format(*angle)
+
+ self.set('orientation', angle)
+ return self
+
+class Metadata(BaseElement):
+ """Inkscape Metadata element"""
+ tag_name = 'metadata'
+
+class ForeignObject(BaseElement):
+ """SVG foreignObject element"""
+ tag_name = 'foreignObject'
+
+class Switch(BaseElement):
+ """A switch element"""
+ tag_name = 'switch'
+
+class Grid(BaseElement):
+ """A namedview grid child"""
+ tag_name = 'inkscape:grid'
diff --git a/share/extensions/inkex/elements/_polygons.py b/share/extensions/inkex/elements/_polygons.py
new file mode 100644
index 0000000..0bcbc38
--- /dev/null
+++ b/share/extensions/inkex/elements/_polygons.py
@@ -0,0 +1,231 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Interface for all shapes/polygons such as lines, paths, rectangles, circles etc.
+"""
+
+from ..paths import Path
+from ..transforms import Transform, ImmutableVector2d, Vector2d
+from ..utils import addNS
+from ..units import convert_unit
+
+from ._base import ShapeElement
+
+class PathElementBase(ShapeElement):
+ """Base element for path based shapes"""
+ get_path = lambda self: self.get('d')
+
+ @classmethod
+ def new(cls, path, **attrs):
+ return super(PathElementBase, cls).new(d=Path(path), **attrs)
+
+ def set_path(self, path):
+ """Set the given data as a path as the 'd' attribute"""
+ self.set('d', str(Path(path)))
+
+ def apply_transform(self):
+ """Apply the internal transformation to this node and delete"""
+ if 'transform' in self.attrib:
+ self.path = self.path.transform(self.transform)
+ self.set('transform', Transform())
+
+ @property
+ def original_path(self):
+ """Returns the original path if this is a LPE, or the path if not"""
+ return Path(self.get('inkscape:original-d', self.path))
+
+ @original_path.setter
+ def original_path(self, path):
+ if addNS('inkscape:original-d') in self.attrib:
+ self.set('inkscape:original-d', str(Path(path)))
+ else:
+ self.path = path
+
+
+class PathElement(PathElementBase):
+ """Provide a useful extension for path elements"""
+ tag_name = 'path'
+
+ @classmethod
+ def arc(cls, center, rx, ry=None, **kw): # pylint: disable=invalid-name
+ """Generate a sodipodi arc (special type)"""
+ others = [(name, kw.pop(name, None)) for name in ('start', 'end', 'open')]
+ elem = cls(**kw)
+ elem.set('sodipodi:cx', center[0])
+ elem.set('sodipodi:cy', center[1])
+ elem.set('sodipodi:rx', rx)
+ elem.set('sodipodi:ry', ry or rx)
+ elem.set('sodipodi:type', 'arc')
+ for name, value in others:
+ if value is not None:
+ elem.set('sodipodi:'+name, str(value).lower())
+ return elem
+
+ @classmethod
+ def star(cls, center, radi, sides, rounded=None):
+ """Generate a sodipodi start (special type)"""
+ elem = cls()
+ elem.set('sodipodi:cx', center[0])
+ elem.set('sodipodi:cy', center[1])
+ elem.set('sodipodi:r1', radi[0])
+ elem.set('sodipodi:r2', radi[1])
+ elem.set('sodipodi:arg1', 0.85)
+ elem.set('sodipodi:arg2', 1.3)
+ elem.set('sodipodi:sides', sides)
+ elem.set('inkscape:rounded', rounded)
+ elem.set('sodipodi:type', 'star')
+ return elem
+
+
+class Polyline(ShapeElement):
+ """Like a path, but made up of straight line segments only"""
+ tag_name = 'polyline'
+
+ def get_path(self):
+ return Path('M' + self.get('points'))
+
+ def set_path(self, path):
+ points = ['{:g},{:g}'.format(x, y) for x, y in Path(path).end_points]
+ self.set('points', ' '.join(points))
+
+
+class Polygon(ShapeElement):
+ """A closed polyline"""
+ tag_name = 'polygon'
+ get_path = lambda self: 'M' + self.get('points') + ' Z'
+
+
+class Line(ShapeElement):
+ """A line segment connecting two points"""
+ tag_name = 'line'
+ get_path = lambda self: 'M{0[x1]},{0[y1]} L{0[x2]},{0[y2]} Z'.format(self.attrib)
+
+ @classmethod
+ def new(cls, start, end, **attrs):
+ start = Vector2d(start)
+ end = Vector2d(end)
+ return super(Line, cls).new(x1=start.x, y1=start.y,
+ x2=end.x, y2=end.y, **attrs)
+
+
+class RectangleBase(ShapeElement):
+ """Provide a useful extension for rectangle elements"""
+ left = property(lambda self: convert_unit(self.get('x', '0'), 'px'))
+ top = property(lambda self: convert_unit(self.get('y', '0'), 'px'))
+ right = property(lambda self: self.left + self.width)
+ bottom = property(lambda self: self.top + self.height)
+ width = property(lambda self: convert_unit(self.get('width', '0'), 'px'))
+ height = property(lambda self: convert_unit(self.get('height', '0'), 'px'))
+ rx = property(lambda self: convert_unit(self.get('rx', self.get('ry', 0.0)), 'px'))
+ ry = property(lambda self: convert_unit(self.get('ry', self.get('rx', 0.0)), 'px')) # pylint: disable=invalid-name
+
+ def get_path(self):
+ """Calculate the path as the box around the rect"""
+ if self.rx:
+ rx, ry = self.rx, self.ry # pylint: disable=invalid-name
+ return 'M {1},{0.top}'\
+ 'L {2},{0.top} A {0.rx},{0.ry} 0 0 1 {0.right},{3}'\
+ 'L {0.right},{4} A {0.rx},{0.ry} 0 0 1 {2},{0.bottom}'\
+ 'L {1},{0.bottom} A {0.rx},{0.ry} 0 0 1 {0.left},{4}'\
+ 'L {0.left},{3} A {0.rx},{0.ry} 0 0 1 {1},{0.top} z'\
+ .format(self, self.left + rx, self.right - rx, self.top + ry, self.bottom - ry)
+
+ return 'M {0.left},{0.top} h{0.width}v{0.height}h{1} z'.format(self, -self.width)
+
+
+class Rectangle(RectangleBase):
+ """Provide a useful extension for rectangle elements"""
+ tag_name = 'rect'
+
+ @classmethod
+ def new(cls, left, top, width, height, **attrs):
+ return super(Rectangle, cls).new(x=left, y=top, width=width, height=height, **attrs)
+
+
+class EllipseBase(ShapeElement):
+ """Absorbs common part of Circle and Ellipse classes"""
+
+ def get_path(self):
+ """Calculate the arc path of this circle"""
+ rx, ry = self._rxry()
+ cx, y = self.center.x, self.center.y - ry
+ return ('M {cx},{y} '
+ 'a {rx},{ry} 0 1 0 {rx}, {ry} '
+ 'a {rx},{ry} 0 0 0 -{rx}, -{ry} z'
+ ).format(cx=cx, y=y, rx=rx, ry=ry)
+
+ @property
+ def center(self):
+ return ImmutableVector2d(convert_unit(self.get('cx', '0'), 'px'), convert_unit(self.get('cy', '0'), 'px'))
+
+ @center.setter
+ def center(self, value):
+ value = Vector2d(value)
+ self.set("cx", value.x)
+ self.set("cy", value.y)
+
+ def _rxry(self):
+ # type: () -> Vector2d
+ """Helper function """
+ raise NotImplementedError()
+
+ @classmethod
+ def new(cls, center, radius, **attrs):
+ circle = super(EllipseBase, cls).new(**attrs)
+ circle.center = center
+ circle.radius = radius
+ return circle
+
+
+class Circle(EllipseBase):
+ """Provide a useful extension for circle elements"""
+ tag_name = 'circle'
+
+ @property
+ def radius(self):
+ return convert_unit(self.get('r', '0'), 'px')
+
+ @radius.setter
+ def radius(self, value):
+ self.set("r", value)
+
+ def _rxry(self):
+ r = self.radius
+ return Vector2d(r, r)
+
+
+class Ellipse(EllipseBase):
+ """Provide a similar extension to the Circle interface"""
+ tag_name = 'ellipse'
+
+ @property
+ def radius(self):
+ return ImmutableVector2d(convert_unit(self.get('rx', '0'), 'px'), convert_unit(self.get('ry', '0'), 'px'))
+
+ @radius.setter
+ def radius(self, value):
+ value = Vector2d(value)
+ self.set("rx", str(value.x))
+ self.set("ry", str(value.y))
+
+ def _rxry(self):
+ return self.radius
diff --git a/share/extensions/inkex/elements/_selected.py b/share/extensions/inkex/elements/_selected.py
new file mode 100644
index 0000000..c09c526
--- /dev/null
+++ b/share/extensions/inkex/elements/_selected.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+#
+# 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.,Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+When elements are selected, these structures provide an advanced API.
+"""
+
+from collections import OrderedDict
+
+class ElementList(OrderedDict):
+ """
+ A list of elements, selected by id, iterator or xpath
+
+ This may look like a dictionary, but it is really a list of elements.
+ The default iterator is the element objects themselves (not keys) and it is
+ possible to key elements by their numerical index.
+
+ It is also possible to look up items by their id and the element object itself.
+ """
+ def __init__(self, svg, _iter=None):
+ self.svg = svg
+ self.ids = OrderedDict()
+ super(ElementList, self).__init__()
+ if _iter:
+ self.set(*list(_iter))
+
+ def __getitem__(self, key):
+ return super(ElementList, self).__getitem__(self._to_key(key))
+
+ def __contains__(self, key):
+ return super(ElementList, self).__contains__(self._to_key(key))
+
+ def __setitem__(self, orig_key, elem):
+ from ._base import BaseElement
+ if orig_key != elem and orig_key != elem.get('id'):
+ raise ValueError("Refusing to set bad key in ElementList {}".format(orig_key))
+ if isinstance(elem, str):
+ key = elem
+ elem = self.svg.getElementById(elem)
+ if elem is None:
+ return
+ if isinstance(elem, BaseElement):
+ # Selection is a list of elements to select
+ key = elem.xml_path
+ element_id = elem.get('id')
+ if element_id is not None:
+ self.ids[element_id] = key
+ super(ElementList, self).__setitem__(key, elem)
+ else:
+ kind = type(elem).__name__
+ raise ValueError("Unknown element type: {}".format(kind))
+
+ def _to_key(self, key, default=None):
+ """Takes a key (id, element, etc) and returns an xml_path key"""
+ from ._base import BaseElement
+ if self and key is None:
+ key = default
+ if isinstance(key, int):
+ return list(self.keys())[key]
+ elif isinstance(key, BaseElement):
+ return key.xml_path
+ elif isinstance(key, str) and key[0] != '/':
+ return self.ids.get(key, key)
+ return key
+
+ def clear(self):
+ """Also clear ids"""
+ self.ids.clear()
+ super(ElementList, self).clear()
+
+ def set(self, *ids):
+ """
+ Sets the currently selected elements to these ids, any existing
+ selection is cleared.
+
+ Arguments a list of element ids, element objects or
+ a single xpath expression starting with "//".
+
+ All element objects must have an id to be correctly set.
+
+ >>> selection.set("rect123", "path456", "text789")
+ >>> selection.set(elem1, elem2, elem3)
+ >>> selection.set("//rect")
+ """
+ self.clear()
+ self.add(*ids)
+
+ def pop(self, key=None):
+ """Remove the key item or remove the last item selected"""
+ item = super(ElementList, self).pop(self._to_key(key, default=-1))
+ self.ids.pop(item.get('id'))
+ return item
+
+ def add(self, *ids):
+ """Like set() but does not clear first"""
+ # Allow selecting of xpath elements directly
+ if len(ids) == 1 and isinstance(ids[0], str) and ids[0].startswith('//'):
+ ids = self.svg.xpath(ids[0])
+
+ for elem in ids:
+ self[elem] = elem # This doesn't matter
+
+ def paint_order(self):
+ """Get the selected elements, but ordered by their appearance in the document"""
+ new_list = ElementList(self.svg)
+ new_list.set(*[elem for _, elem in sorted(self.items(), key=lambda x: x[0])])
+ return new_list
+
+ def filter(self, *types):
+ """Filter selected elements of the given type, returns a new SelectedElements object"""
+ return ElementList(self.svg, [e for e in self.values() if not types or isinstance(e, types)])
+
+ def get(self, *types):
+ """Like filter, but will enter each element searching for any child of the given types"""
+ def _recurse(elem):
+ if not types or isinstance(elem, types):
+ yield elem
+ for child in elem:
+ for item in _recurse(child):
+ yield item
+ return ElementList(self.svg, [r for e in self.values() for r in _recurse(e)])
+
+ def id_dict(self):
+ """For compatibility, return regular dictionary of id -> element pairs"""
+ return OrderedDict([(eid, self[xid]) for eid, xid in self.ids.items()])
+
+ def bounding_box(self):
+ """
+ Gets a :class:`inkex.transforms.BoundingBox` object for the selected items.
+
+ Text objects have a bounding box without width or height that only
+ reflects the coordinate of their anchor. If a text object is a part of
+ the selection's boundary, the bounding box may be inaccurate.
+
+ When no object is selected or when the object's location cannot be
+ determined (e.g. empty group or layer), all coordinates will be None.
+ """
+ return sum([elem.bounding_box() for elem in self.values()], None)
+
+ def first(self):
+ """Returns the first item in the selected list"""
+ for elem in self.values():
+ return elem
+ return None
diff --git a/share/extensions/inkex/elements/_svg.py b/share/extensions/inkex/elements/_svg.py
new file mode 100644
index 0000000..3e1af09
--- /dev/null
+++ b/share/extensions/inkex/elements/_svg.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Windell Oskay <windell@oskay.net>
+#
+# 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.
+#
+# pylint: disable=attribute-defined-outside-init
+#
+"""
+Provide a way to load lxml attributes with an svg API on top.
+"""
+
+import random
+from lxml import etree
+
+from ..deprecated import DeprecatedSvgMixin
+from ..units import discover_unit, convert_unit, render_unit
+from ._selected import ElementList
+from ..transforms import BoundingBox
+from ..styles import StyleSheets
+
+from ._base import BaseElement
+from ._meta import NamedView, Defs, StyleElement, Metadata
+
+if False: # pylint: disable=using-constant-test
+ import typing # pylint: disable=unused-import
+
+
+class SvgDocumentElement(DeprecatedSvgMixin, BaseElement):
+ """Provide access to the document level svg functionality"""
+ tag_name = 'svg'
+
+ def _init(self):
+ self.current_layer = None
+ self.view_center = (0.0, 0.0)
+ self.selection = ElementList(self)
+ self.ids = {}
+
+ def tostring(self):
+ """Convert document to string"""
+ return etree.tostring(etree.ElementTree(self))
+
+ def get_ids(self):
+ """Returns a set of unique document ids"""
+ if not self.ids:
+ self.ids = set(self.xpath('//@id'))
+ return self.ids
+
+ def get_unique_id(self, prefix, size=4):
+ """Generate a new id from an existing old_id"""
+ ids = self.get_ids()
+ new_id = None
+ _from = 10 ** size - 1
+ _to = 10 ** size
+ while new_id is None or new_id in ids:
+ # Do not use randint because py2/3 incompatibility
+ new_id = prefix + str(int(random.random() * _from - _to) + _to)
+ self.ids.add(new_id)
+ return new_id
+
+ def get_page_bbox(self):
+ """Gets the page dimensions as a bbox"""
+ return BoundingBox((0, float(self.width)), (0, float(self.height)))
+
+ def get_current_layer(self):
+ """Returns the currently selected layer"""
+ layer = self.getElementById(self.namedview.current_layer, 'svg:g')
+ if layer is None:
+ return self
+ return layer
+
+ def getElement(self, xpath): # pylint: disable=invalid-name
+ """Gets a single element from the given xpath or returns None"""
+ return self.findone(xpath)
+
+ def getElementById(self, eid, elm='*'): # pylint: disable=invalid-name
+ """Get an element in this svg document by it's ID attribute"""
+ if eid is not None:
+ eid = eid.strip()[4:-1] if eid.startswith('url(') else eid
+ eid = eid.lstrip('#')
+ return self.getElement('//{elm}[@id="{eid}"]'.format(elm=elm, eid=eid))
+
+ def getElementByName(self, name, elm='*'): # pylint: disable=invalid-name
+ """Get an element by it's inkscape:label (aka name)"""
+ return self.getElement('//{elm}[@inkscape:label="{name}"]'.format(elm=elm, name=name))
+
+ def getElementsByClass(self, class_name): # pylint: disable=invalid-name
+ """Get elements by it's class name"""
+ from inkex.styles import ConditionalRule
+ return self.xpath(ConditionalRule(".{}".format(class_name)).to_xpath())
+
+ def getElementsByHref(self, eid): # pylint: disable=invalid-name
+ """Get elements by their href xlink attribute"""
+ return self.xpath('//*[@xlink:href="#{}"]'.format(eid))
+
+ def getElementsByStyleUrl(self, eid, style=None): # pylint: disable=invalid-name
+ """Get elements by a style attribute url"""
+ url = "url(#{})".format(eid)
+ if style is not None:
+ url = style + ":" + url
+ return self.xpath('//*[contains(@style,"{}")]'.format(url))
+
+ @property
+ def name(self):
+ """Returns the Document Name"""
+ return self.get('sodipodi:docname', '')
+
+ @property
+ def namedview(self):
+ """Return the sp namedview meta information element"""
+ return self.get_or_create('//sodipodi:namedview', NamedView, True)
+
+ @property
+ def metadata(self):
+ """Return the svg metadata meta element container"""
+ return self.get_or_create('//svg:metadata', Metadata, True)
+
+ @property
+ def defs(self):
+ """Return the svg defs meta element container"""
+ return self.get_or_create('//svg:defs', Defs, True)
+
+ def get_viewbox(self):
+ """Parse and return the document's viewBox attribute"""
+ try:
+ ret = [float(unit) for unit in self.get('viewBox', '0').split()]
+ except ValueError:
+ ret = ''
+ if len(ret) != 4:
+ return [0, 0, 0, 0]
+ return ret
+
+ @property
+ def width(self): # getDocumentWidth(self):
+ """Fault tolerance for lazily defined SVG"""
+ return self.unittouu(self.get('width')) or self.get_viewbox()[2]
+
+ @property
+ def height(self): # getDocumentHeight(self):
+ """Returns a string corresponding to the height of the document, as
+ defined in the SVG file. If it is not defined, returns the height
+ as defined by the viewBox attribute. If viewBox is not defined,
+ returns the string '0'."""
+ return self.unittouu(self.get('height')) or self.get_viewbox()[3]
+
+ @property
+ def scale(self):
+ """Return the ratio between the page width and the viewBox width"""
+ try:
+ scale_x = float(self.width) / float(self.get_viewbox()[2])
+ scale_y = float(self.height) / float(self.get_viewbox()[3])
+ return max([scale_x, scale_y])
+ except (ValueError, ZeroDivisionError):
+ return 1.0
+
+ @property
+ def unit(self):
+ """Returns the unit used for in the SVG document.
+ In the case the SVG document lacks an attribute that explicitly
+ defines what units are used for SVG coordinates, it tries to calculate
+ the unit from the SVG width and viewBox attributes.
+ Defaults to 'px' units."""
+ viewbox = self.get_viewbox()
+ if viewbox and set(viewbox) != {0}:
+ return discover_unit(self.get('width'), viewbox[2], default='px')
+ return 'px' # Default is px
+
+ def unittouu(self, value):
+ """Convert a unit value into the document's units"""
+ return convert_unit(value, self.unit)
+
+ def uutounit(self, value, to_unit):
+ """Convert from the document's units to the given unit"""
+ return convert_unit(render_unit(value, self.unit), to_unit)
+
+ def add_unit(self, value):
+ """Add document unit when no unit is specified in the string """
+ return render_unit(value, self.unit)
+
+ @property
+ def stylesheets(self):
+ """Get all the stylesheets, bound together to one, (for reading)"""
+ sheets = StyleSheets(self)
+ for node in self.xpath('//svg:style'):
+ sheets.append(node.stylesheet())
+ return sheets
+
+ @property
+ def stylesheet(self):
+ """Return the first stylesheet or create one if needed (for writing)"""
+ for sheet in self.stylesheets:
+ return sheet
+
+ style_node = StyleElement()
+ self.defs.append(style_node)
+ return style_node.stylesheet()
diff --git a/share/extensions/inkex/elements/_text.py b/share/extensions/inkex/elements/_text.py
new file mode 100644
index 0000000..766344c
--- /dev/null
+++ b/share/extensions/inkex/elements/_text.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+#
+# 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.
+#
+# pylint: disable=arguments-differ
+"""
+Provide text based element classes interface.
+
+Because text is not rendered at all, no information about a text's path
+size or actual location can be generated yet.
+"""
+
+from ..paths import Path
+from ..transforms import Transform, BoundingBox
+from ..units import convert_unit
+
+from ._base import BaseElement, ShapeElement
+from ._polygons import PathElementBase
+
+class FlowRegion(ShapeElement):
+ """SVG Flow Region (SVG 2.0)"""
+ tag_name = 'flowRegion'
+
+ def get_path(self):
+ # This ignores flowRegionExcludes
+ return sum([child.path for child in self], Path())
+
+class FlowRoot(ShapeElement):
+ """SVG Flow Root (SVG 2.0)"""
+ tag_name = 'flowRoot'
+
+ @property
+ def region(self):
+ """Return the first flowRegion in this flowRoot"""
+ return self.findone('svg:flowRegion')
+
+ def get_path(self):
+ region = self.region
+ return region.get_path() if region is not None else Path()
+
+class FlowPara(ShapeElement):
+ """SVG Flow Paragraph (SVG 2.0)"""
+ tag_name = 'flowPara'
+
+ def get_path(self):
+ # XXX: These empty paths mean the bbox for text elements will be nothing.
+ return Path()
+
+class FlowDiv(ShapeElement):
+ """SVG Flow Div (SVG 2.0)"""
+ tag_name = 'flowDiv'
+
+ def get_path(self):
+ # XXX: These empty paths mean the bbox for text elements will be nothing.
+ return Path()
+
+class FlowSpan(ShapeElement):
+ """SVG Flow Span (SVG 2.0)"""
+ tag_name = 'flowSpan'
+
+ def get_path(self):
+ # XXX: These empty paths mean the bbox for text elements will be nothing.
+ return Path()
+
+class TextElement(ShapeElement):
+ """A Text element"""
+ tag_name = 'text'
+ x = property(lambda self: convert_unit(self.get('x', 0), 'px'))
+ y = property(lambda self: convert_unit(self.get('y', 0), 'px'))
+
+ def get_path(self):
+ return Path()
+
+ def tspans(self):
+ """Returns all children that are tspan elements"""
+ return self.findall('svg:tspan')
+
+ def get_text(self, sep="\n"):
+ """Return the text content including tspans"""
+ nodes = [self] + list(self.tspans())
+ return sep.join([elem.text for elem in nodes if elem.text is not None])
+
+ def shape_box(self, transform=None):
+ """
+ Returns a horrible bounding box that just contains the coord points
+ of the text without width or height (which is impossible to calculate)
+ """
+ effective_transform = Transform(transform) * self.transform
+ x, y = effective_transform.apply_to_point((self.x, self.y))
+ bbox = BoundingBox(x, y)
+ for tspan in self.tspans():
+ bbox += tspan.bounding_box(effective_transform)
+ return bbox
+
+class TextPath(ShapeElement):
+ """A textPath element"""
+ tag_name = 'textPath'
+
+ def get_path(self):
+ return Path()
+
+class Tspan(ShapeElement):
+ """A tspan text element"""
+ tag_name = 'tspan'
+ x = property(lambda self: convert_unit(self.get('x', 0), 'px'))
+ y = property(lambda self: convert_unit(self.get('y', 0), 'px'))
+
+ @classmethod
+ def superscript(cls, text):
+ """Adds a superscript tspan element"""
+ return cls(text, style="font-size:65%;baseline-shift:super")
+
+ def get_path(self):
+ return Path()
+
+ def shape_box(self, transform=None):
+ """
+ Returns a horrible bounding box that just contains the coord points
+ of the text without width or height (which is impossible to calculate)
+ """
+ effective_transform = Transform(transform) * self.transform
+ x1, y1 = effective_transform.apply_to_point((self.x, self.y))
+ fontsize = convert_unit(self.style.get('font-size', '1em'), 'px')
+ x2 = self.x + 0 # XXX This is impossible to calculate!
+ y2 = self.y + float(fontsize)
+ x2, y2 = effective_transform.apply_to_point((x2, y2))
+ return BoundingBox((x1, x2), (y1, y2))
+
+
+class SVGfont(BaseElement):
+ """An svg font element"""
+ tag_name = 'font'
+
+class FontFace(BaseElement):
+ """An svg font font-face element"""
+ tag_name = 'font-face'
+
+class Glyph(PathElementBase):
+ """An svg font glyph element"""
+ tag_name = 'glyph'
+
+class MissingGlyph(BaseElement):
+ """An svg font missing-glyph element"""
+ tag_name = 'missing-glyph'
diff --git a/share/extensions/inkex/elements/_use.py b/share/extensions/inkex/elements/_use.py
new file mode 100644
index 0000000..b5a38c5
--- /dev/null
+++ b/share/extensions/inkex/elements/_use.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.com>
+#
+# 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.
+#
+"""
+Interface for the Use and Symbol elements
+"""
+
+from ..transforms import Transform
+
+from ._groups import Group, GroupBase
+from ._base import ShapeElement
+
+class Symbol(GroupBase):
+ """SVG symbol element"""
+ tag_name = 'symbol'
+
+class Use(ShapeElement):
+ """A 'use' element that links to another in the document"""
+ tag_name = 'use'
+
+ @classmethod
+ def new(cls, elem, x, y, **attrs): # pylint: disable=arguments-differ
+ ret = super(Use, cls).new(x=x, y=y, **attrs)
+ ret.href = elem
+ return ret
+
+ def get_path(self):
+ """Returns the path of the cloned href plus any transformation"""
+ path = self.href.path
+ path.transform(self.href.transform)
+ return path
+
+ def effective_style(self):
+ """Href's style plus this object's own styles"""
+ style = self.href.effective_style()
+ style.update(self.style)
+ return style
+
+ def unlink(self):
+ """Unlink this clone, replacing it with a copy of the original"""
+ copy = self.href.copy()
+ if isinstance(copy, Symbol):
+ group = Group(**copy.attrib)
+ group.extend(copy)
+ copy = group
+ copy.transform *= self.transform
+ copy.style = self.style + copy.style
+ self.replace_with(copy)
+ copy.set_random_ids()
+ return copy
+
+ def shape_box(self, transform=None):
+ effective_transform = Transform(transform) * self.transform
+ return self.href.bounding_box(effective_transform)