diff options
Diffstat (limited to 'share/extensions/other/extension-xaml/inkxaml/xamlobjects.py')
-rw-r--r-- | share/extensions/other/extension-xaml/inkxaml/xamlobjects.py | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/share/extensions/other/extension-xaml/inkxaml/xamlobjects.py b/share/extensions/other/extension-xaml/inkxaml/xamlobjects.py new file mode 100644 index 0000000..440a75c --- /dev/null +++ b/share/extensions/other/extension-xaml/inkxaml/xamlobjects.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# Copyright (C) 2020-2022 Jonathan Neuhauser <jonathan.neuhauser@outlook.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. + +"""XAML Object structure and XAML (LXML) parsing utilities""" + + +from __future__ import annotations +from typing import Optional, Union, List, Dict, Callable +import random +import re + +Number = Union[int, float] + +from lxml import etree +import inkex +from inkex.elements._utils import removeNS as inkexRemoveNS + +from .export.config import GLOBAL_SETTINGS + + +class NodeBasedXAMLLookup(etree.PythonElementClassLookup): + """Lookup for etree custom element classes""" + + def lookup(self, doc, element): # pylint: disable=unused-argument + """Lookup called by lxml when assigning elements their object class""" + try: + result = globals()[removeNS(element.tag)[-1]] + except TypeError: + result = None + except KeyError: + result = BaseElement + return result + + +XAML_PARSER = etree.XMLParser(huge_tree=True) +XAML_PARSER.set_element_class_lookup(NodeBasedXAMLLookup()) + + +def addNS(tag, ns=None): # pylint: disable=invalid-name + """Add a known namespace to a name for use with lxml""" + return inkex.addNS(tag, ns, GLOBAL_SETTINGS.xaml_namespaces) + + +def removeNS(name): # pylint: disable=invalid-name + """The reverse of addNS, finds any namespace and returns tuple (ns, tag)""" + return inkexRemoveNS(name, GLOBAL_SETTINGS.xaml_namespaces_reversed, default="xaml") + + +KEY_ATTR = "x:Key" + + +class XAMLObject(etree.ElementBase): + """Base class for XAML objects""" + + PARSER = XAML_PARSER + NAMESPACE = property(lambda self: GLOBAL_SETTINGS.xaml_namespaces["xaml"]) + TAG = property(lambda self: removeNS(self.classtag())[-1]) + + @classmethod + def classtag(cls) -> str: + """Returns the xml tag of this class""" + return cls.__name__ + + @property + def tag(self) -> str: + """Returns the xml tag of this element (i.e. the tag of the elements's class)""" + return self.__class__.classtag() + + @staticmethod + def sanitize_name(name): + """Replace all non-alphanumeric characters; more precise validation would be + https://docs.microsoft.com/en-us/dotnet/desktop/xaml-services/xamlname-grammar + also ensure that the first character is not a number""" + result = re.sub("[^0-9a-zA-Z]", "_", name) + return re.sub(r"^(\d.*)", r"_\1", result) + + @staticmethod + def make_key_unique(parent, element: BaseElement, key=KEY_ATTR, force=True): + """Makes sure that an element does not create a key collision. If so, the key is + changed to the existing key (or the element tag if not set) + underscore + + random number""" + current_key = element.get(key, None) + if current_key is not None: + current_key = BaseElement.sanitize_name(current_key) + element.set(key, current_key) + newkey = False + if current_key is None: + if force: + newkey = True + current_key = element.tag + else: + # No key is set, there is no danger for collision. + return + + xpath = f"//*[@{key}='{element.get(key)}']" + elements = parent.xpath(xpath) + if len(elements) > 2 or (len(elements) == 1 and elements[0] != element): + newkey = True + temp_key = current_key + while newkey or len(parent.xpath(f"//*[@{key}='{temp_key}']")) > 0: + temp_key = f"{current_key}_{random.randint(0, 1000)}" + newkey = False + element.set(addNS(key), temp_key) + + def get_tag_name(self, prop: str) -> str: + """Get the tag name for a property""" + prop = ":".join(removeNS(prop)) + if prop.startswith("xaml:"): + prop = prop[5:] + if ":" in prop: + return prop + return f"{self.tag}.{prop}" + + def set(self, name, val): + """Check if the attribute is already set, in this case delete it. + Otherwise set it.""" + self.remove_property(name) + if isinstance(val, str): + super().set(addNS(name), val) + elif isinstance(val, (int, float)): + super().set(addNS(name), str(val)) + elif isinstance(val, list): + super().set(addNS(name), ",".join(map(str, val))) + elif isinstance(val, BaseElement): + # Order of children: + # Property Element(s) -> text -> child1 -> child1.tail -> c2 -> c2.tail + prophandle = etree.Element(self.get_tag_name(name)) + self.insert(0, prophandle) + if self.text != "": + prophandle.tail = self.text + self.text = "" + prophandle.append(val) + + def remove_property(self, name): + """Remove a property""" + for node in self: + if super(etree.ElementBase, node).tag == self.get_tag_name(name): + self.remove(node) + if addNS(name) in self.keys(): + self.attrib.pop(addNS(name)) # pylint: disable=no-member + + def get(self, name, default=None): + if ":" not in name: + for node in self.xpath("/" + self.get_tag_name(name)): + return node + return super().get(addNS(name), default) + + def add_property( + self, + name: str, + val: Optional[Union[str, Number, XAMLObject, List[Number]]], + default=None, + ): + """Add a property if not None""" + if val is not None: + if val == default: + self.remove_property(name) + else: + self.set(name, val) + + def add_properties( + self, properties: Dict[str, Union[str, XAMLObject, Number, List[Number]]] + ): + """Add multiple properties if not None""" + for key, value in properties.items(): + self.add_property(key, value) + + def add_children(self, children: List[XAMLObject]): + """Append multiple children""" + for child in children: + self.append(child) + + def add_name(self, name: str): + """Adding names lead to an exception when parsing the XAML if it's wrapped in a + resource dictionary.""" + + @property + def key(self): + """Get the objects' x:Key attribute.""" + return self.get(KEY_ATTR, None) + + @property + def root(self): + """Get the root ResourceDictionary from any element descendent""" + if self.getparent() is not None: + return self.getparent().root + if not isinstance(self, ResourceDictionary): + raise ValueError( + "Element fragment does not have a ResourceDictionary root!" + ) + return self + + def add_child(self, element: Optional[XAMLObject], front=False): + """Append/insert a child if not none""" + if element is None: + return + if not front: + self.append(element) + else: + # TODO append after property elements + self.insert(0, element) + + @staticmethod + def remove_key(element): + """remove the x:key attribute if set""" + if element.get(KEY_ATTR, None) is not None: + element.attrib.pop(addNS(KEY_ATTR)) + + def append(self, element): + """Append without key (key is only valid inside ResourceDictionary)""" + BaseElement.remove_key(element) + self.make_key_unique(self, element, "Name", False) + super().append(element) + + def insert(self, index, element): + """Insert without key (key is only valid inside ResourceDictionary)""" + BaseElement.remove_key(element) + self.make_key_unique(self, element, "Name", False) + super().insert(index, element) + + def xpath(self, expr, namespaces=None): + return super().xpath( + expr, namespaces=namespaces or GLOBAL_SETTINGS.xaml_namespaces + ) + + def parse_transform(self, attribute="Transform") -> inkex.Transform: + """Reads the transform attribute and returns an inkex.Transform object. + This currently does not support TransformGroup""" + read = self.get(attribute, None) + if read is None: + return inkex.Transform() + return inkex.Transform(f"matrix({read})") + + def __str__(self): + return f"{self.TAG} element with {len(self.getchildren())} children" + + def set_invisible(self): + """Makes an element invisible""" + self.add_property("ClipGeometry", "") + + @staticmethod + def transform_to_xaml(transform: inkex.Transform): + """Converts an inkex Transform struct to an XAML object representation""" + as_string = ",".join(map(str, transform.to_hexad())) + if GLOBAL_SETTINGS.avalonia: + result = MatrixTransform() + result.set("Matrix", as_string) + return result + return as_string + + def set_transform(self, transform: inkex.Transform): + """Sets a transform on an object""" + if transform != inkex.Transform(): # don't add the identity transform + self.add_property("Transform", self.transform_to_xaml(transform)) + + +class BaseElement(XAMLObject): + """Superclass for any XAML object""" + + +class ResourceDictionary(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.resourcedictionary""" + + def insert_wrapper(self, element: BaseElement, insert_function: Callable): + """Inserting with unique key""" + self.make_key_unique(self, element) + key = element.get(KEY_ATTR, None) + result = insert_function(element) + element.set(addNS(KEY_ATTR), key) + return result + + def append(self, element: BaseElement): + """Appending with unique key""" + return self.insert_wrapper(element, super().append) + + def insert(self, index: int, element: BaseElement): + """Inserting with unique key""" + inserter = super().insert + return self.insert_wrapper(element, lambda el: inserter(index, el)) + + def get_child(self, key): + """Finds a direct subchild based on key""" + for i in self.iterdescendants(): + if i.get(KEY_ATTR) == key: + return i + return None + + +class Drawing(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.drawing""" + + +class DrawingGroup(Drawing): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.drawinggroup""" + + +class GeometryDrawing(Drawing): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.geometrydrawing""" + + +class Geometry(Drawing): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.geometry""" + + +class PathGeometry(Geometry): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.pathgeometry""" + + +class LineGeometry(Geometry): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.linegeometry""" + + +class RectangleGeometry(Geometry): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.rectanglegeometry""" + + +class EllipseGeometry(Geometry): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.ellipsegeometry""" + + +class Brush(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.brush""" + + +class SolidColorBrush(Brush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.solidcolorbrush""" + + +class DrawingBrush(Brush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.drawingbrush""" + + +class GradientBrush(Brush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.gradientbrush""" + + +class LinearGradientBrush(GradientBrush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.lineargradientbrush""" + + +class RadialGradientBrush(GradientBrush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.radialgradientbrush""" + + +class VisualBrush(Brush): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.visualbrush""" + + +class Pen(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.pen""" + + +class DashStyle(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.dashstyle""" + + +class GradientStop(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.gradientstop""" + + +class DrawingImage(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.drawingimage""" + + +class UIElement(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.controls.uielement""" + + def set_invisible(self): + if GLOBAL_SETTINGS.wpf: + self.add_property("Visibility", "Hidden") + else: + self.add_property("IsVisible", "False") + + def set_transform(self, transform: inkex.Transform): + if transform != inkex.Transform(): + self.add_property("RenderTransform", self.transform_to_xaml(transform)) + if GLOBAL_SETTINGS.avalonia: + self.add_property("RenderTransformOrigin", "0,0") + + +class Canvas(UIElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.controls.canvas""" + + +class Viewbox(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.controls.viewbox""" + + +class Shape(UIElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.shape""" + + +class Path(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.path""" + + +class Rectangle(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.rectangle""" + + +class Polygon(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.polygon""" + + +class Polyline(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.polyline""" + + +class Line(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.line""" + + +class Ellipse(Shape): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.shapes.ellipse""" + + +class GeometryGroup(Geometry): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.geometrygroup""" + + +class MatrixTransform(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.matrixtransform""" + + +class BlurEffect(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.media.effects.blureffect""" + + +class TextBlock(UIElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.controls.textblock""" + + +class Span(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.documents.span""" + + +class LineBreak(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.documents.linebreak""" + + +class TextDecoration(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.textdecoration""" + + +class TextDecorationCollection(BaseElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.textdecorationcollection""" + + +class Grid(UIElement): + """Interface for + https://docs.microsoft.com/dotnet/api/system.windows.controls.grid""" |