# -*- coding: utf-8 -*- # # Copyright (c) 2020 Martin Owens # Sergei Izmailov # Ryan Jarvis # # 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 ..transforms import BoundingBox, Transform, Vector2d from ._utils import addNS from ._base import ShapeElement, ViewboxMixin from ._polygons import PathElement try: from typing import Optional, List # 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): child_path = child.path.transform(child.transform) if child_path and child_path[0].is_relative: child_path[0] = child_path[0].to_absolute(Vector2d(0, 0)) ret += child_path return ret def bounding_box(self, transform=None): # type: (Optional[Transform]) -> Optional[BoundingBox] """BoundingBox of the shape .. versionchanged:: 1.4 Exclude invisible child objects from bounding box computation .. versionchanged:: 1.1 result adjusted for element's clip path if applicable. """ bbox = None effective_transform = Transform(transform) @ self.transform for child in self: if isinstance(child, ShapeElement) and child.is_visible(): child_bbox = child.bounding_box(transform=effective_transform) if child_bbox is not None: bbox += child_bbox clip = self.clip if clip is None or bbox is None: return bbox return bbox & clip.bounding_box(Transform(transform) @ self.transform) def shape_box(self, transform=None): # type: (Optional[Transform]) -> Optional[BoundingBox] """BoundingBox of the unclipped shape .. versionchanged:: 1.4 returns the bounding box without possible clip effects of child objects .. versionadded:: 1.1 Previous :func:`bounding_box` function, returning the bounding box without computing the effect of a possible clip.""" bbox = None effective_transform = Transform(transform) @ self.transform for child in self: if isinstance(child, ShapeElement): child_bbox = child.shape_box(transform=effective_transform) if child_bbox is not None: bbox += child_bbox return bbox def bake_transforms_recursively(self, apply_to_paths=True): """Bake transforms, i.e. each leaf node has the effective transform (starting from this group) set, and parent transforms are removed. .. versionadded:: 1.4 Args: apply_to_paths (bool, optional): For path elements, the path data is transformed with its effective transform. Nodes and handles will have the same position as before, but visual appearance of the stroke may change (stroke-width is not touched). Defaults to True. """ # pylint: disable=attribute-defined-outside-init self.transform: Transform for element in self: if isinstance(element, PathElement) and apply_to_paths: element.path = element.path.transform(self.transform) else: element.transform = self.transform @ element.transform if isinstance(element, GroupBase): element.bake_transforms_recursively(apply_to_paths) self.transform = None 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().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, elem): # type: (etree.Element) -> bool return ( elem.get("{http://www.inkscape.org/namespaces/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().new(*children, **attrs) class ClipPath(GroupBase): """A path used to clip objects""" tag_name = "clipPath" class Marker(GroupBase, ViewboxMixin): """The element defines the graphic that is to be used for drawing arrowheads or polymarkers on a given , , or element.""" tag_name = "marker" def get_viewbox(self) -> List[float]: """Returns the viewbox of the Marker, falling back to [0 0 markerWidth markerHeight] .. versionadded:: 1.3""" vbox = self.get("viewBox", None) result = self.parse_viewbox(vbox) if result is None: # use viewport, https://www.w3.org/TR/SVG11/painting.html#MarkerElement return [ 0, 0, self.to_dimensionless(self.get("markerWidth")), self.to_dimensionless(self.get("markerHeight")), ] return result