summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/elements/_meta.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/inkex/elements/_meta.py')
-rw-r--r--share/extensions/inkex/elements/_meta.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/share/extensions/inkex/elements/_meta.py b/share/extensions/inkex/elements/_meta.py
new file mode 100644
index 0000000..e9eeb0f
--- /dev/null
+++ b/share/extensions/inkex/elements/_meta.py
@@ -0,0 +1,293 @@
+# -*- 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.
+"""
+
+from __future__ import annotations
+import math
+
+from typing import Optional
+
+from lxml import etree
+
+from ..styles import StyleSheet
+from ..transforms import Vector2d, VectorLike, DirectedLineSegment
+
+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.viewport_to_unit(self.get("inkscape:cx") or 0),
+ self.root.viewport_to_unit(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
+
+ Args:
+ position: a float containing the y position for ``orient is True``, or
+ the x position for ``orient is False``
+
+ .. versionchanged:: 1.2
+ Alternatively, the position may be given as Tuple (or VectorLike)
+ orient: True for horizontal, False for Vertical
+
+ .. versionchanged:: 1.2
+ Tuple / Vector specifying x and y coordinates of the normal vector
+ of the guide.
+ name: label of the guide
+
+ Returns:
+ the created guide"""
+ if orient is True:
+ elem = Guide().move_to(0, position, (0, 1))
+ elif orient is False:
+ elem = Guide().move_to(position, 0, (1, 0))
+ else:
+ elem = Guide().move_to(*position, orient)
+ if name:
+ elem.set("inkscape:label", str(name))
+ return self.add(elem)
+
+ def new_unique_guide(
+ self, position: VectorLike, orientation: VectorLike
+ ) -> Optional[Guide]:
+ """Add a guide iif there is no guide that looks the same.
+
+ .. versionadded:: 1.2"""
+ elem = Guide().move_to(position[0], position[1], orientation)
+ return self.add(elem) if self.get_similar_guide(elem) is None else None
+
+ def get_similar_guide(self, other: Guide) -> Optional[Guide]:
+ """Check if the namedview contains a guide that looks identical to one
+ defined by (position, orientation). If such a guide exists, return it;
+ otherwise, return None.
+
+ .. versionadded:: 1.2"""
+ for guide in self.get_guides():
+ if Guide.guides_coincident(guide, other):
+ return guide
+ return None
+
+ def get_pages(self):
+ """Returns a list of pages
+
+ .. versionadded:: 1.2"""
+ return self.findall("inkscape:page")
+
+ def new_page(self, x, y, width, height, label=None):
+ """Creates a new page in this namedview
+
+ .. versionadded:: 1.2"""
+ elem = Page(width=width, height=height, x=x, y=y)
+ if label:
+ elem.set("inkscape:label", str(label))
+ return self.add(elem)
+
+
+class Guide(BaseElement):
+ """An inkscape guide"""
+
+ tag_name = "sodipodi:guide"
+
+ @property
+ def orientation(self) -> Vector2d:
+ """Vector normal to the guide
+
+ .. versionadded:: 1.2"""
+ return Vector2d(self.get("orientation"), fallback=(1, 0))
+
+ is_horizontal = property(
+ lambda self: self.orientation[0] == 0 and self.orientation[1] != 0
+ )
+ is_vertical = property(
+ lambda self: self.orientation[0] != 0 and self.orientation[1] == 0
+ )
+
+ @property
+ def point(self) -> Vector2d:
+ """Position of the guide handle. The y coordinate is flipped and relative
+ to the bottom of the viewbox, this is a remnant of the pre-1.0 coordinate system
+ """
+ return Vector2d(self.get("position"), fallback=(0, 0))
+
+ @classmethod
+ def new(cls, pos_x, pos_y, angle, **attrs):
+ guide = super().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.
+ If not given at all, the orientation remains unchanged.
+ """
+ self.set("position", f"{float(pos_x):g},{float(pos_y):g}")
+ 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 = ",".join(f"{i:g}" for i in angle)
+
+ if angle is not None:
+ self.set("orientation", angle)
+ return self
+
+ @staticmethod
+ def guides_coincident(guide1, guide2):
+ """Check if two guides defined by (position, orientation) and (opos, oor) look
+ identical (i.e. the position lies on the other guide AND the guide is
+ (anti)parallel to the other guide).
+
+ .. versionadded:: 1.2"""
+ # normalize orientations first
+ orientation = guide1.orientation / guide1.orientation.length
+ oor = guide2.orientation / guide2.orientation.length
+
+ position = guide1.point
+ opos = guide2.point
+
+ return (
+ DirectedLineSegment(
+ position, position + Vector2d(orientation[1], -orientation[0])
+ ).perp_distance(*opos)
+ < 1e-6
+ and abs(abs(orientation[1] * oor[0]) - abs(orientation[0] * oor[1])) < 1e-6
+ )
+
+
+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"
+
+
+class Page(BaseElement):
+ """A namedview page child
+
+ .. versionadded:: 1.2"""
+
+ tag_name = "inkscape:page"
+
+ width = property(lambda self: self.to_dimensionless(self.get("width") or 0))
+ height = property(lambda self: self.to_dimensionless(self.get("height") or 0))
+ x = property(lambda self: self.to_dimensionless(self.get("x") or 0))
+ y = property(lambda self: self.to_dimensionless(self.get("y") or 0))
+
+ @classmethod
+ def new(cls, width, height, x, y):
+ """Creates a new page element in the namedview"""
+ page = super().new()
+ page.move_to(x, y)
+ page.set("width", width)
+ page.set("height", height)
+ return page
+
+ def move_to(self, x, y):
+ """Move this page to the given x,y position"""
+ self.set("position", f"{float(x):g},{float(y):g}")
+ return self