Browse Source

Merge pull request #701 from PixiEditor/svg-import

Basic SVG import
Krzysztof Krysiński 8 months ago
parent
commit
6c072b7751
60 changed files with 1726 additions and 200 deletions
  1. 1 1
      src/Drawie
  2. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  3. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  4. 12 0
      src/PixiEditor.SVG/Attributes/SvgValueAttribute.cs
  5. 6 0
      src/PixiEditor.SVG/Elements/SvgCircle.cs
  6. 8 1
      src/PixiEditor.SVG/Elements/SvgEllipse.cs
  7. 8 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  8. 8 1
      src/PixiEditor.SVG/Elements/SvgImage.cs
  9. 7 0
      src/PixiEditor.SVG/Elements/SvgLine.cs
  10. 8 1
      src/PixiEditor.SVG/Elements/SvgMask.cs
  11. 9 1
      src/PixiEditor.SVG/Elements/SvgPath.cs
  12. 0 8
      src/PixiEditor.SVG/Elements/SvgPolyline.cs
  13. 20 2
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  14. 10 0
      src/PixiEditor.SVG/Elements/SvgRectangle.cs
  15. 12 0
      src/PixiEditor.SVG/Enums/SvgFillRule.cs
  16. 8 0
      src/PixiEditor.SVG/Exceptions/SvgParsingException.cs
  17. 2 1
      src/PixiEditor.SVG/Features/IFillable.cs
  18. 5 0
      src/PixiEditor.SVG/Helpers/StringExtensions.cs
  19. 84 0
      src/PixiEditor.SVG/StyleContext.cs
  20. 79 14
      src/PixiEditor.SVG/SvgDocument.cs
  21. 66 18
      src/PixiEditor.SVG/SvgElement.cs
  22. 174 0
      src/PixiEditor.SVG/SvgParser.cs
  23. 1 1
      src/PixiEditor.SVG/SvgProperty.cs
  24. 0 11
      src/PixiEditor.SVG/Units/SvgArray.cs
  25. 35 2
      src/PixiEditor.SVG/Units/SvgColorUnit.cs
  26. 38 2
      src/PixiEditor.SVG/Units/SvgEnumUnit.cs
  27. 8 0
      src/PixiEditor.SVG/Units/SvgLinkUnit.cs
  28. 19 0
      src/PixiEditor.SVG/Units/SvgList.cs
  29. 50 1
      src/PixiEditor.SVG/Units/SvgNumericUnit.cs
  30. 34 0
      src/PixiEditor.SVG/Units/SvgRectUnit.cs
  31. 5 0
      src/PixiEditor.SVG/Units/SvgStringUnit.cs
  32. 24 0
      src/PixiEditor.SVG/Units/SvgTransformUnit.cs
  33. 1 0
      src/PixiEditor.SVG/Units/SvgUnit.cs
  34. 146 0
      src/PixiEditor.SVG/Utils/SvgColorUtility.cs
  35. 156 0
      src/PixiEditor.SVG/Utils/WellKnownColorNames.cs
  36. 11 1
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  37. 3 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  38. 5 0
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  39. 18 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  40. 3 1
      src/PixiEditor/Models/Files/SvgFileType.cs
  41. 9 0
      src/PixiEditor/Models/IO/CustomDocumentFormats/IDocumentBuilder.cs
  42. 220 0
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  43. 0 16
      src/PixiEditor/Models/IO/Importer.cs
  44. 5 0
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  45. 9 0
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  46. 2 0
      src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs
  47. 1 0
      src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs
  48. 3 1
      src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs
  49. 3 1
      src/PixiEditor/Models/Serialization/Factories/RectangleSerializationFactory.cs
  50. 3 1
      src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs
  51. 18 1
      src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs
  52. 26 27
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  53. 32 13
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  54. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  55. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  56. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  57. 3 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  58. 42 63
      src/PixiEditor/Views/Visuals/PixiFilePreviewImage.cs
  59. 0 2
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml
  60. 253 0
      tests/PixiEditor.Tests/SvgTests.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 5e6f8e7762ea56e299bf6ab1a880996a7b856fef
+Subproject commit 1124f58f7ca5b4312aeb0ef8e22ceea7103a9bb2

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs

@@ -8,6 +8,7 @@ public interface IReadOnlyShapeVectorData
 {
     public Matrix3X3 TransformationMatrix { get; }
     public Color StrokeColor { get; }
+    public bool Fill { get; }
     public Color FillColor { get; }
     public float StrokeWidth { get; }
     public RectD GeometryAABB { get; }

+ 6 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -34,6 +34,12 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         Center = center;
         Size = size;
     }
+    
+    public RectangleVectorData(double x, double y, double width, double height)
+    {
+        Center = new VecD(x + width / 2, y + height / 2);
+        Size = new VecD(width, height);
+    }
 
     public override void RasterizeGeometry(DrawingSurface drawingSurface)
     {

+ 12 - 0
src/PixiEditor.SVG/Attributes/SvgValueAttribute.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.SVG.Attributes;
+
+[AttributeUsage(AttributeTargets.Field)]
+public class SvgValueAttribute : Attribute
+{
+    public string Value { get; }
+
+    public SvgValueAttribute(string value)
+    {
+        Value = value;
+    }
+}

+ 6 - 0
src/PixiEditor.SVG/Elements/SvgCircle.cs

@@ -8,4 +8,10 @@ public class SvgCircle() : SvgPrimitive("circle")
     public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy");
 
     public SvgProperty<SvgNumericUnit> R { get; } = new("r");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return Cx;
+        yield return Cy;
+        yield return R;
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgEllipse.cs

@@ -9,5 +9,12 @@ public class SvgEllipse() : SvgPrimitive("ellipse")
     public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy"); 
     
     public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx"); 
-    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry"); 
+    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return Cx;
+        yield return Cy;
+        yield return Rx;
+        yield return Ry;
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Features;
+using System.Xml;
+using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
@@ -10,4 +11,10 @@ public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth };
+        ParseAttributes(properties, reader);
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgImage.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Enums;
+using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
@@ -19,4 +20,10 @@ public class SvgImage : SvgElement
     {
         RequiredNamespaces.Add("xlink", "http://www.w3.org/1999/xlink");
     }
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { X, Y, Width, Height, Href, Mask, ImageRendering };
+        ParseAttributes(properties, reader);
+    }
 }

+ 7 - 0
src/PixiEditor.SVG/Elements/SvgLine.cs

@@ -9,4 +9,11 @@ public class SvgLine() : SvgPrimitive("line")
     
     public SvgProperty<SvgNumericUnit> X2 { get; } = new("x2");
     public SvgProperty<SvgNumericUnit> Y2 { get; } = new("y2");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return X1;
+        yield return Y1;
+        yield return X2;
+        yield return Y2;
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgMask.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Features;
+using System.Xml;
+using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
@@ -11,4 +12,10 @@ public class SvgMask() : SvgElement("mask"), IElementContainer
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
     public List<SvgElement> Children { get; } = new();
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { X, Y, Width, Height };
+        ParseAttributes(properties, reader);
+    }
 }

+ 9 - 1
src/PixiEditor.SVG/Elements/SvgPath.cs

@@ -1,8 +1,16 @@
-using PixiEditor.SVG.Units;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
 public class SvgPath() : SvgPrimitive("path")
 {
     public SvgProperty<SvgStringUnit> PathData { get; } = new("d");
+    public SvgProperty<SvgEnumUnit<SvgFillRule>> FillRule { get; } = new("fill-rule");
+
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return PathData;
+        yield return FillRule;
+    }
 }

+ 0 - 8
src/PixiEditor.SVG/Elements/SvgPolyline.cs

@@ -1,8 +0,0 @@
-using PixiEditor.SVG.Units;
-
-namespace PixiEditor.SVG.Elements;
-
-public class SvgPolyline() : SvgPrimitive("polyline")
-{
-    public SvgArray<SvgNumericUnit> Points { get; } = new SvgArray<SvgNumericUnit>("points");
-}

+ 20 - 2
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -1,12 +1,30 @@
-using PixiEditor.SVG.Features;
+using System.Xml;
+using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
-public class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
+public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
 {
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = GetProperties().ToList();
+        
+        properties.Add(Transform);
+        properties.Add(Fill);
+        properties.Add(Stroke);
+        properties.Add(StrokeWidth);
+
+        do
+        {
+            ParseAttributes(properties, reader);
+        } while (reader.MoveToNextAttribute());
+    }
+
+    protected abstract IEnumerable<SvgProperty> GetProperties();
 }

+ 10 - 0
src/PixiEditor.SVG/Elements/SvgRectangle.cs

@@ -12,4 +12,14 @@ public class SvgRectangle() : SvgPrimitive("rect")
     
     public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx");
     public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry");
+    
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return X;
+        yield return Y;
+        yield return Width;
+        yield return Height;
+        yield return Rx;
+        yield return Ry;
+    }
 }

+ 12 - 0
src/PixiEditor.SVG/Enums/SvgFillRule.cs

@@ -0,0 +1,12 @@
+using PixiEditor.SVG.Attributes;
+
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgFillRule
+{
+    [SvgValue("nonzero")]
+    NonZero,
+    
+    [SvgValue("evenodd")]
+    EvenOdd
+}

+ 8 - 0
src/PixiEditor.SVG/Exceptions/SvgParsingException.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.SVG.Exceptions;
+
+public class SvgParsingException : Exception
+{
+    public SvgParsingException(string message) : base(message)
+    {
+    }
+}

+ 2 - 1
src/PixiEditor.SVG/Features/IFillable.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Units;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Features;
 

+ 5 - 0
src/PixiEditor.SVG/Helpers/StringExtensions.cs

@@ -6,4 +6,9 @@ public static class StringExtensions
     {
         return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLower();
     }
+    
+    public static string FromKebabToTitleCase(this string str)
+    {
+        return string.Concat(str.Split('-').Select(x => char.ToUpper(x[0]) + x.Substring(1)));
+    }
 }

+ 84 - 0
src/PixiEditor.SVG/StyleContext.cs

@@ -0,0 +1,84 @@
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG;
+
+public struct StyleContext
+{
+    public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
+    public SvgProperty<SvgColorUnit> Stroke { get; }
+    public SvgProperty<SvgColorUnit> Fill { get; }
+    public SvgProperty<SvgTransformUnit> Transform { get; }
+
+    public StyleContext()
+    {
+        StrokeWidth = new("stroke-width");
+        Stroke = new("stroke");
+        Fill = new("fill");
+        Transform = new("transform");
+    }
+    
+    public StyleContext(SvgDocument document)
+    {
+        StrokeWidth = document.StrokeWidth;
+        Stroke = document.Stroke;
+        Fill = document.Fill;
+        Transform = document.Transform;
+    }
+
+    public StyleContext WithElement(SvgElement element)
+    {
+        StyleContext styleContext = Copy();
+
+        if (element is ITransformable { Transform.Unit: not null } transformableElement)
+        {
+            styleContext.Transform.Unit = transformableElement.Transform.Unit;
+        }
+
+        if (element is IFillable { Fill.Unit: not null } fillableElement)
+        {
+            styleContext.Fill.Unit = fillableElement.Fill.Unit;
+        }
+
+        if (element is IStrokable strokableElement)
+        {
+            if (strokableElement.Stroke.Unit != null)
+            {
+                styleContext.Stroke.Unit = strokableElement.Stroke.Unit;
+            }
+
+            if (strokableElement.StrokeWidth.Unit != null)
+            {
+                styleContext.StrokeWidth.Unit = strokableElement.StrokeWidth.Unit;
+            }
+        }
+
+        return styleContext;
+    }
+
+    private StyleContext Copy()
+    {
+        StyleContext styleContext = new();
+        if (StrokeWidth.Unit != null)
+        {
+            styleContext.StrokeWidth.Unit = StrokeWidth.Unit;
+        }
+
+        if (Stroke.Unit != null)
+        {
+            styleContext.Stroke.Unit = Stroke.Unit;
+        }
+
+        if (Fill.Unit != null)
+        {
+            styleContext.Fill.Unit = Fill.Unit;
+        }
+
+        if (Transform.Unit != null)
+        {
+            styleContext.Transform.Unit = Transform.Unit;
+        }
+
+        return styleContext;
+    }
+}

+ 79 - 14
src/PixiEditor.SVG/SvgDocument.cs

@@ -1,22 +1,55 @@
-using System.Text;
+using System.Xml;
+using System.Xml.Linq;
 using Drawie.Numerics;
 using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG;
 
-public class SvgDocument(RectD viewBox) : IElementContainer
+public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable
 {
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string Version { get; set; } = "1.1";
-    public RectD ViewBox { get; set; } = viewBox;
+    
+    public SvgProperty<SvgRectUnit> ViewBox { get; } = new("viewBox");
+    public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
+    public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+    public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public List<SvgElement> Children { get; } = new();
 
+    public SvgDocument() : base("svg")
+    {
+        
+    }
+    
+    public SvgDocument(RectD viewBox) : base("svg")
+    {
+        ViewBox.Unit = new SvgRectUnit(viewBox);
+    }
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new()
+        {
+            Fill,
+            Stroke,
+            StrokeWidth,
+            Transform
+        };
+        
+        ParseAttributes(properties, reader);
+    }
+
     public string ToXml()
     {
-        StringBuilder builder = new();
-        builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
-        builder.AppendLine(
-            $"<svg xmlns=\"{RootNamespace}\" version=\"{Version}\" viewBox=\"{ViewBox.X} {ViewBox.Y} {ViewBox.Width} {ViewBox.Height}\"");
+        XDocument document = new XDocument();
+        document.Declaration = new XDeclaration("1.0", "UTF-8", "yes");
+        XNamespace ns = RootNamespace;
+        document.Add(new XElement(
+            ns + "svg",
+            new XAttribute("version", Version))
+        );
 
         Dictionary<string, string> usedNamespaces = new();
 
@@ -24,20 +57,23 @@ public class SvgDocument(RectD viewBox) : IElementContainer
 
         foreach (var usedNamespace in usedNamespaces)
         {
-            builder.AppendLine(
-                $"xmlns:{usedNamespace.Key}=\"{usedNamespace.Value}\"");
+            document.Root.Add(new XAttribute($"xmlns:{usedNamespace.Key}", usedNamespace.Value));
         }
-        
-        builder.AppendLine(">");
+
+        AppendProperties(document.Root);
 
         foreach (SvgElement child in Children)
         {
-            builder.AppendLine(child.ToXml());
+            document.Root.Add(child.ToXml(ns));
         }
 
-        builder.AppendLine("</svg>");
+        return document.ToString();
+    }
 
-        return builder.ToString();
+    public static SvgDocument Parse(string xml)
+    {
+        SvgParser parser = new(xml);
+        return parser.Parse();
     }
 
     private void GatherRequiredNamespaces(Dictionary<string, string> usedNamespaces, List<SvgElement> elements)
@@ -48,10 +84,39 @@ public class SvgDocument(RectD viewBox) : IElementContainer
             {
                 GatherRequiredNamespaces(usedNamespaces, container.Children);
             }
+
             foreach (KeyValuePair<string, string> ns in child.RequiredNamespaces)
             {
                 usedNamespaces[ns.Key] = ns.Value;
             }
         }
     }
+
+    private void AppendProperties(XElement? root)
+    {
+        if(ViewBox.Unit != null)
+        {
+            root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml()));
+        }
+        
+        if (Fill.Unit != null)
+        {
+            root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml()));
+        }
+
+        if (Stroke.Unit != null)
+        {
+            root.Add(new XAttribute("stroke", Stroke.Unit.Value.ToXml()));
+        }
+
+        if (StrokeWidth.Unit != null)
+        {
+            root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml()));
+        }
+        
+        if (Transform.Unit != null)
+        {
+            root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
+        }
+    }
 }

+ 66 - 18
src/PixiEditor.SVG/SvgElement.cs

@@ -1,4 +1,7 @@
 using System.Text;
+using System.Xml;
+using System.Xml.Linq;
+using PixiEditor.SVG.Exceptions;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
@@ -10,41 +13,86 @@ public class SvgElement(string tagName)
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public string TagName { get; } = tagName;
 
-    public string ToXml()
+
+    public XElement ToXml(XNamespace nameSpace)
     {
-        StringBuilder builder = new();
-        builder.Append($"<{TagName}");
+        XElement element = new XElement(nameSpace + TagName);
 
         foreach (var property in GetType().GetProperties())
         {
             if (property.PropertyType.IsAssignableTo(typeof(SvgProperty)))
             {
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
-                if (prop != null)
+                if (prop?.Unit != null)
                 {
-                    if (prop.Unit != null)
-                    {
-                        builder.Append($" {prop.SvgName}=\"{prop.Unit.ToXml()}\"");
-                    }
+                    element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
                 }
             }
         }
-        
-        if (this is not IElementContainer container)
+
+        if (this is IElementContainer container)
         {
-            builder.Append(" />");
+            foreach (SvgElement child in container.Children)
+            {
+                element.Add(child.ToXml(nameSpace));
+            }
         }
-        else
+        
+        return element;
+    }
+
+    public virtual void ParseData(XmlReader reader)
+    {
+        // This is supposed to be overriden by child classes
+        throw new SvgParsingException($"Element {TagName} does not support parsing");
+    }
+
+    protected void ParseAttributes(List<SvgProperty> properties, XmlReader reader)
+    {
+        do
         {
-            builder.Append(">");
-            foreach (SvgElement child in container.Children)
+            SvgProperty matchingProperty = properties.FirstOrDefault(x =>
+                string.Equals(x.SvgName, reader.Name, StringComparison.OrdinalIgnoreCase));
+            if (matchingProperty != null)
             {
-                builder.AppendLine(child.ToXml());
+                ParseAttribute(matchingProperty, reader);
             }
-            
-            builder.Append($"</{TagName}>");
+        } while (reader.MoveToNextAttribute());
+    }
+
+    private void ParseAttribute(SvgProperty property, XmlReader reader)
+    {
+        if (property is SvgList list)
+        {
+            ParseListProperty(list, reader);
+        }
+        else
+        {
+            property.Unit ??= CreateDefaultUnit(property);
+            property.Unit.ValuesFromXml(reader.Value);
+        }
+    }
+    
+    private void ParseListProperty(SvgList list, XmlReader reader)
+    {
+        list.Unit ??= CreateDefaultUnit(list);
+        list.Unit.ValuesFromXml(reader.Value);
+    }
+
+    private ISvgUnit CreateDefaultUnit(SvgProperty property)
+    {
+        var genericType = property.GetType().GetGenericArguments();
+        if (genericType.Length == 0)
+        {
+            throw new InvalidOperationException("Property does not have a generic type");
+        }
+
+        ISvgUnit unit = Activator.CreateInstance(genericType[0]) as ISvgUnit;
+        if (unit == null)
+        {
+            throw new InvalidOperationException("Could not create unit");
         }
 
-        return builder.ToString();
+        return unit;
     }
 }

+ 174 - 0
src/PixiEditor.SVG/SvgParser.cs

@@ -0,0 +1,174 @@
+using System.Xml;
+using System.Xml.Linq;
+using Drawie.Numerics;
+using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG;
+
+public class SvgParser
+{
+    private static Dictionary<string, Type> wellKnownElements = new()
+    {
+        { "ellipse", typeof(SvgEllipse) },
+        { "rect", typeof(SvgRectangle) },
+        { "circle", typeof(SvgCircle) },
+        { "line", typeof(SvgLine) },
+        { "path", typeof(SvgPath) },
+        { "g", typeof(SvgGroup) },
+        { "mask", typeof(SvgMask) },
+        { "image", typeof(SvgImage) },
+        { "svg", typeof(SvgDocument) }
+    };
+
+    public string Source { get; set; }
+
+    public SvgParser(string xml)
+    {
+        Source = xml;
+    }
+
+    public SvgDocument? Parse()
+    {
+        XDocument document = XDocument.Parse(Source);
+        using var reader = document.CreateReader();
+
+        XmlNodeType node = reader.MoveToContent();
+        if (node != XmlNodeType.Element || reader.Name != "svg")
+        {
+            return null;
+        }
+        
+        SvgDocument root = (SvgDocument)ParseElement(reader)!;
+
+        RectD bounds = ParseBounds(reader); // this takes into account viewBox, width, height, x, y
+        
+        root.ViewBox.Unit = new SvgRectUnit(bounds);
+
+        while (reader.Read())
+        {
+            if (reader.NodeType == XmlNodeType.Element)
+            {
+                SvgElement? element = ParseElement(reader);
+                if (element != null)
+                {
+                    root.Children.Add(element);
+
+                    if (element is IElementContainer container)
+                    {
+                        ParseChildren(reader, container, element.TagName);
+                    }
+                }
+            }
+        }
+
+        return root;
+    }
+
+    private void ParseChildren(XmlReader reader, IElementContainer container, string tagName)
+    {
+        while (reader.Read())
+        {
+            if (reader.NodeType == XmlNodeType.Element)
+            {
+                SvgElement? element = ParseElement(reader);
+                if (element != null)
+                {
+                    container.Children.Add(element);
+
+                    if (element is IElementContainer childContainer)
+                    {
+                        ParseChildren(reader, childContainer, element.TagName);
+                    }
+                }
+            }
+            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == tagName)
+            {
+                break;
+            }
+        }
+    }
+
+    private SvgElement? ParseElement(XmlReader reader)
+    {
+        if (wellKnownElements.TryGetValue(reader.Name, out Type elementType))
+        {
+            SvgElement element = (SvgElement)Activator.CreateInstance(elementType);
+            if (reader.MoveToFirstAttribute())
+            {
+                element.ParseData(reader);
+            }
+
+            return element;
+        }
+
+        return null;
+    }
+
+    private RectD ParseBounds(XmlReader reader)
+    {
+        string viewBox = reader.GetAttribute("viewBox");
+        string width = reader.GetAttribute("width");
+        string height = reader.GetAttribute("height");
+        string x = reader.GetAttribute("x");
+        string y = reader.GetAttribute("y");
+
+        if (viewBox == null && width == null && height == null && x == null && y == null)
+        {
+            return new RectD(0, 0, 0, 0);
+        }
+
+        double finalX = 0;
+        double finalY = 0;
+        double finalWidth = 0;
+        double finalHeight = 0;
+
+        if (viewBox != null)
+        {
+            string[] parts = viewBox.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+            if (parts.Length == 4)
+            {
+                finalX = double.Parse(parts[0]);
+                finalY = double.Parse(parts[1]);
+                finalWidth = double.Parse(parts[2]);
+                finalHeight = double.Parse(parts[3]);
+            }
+        }
+
+        if (x != null)
+        {
+            if (double.TryParse(x, out double xValue))
+            {
+                finalX = xValue;
+            }
+        }
+
+        if (y != null)
+        {
+            if (double.TryParse(y, out double yValue))
+            {
+                finalY = yValue;
+            }
+        }
+
+        if (width != null)
+        {
+            if (double.TryParse(width, out double widthValue))
+            {
+                finalWidth = widthValue;
+            }
+        }
+
+        if (height != null)
+        {
+            if (double.TryParse(height, out double heightValue))
+            {
+                finalHeight = heightValue;
+            }
+        }
+
+
+        return new RectD(finalX, finalY, finalWidth, finalHeight);
+    }
+}

+ 1 - 1
src/PixiEditor.SVG/SvgProperty.cs

@@ -20,7 +20,7 @@ public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
         get => (T?)base.Unit;
         set => base.Unit = value;
     }
-
+    
     public SvgProperty(string svgName) : base(svgName)
     {
     }

+ 0 - 11
src/PixiEditor.SVG/Units/SvgArray.cs

@@ -1,11 +0,0 @@
-namespace PixiEditor.SVG.Units;
-
-public class SvgArray<T> : SvgProperty where T : ISvgUnit
-{
-    public T[] Units { get; set; }
-
-    public SvgArray(string svgName, params T[] units) : base(svgName)
-    {
-        Units = units;
-    }
-}

+ 35 - 2
src/PixiEditor.SVG/Units/SvgColorUnit.cs

@@ -1,8 +1,26 @@
-namespace PixiEditor.SVG.Units;
+using Drawie.Backend.Core.ColorsImpl;
+using PixiEditor.SVG.Exceptions;
+using PixiEditor.SVG.Utils;
+
+namespace PixiEditor.SVG.Units;
 
 public struct SvgColorUnit : ISvgUnit
 {
-    public string Value { get; set; }
+    private string value;
+
+    public string Value
+    {
+        get => value;
+        set
+        {
+            this.value = value;
+            if(SvgColorUtility.TryConvertStringToColor(value, out Color color))
+            {
+                Color = color;
+            }
+        }
+    }
+    public Color Color { get; private set; }
 
     public SvgColorUnit(string value)
     {
@@ -38,4 +56,19 @@ public struct SvgColorUnit : ISvgUnit
     {
         return Value;
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        Value = readerValue;
+    }
+}
+
+public enum SvgColorType
+{
+    Hex,
+    Rgb,
+    Rgba,
+    Hsl,
+    Hsla,
+    Named
 }

+ 38 - 2
src/PixiEditor.SVG/Units/SvgEnumUnit.cs

@@ -1,8 +1,10 @@
-using PixiEditor.SVG.Helpers;
+using System.Reflection;
+using PixiEditor.SVG.Attributes;
+using PixiEditor.SVG.Helpers;
 
 namespace PixiEditor.SVG.Units;
 
-public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
+public struct SvgEnumUnit<T> : ISvgUnit where T : struct, Enum
 {
     public T Value { get; set; }
 
@@ -13,6 +15,40 @@ public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
 
     public string ToXml()
     {
+        FieldInfo field = Value.GetType().GetField(Value.ToString());
+        SvgValueAttribute attribute = field.GetCustomAttribute<SvgValueAttribute>();
+        
+        if (attribute != null)
+        {
+            return attribute.Value;
+        }
+        
         return Value.ToString().ToKebabCase();
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        bool matched = TryMatchEnum(readerValue);
+        if (!matched && Enum.TryParse(readerValue.FromKebabToTitleCase(), out T result))
+        {
+            Value = result;
+        }
+    }
+    
+    private bool TryMatchEnum(string value)
+    {
+        foreach (T enumValue in Enum.GetValues(typeof(T)))
+        {
+            FieldInfo field = enumValue.GetType().GetField(enumValue.ToString());
+            SvgValueAttribute attribute = field.GetCustomAttribute<SvgValueAttribute>();
+            
+            if (attribute != null && attribute.Value == value)
+            {
+                Value = enumValue;
+                return true;
+            }
+        }
+        
+        return false;
+    }
 }

+ 8 - 0
src/PixiEditor.SVG/Units/SvgLinkUnit.cs

@@ -10,6 +10,14 @@ public struct SvgLinkUnit : ISvgUnit
         return ObjectReference != null ? $"url(#{ObjectReference}" : string.Empty;
     }
 
+    public void ValuesFromXml(string readerValue)
+    {
+        if (readerValue.StartsWith("url(#") && readerValue.EndsWith(')'))
+        {
+            ObjectReference = readerValue[5..^1];
+        }
+    }
+
     public static SvgLinkUnit FromElement(SvgElement element)
     {
         return new SvgLinkUnit

+ 19 - 0
src/PixiEditor.SVG/Units/SvgList.cs

@@ -0,0 +1,19 @@
+namespace PixiEditor.SVG.Units;
+
+public class SvgList : SvgProperty
+{
+    public char Separator { get; set; }
+    public SvgList(string svgName, char separator) : base(svgName)
+    {
+        Separator = separator;
+    }
+}
+
+public class SvgList<T> : SvgList where T : ISvgUnit
+{
+
+    public SvgList(string svgName, char separator, params T[] units) : base(svgName, separator)
+    {
+        
+    }
+}

+ 50 - 1
src/PixiEditor.SVG/Units/SvgNumericUnit.cs

@@ -4,7 +4,7 @@ namespace PixiEditor.SVG.Units;
 
 public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
 {
-    public string PostFix { get; } = postFix;
+    public string PostFix { get; set; } = postFix;
     public double Value { get; set; } = value;
 
     public static SvgNumericUnit FromUserUnits(double value)
@@ -42,4 +42,53 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
         string invariantValue = Value.ToString(CultureInfo.InvariantCulture);
         return $"{invariantValue}{PostFix}";
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        string? extractedPostFix = ExtractPostFix(readerValue);
+        
+        if (extractedPostFix == null)
+        {
+            if (double.TryParse(readerValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
+            {
+                Value = result;
+                PostFix = string.Empty;
+            }
+        }
+        else
+        {
+            string value = readerValue.Substring(0, readerValue.Length - extractedPostFix.Length);
+            if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
+            {
+                Value = result;
+                PostFix = extractedPostFix;
+            }
+        }
+    }
+    
+    private string? ExtractPostFix(string readerValue)
+    {
+        if (readerValue.Length == 0)
+        {
+            return null;
+        }
+
+        int postFixStartIndex = readerValue.Length;
+        
+        for (int i = readerValue.Length - 1; i >= 0; i--)
+        {
+            if (!char.IsDigit(readerValue[i]))
+            {
+                postFixStartIndex = i + 1;
+                break;
+            }
+        }
+        
+        if (postFixStartIndex == readerValue.Length)
+        {
+            return null;
+        }
+        
+        return readerValue.Substring(postFixStartIndex);
+    }
 }

+ 34 - 0
src/PixiEditor.SVG/Units/SvgRectUnit.cs

@@ -0,0 +1,34 @@
+using Drawie.Numerics;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgRectUnit(RectD rect) : ISvgUnit
+{
+    public RectD Value { get; set; } = rect;
+    public string ToXml()
+    {
+        return $"{Value.X} {Value.Y} {Value.Width} {Value.Height}";
+    }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        string[] values = readerValue.Split(' ');
+        
+        if (values.Length == 4)
+        {
+            double x, y, width, height;
+            
+            x = TryParseOrZero(values[0]);
+            y = TryParseOrZero(values[1]);
+            width = TryParseOrZero(values[2]);
+            height = TryParseOrZero(values[3]);
+            
+            Value = new RectD(x, y, width, height);
+        }
+        
+        double TryParseOrZero(string value)
+        {
+            return double.TryParse(value, out double result) ? result : 0;
+        }
+    }
+}

+ 5 - 0
src/PixiEditor.SVG/Units/SvgStringUnit.cs

@@ -12,4 +12,9 @@ public struct SvgStringUnit : ISvgUnit
     {
         return Value;
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        Value = readerValue;
+    }
 }

+ 24 - 0
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -28,4 +28,28 @@ public struct SvgTransformUnit : ISvgUnit
         
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        if (readerValue.StartsWith("matrix(") && readerValue.EndsWith(")"))
+        {
+            string[] values = readerValue[7..^1].Split(", ");
+            if (values.Length == 6)
+            {
+                if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float scaleX) &&
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewY) &&
+                    float.TryParse(values[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewX) &&
+                    float.TryParse(values[3], NumberStyles.Any, CultureInfo.InvariantCulture, out float scaleY) &&
+                    float.TryParse(values[4], NumberStyles.Any, CultureInfo.InvariantCulture, out float translateX) &&
+                    float.TryParse(values[5], NumberStyles.Any, CultureInfo.InvariantCulture, out float translateY))
+                {
+                    MatrixValue = new Matrix3X3(scaleX, skewX, translateX, skewY, scaleY, translateY, 0, 0, 1);
+                }
+            }
+        }
+        else
+        {
+            // todo: parse other types of transformation syntax (rotate, translate, scale etc)
+        }
+    }
 }

+ 1 - 0
src/PixiEditor.SVG/Units/SvgUnit.cs

@@ -3,4 +3,5 @@
 public interface ISvgUnit
 {
     public string ToXml();
+    public void ValuesFromXml(string readerValue);
 }

+ 146 - 0
src/PixiEditor.SVG/Utils/SvgColorUtility.cs

@@ -0,0 +1,146 @@
+using Drawie.Backend.Core.ColorsImpl;
+using PixiEditor.SVG.Exceptions;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Utils;
+
+public static class SvgColorUtility
+{
+    public static SvgColorType ResolveColorType(string value)
+    {
+        if (string.IsNullOrWhiteSpace(value))
+        {
+            throw new SvgParsingException("Color value is empty");
+        }
+
+        if (value.StartsWith('#'))
+        {
+            return SvgColorType.Hex;
+        }
+
+        if (value.StartsWith("rgb"))
+        {
+            return value.Length > 3 && value[3] == 'a' ? SvgColorType.Rgba : SvgColorType.Rgb;
+        }
+
+        if (value.StartsWith("hsl"))
+        {
+            return value.Length > 3 && value[3] == 'a' ? SvgColorType.Hsla : SvgColorType.Hsl;
+        }
+
+        return SvgColorType.Named;
+    }
+
+    public static float[]? ExtractColorValues(string readerValue, SvgColorType colorType)
+    {
+        if (colorType == SvgColorType.Hex)
+        {
+            return [];
+        }
+
+        if (colorType == SvgColorType.Named)
+        {
+            return [];
+        }
+
+        string[] values = readerValue.Split(',', '(', ')').Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
+        float[] colorValues = new float[values.Length - 1];
+        for (int i = 1; i < values.Length; i++)
+        {
+            if (colorType is SvgColorType.Rgba or SvgColorType.Hsla)
+            {
+                if (i == values.Length - 1)
+                {
+                    if (float.TryParse(values[i], out float alpha))
+                    {
+                        if (alpha > 0 && alpha < 1)
+                        {
+                            colorValues[i - 1] = (byte)(alpha * 255);
+                        }
+                        else
+                        {
+                            colorValues[i - 1] = Math.Clamp(alpha, 0, 255);
+                        }
+
+                        continue;
+                    }
+
+                    throw new SvgParsingException($"Could not parse alpha value: {values[i]}");
+                }
+            }
+            
+            if(colorType is SvgColorType.Hsl or SvgColorType.Hsla)
+            {
+                if (values[i].EndsWith('%'))
+                {
+                    values[i] = values[i][..^1];
+                }
+                
+                if (float.TryParse(values[i], out float result))
+                {
+                    int clampMax = i == 1 ? 360 : 100;
+                    colorValues[i - 1] = Math.Clamp(result, 0, clampMax);
+                }
+            }
+            else if (int.TryParse(values[i], out int result))
+            {
+                int clamped = Math.Clamp(result, 0, 255);
+                colorValues[i - 1] = (byte)clamped;
+            }
+            else
+            {
+                throw new SvgParsingException($"Could not parse color value: {values[i]}");
+            }
+        }
+
+        return colorValues;
+    }
+
+    public static bool TryConvertStringToColor(string input, out Color color)
+    {
+        try
+        {
+            if(input == "none")
+            {
+                color = Colors.Transparent;
+                return true;
+            }
+            
+            SvgColorType colorType = ResolveColorType(input);
+            float[]? values = ExtractColorValues(input, colorType);
+            int requiredValues = colorType switch
+            {
+                SvgColorType.Rgb => 3,
+                SvgColorType.Rgba => 4,
+                SvgColorType.Hsl => 3,
+                SvgColorType.Hsla => 4,
+                _ => 0
+            };
+
+            if (values == null || values.Length != requiredValues)
+            {
+                color = default;
+                return false;
+            }
+
+            color = colorType switch
+            {
+                SvgColorType.Hex => Color.FromHex(input),
+                SvgColorType.Named => Color.FromHex(
+                    WellKnownColorNames.NamedToHexMap.GetValueOrDefault(input, "#000000")),
+                SvgColorType.Rgb => Color.FromRgb((byte)values![0], (byte)values[1], (byte)values[2]),
+                SvgColorType.Rgba => Color.FromRgba((byte)values![0], (byte)values[1], (byte)values[2], (byte)values[3]),
+                SvgColorType.Hsl => Color.FromHsl(values![0], values[1], values[2]),
+                SvgColorType.Hsla => Color.FromHsla(values![0], values[1], values[2], (byte)values[3]),
+                _ => default
+            };
+        }
+        catch (SvgParsingException)
+        {
+            color = default;
+            return false;
+        }
+        
+        return true;
+    }
+}

+ 156 - 0
src/PixiEditor.SVG/Utils/WellKnownColorNames.cs

@@ -0,0 +1,156 @@
+using System.Collections.Frozen;
+
+namespace PixiEditor.SVG.Utils;
+
+public static class WellKnownColorNames
+{
+    public static FrozenDictionary<string, string> NamedToHexMap { get; }
+
+    static WellKnownColorNames()
+    {
+        NamedToHexMap = new Dictionary<string, string>()
+            {
+                { "black", "#000000" },
+                { "silver", "#C0C0C0" },
+                { "gray", "#808080" },
+                { "white", "#FFFFFF" },
+                { "maroon", "#800000" },
+                { "red", "#FF0000" },
+                { "purple", "#800080" },
+                { "fuchsia", "#FF00FF" },
+                { "green", "#008000" },
+                { "lime", "#00FF00" },
+                { "olive", "#808000" },
+                { "yellow", "#FFFF00" },
+                { "navy", "#000080" },
+                { "blue", "#0000FF" },
+                { "teal", "#008080" },
+                { "aqua", "#00FFFF" },
+                { "aliceblue", "#F0F8FF" },
+                { "antiquewhite", "#FAEBD7" },
+                { "aquamarine", "#7FFFD4" },
+                { "azure", "#F0FFFF" },
+                { "beige", "#F5F5DC" },
+                { "bisque", "#FFE4C4" },
+                { "blanchedalmond", "#FFEBCD" },
+                { "blueviolet", "#8A2BE2" },
+                { "brown", "#A52A2A" },
+                { "burlywood", "#DEB887" },
+                { "cadetblue", "#5F9EA0" },
+                { "chartreuse", "#7FFF00" },
+                { "chocolate", "#D2691E" },
+                { "coral", "#FF7F50" },
+                { "cornflowerblue", "#6495ED" },
+                { "cornsilk", "#FFF8DC" },
+                { "crimson", "#DC143C" },
+                { "cyan", "#00FFFF" },
+                { "darkblue", "#00008B" },
+                { "darkcyan", "#008B8B" },
+                { "darkgoldenrod", "#B8860B" },
+                { "darkgray", "#A9A9A9" },
+                { "darkgreen", "#006400" },
+                { "darkkhaki", "#BDB76B" },
+                { "darkmagenta", "#8B008B" },
+                { "darkolivegreen", "#556B2F" },
+                { "darkorange", "#FF8C00" },
+                { "darkorchid", "#9932CC" },
+                { "darkred", "#8B0000" },
+                { "darksalmon", "#E9967A" },
+                { "darkseagreen", "#8FBC8F" },
+                { "darkslateblue", "#483D8B" },
+                { "darkslategray", "#2F4F4F" },
+                { "darkturquoise", "#00CED1" },
+                { "darkviolet", "#9400D3" },
+                { "deeppink", "#FF1493" },
+                { "deepskyblue", "#00BFFF" },
+                { "dimgray", "#696969" },
+                { "dodgerblue", "#1E90FF" },
+                { "firebrick", "#B22222" },
+                { "floralwhite", "#FFFAF0" },
+                { "forestgreen", "#228B22" },
+                { "gainsboro", "#DCDCDC" },
+                { "ghostwhite", "#F8F8FF" },
+                { "gold", "#FFD700" },
+                { "goldenrod", "#DAA520" },
+                { "greenyellow", "#ADFF2F" },
+                { "honeydew", "#F0FFF0" },
+                { "hotpink", "#FF69B4" },
+                { "indianred", "#CD5C5C" },
+                { "indigo", "#4B0082" },
+                { "ivory", "#FFFFF0" },
+                { "khaki", "#F0E68C" },
+                { "lavender", "#E6E6FA" },
+                { "lavenderblush", "#FFF0F5" },
+                { "lawngreen", "#7CFC00" },
+                { "lemonchiffon", "#FFFACD" },
+                { "lightblue", "#ADD8E6" },
+                { "lightcoral", "#F08080" },
+                { "lightcyan", "#E0FFFF" },
+                { "lightgoldenrodyellow", "#FAFAD2" },
+                { "lightgray", "#D3D3D3" },
+                { "lightgreen", "#90EE90" },
+                { "lightpink", "#FFB6C1" },
+                { "lightsalmon", "#FFA07A" },
+                { "lightseagreen", "#20B2AA" },
+                { "lightskyblue", "#87CEFA" },
+                { "lightslategray", "#778899" },
+                { "lightsteelblue", "#B0C4DE" },
+                { "lightyellow", "#FFFFE0" },
+                { "limegreen", "#32CD32" },
+                { "linen", "#FAF0E6" },
+                { "magenta", "#FF00FF" },
+                { "mediumaquamarine", "#66CDAA" },
+                { "mediumblue", "#0000CD" },
+                { "mediumorchid", "#BA55D3" },
+                { "mediumpurple", "#9370DB" },
+                { "mediumseagreen", "#3CB371" },
+                { "mediumslateblue", "#7B68EE" },
+                { "mediumspringgreen", "#00FA9A" },
+                { "mediumturquoise", "#48D1CC" },
+                { "mediumvioletred", "#C71585" },
+                { "midnightblue", "#191970" },
+                { "mintcream", "#F5FFFA" },
+                { "mistyrose", "#FFE4E1" },
+                { "moccasin", "#FFE4B5" },
+                { "navajowhite", "#FFDEAD" },
+                { "oldlace", "#FDF5E6" },
+                { "olivedrab", "#6B8E23" },
+                { "orange", "#FFA500" },
+                { "orangered", "#FF4500" },
+                { "orchid", "#DA70D6" },
+                { "palegoldenrod", "#EEE8AA" },
+                { "palegreen", "#98FB98" },
+                { "paleturquoise", "#AFEEEE" },
+                { "palevioletred", "#DB7093" },
+                { "papayawhip", "#FFEFD5" },
+                { "peachpuff", "#FFDAB9" },
+                { "peru", "#CD853F" },
+                { "pink", "#FFC0CB" },
+                { "plum", "#DDA0DD" },
+                { "powderblue", "#B0E0E6" },
+                { "rosybrown", "#BC8F8F" },
+                { "royalblue", "#4169E1" },
+                { "saddlebrown", "#8B4513" },
+                { "salmon", "#FA8072" },
+                { "sandybrown", "#F4A460" },
+                { "seagreen", "#2E8B57" },
+                { "seashell", "#FFF5EE" },
+                { "sienna", "#A0522D" },
+                { "skyblue", "#87CEEB" },
+                { "slateblue", "#6A5ACD" },
+                { "slategray", "#708090" },
+                { "snow", "#FFFAFA" },
+                { "springgreen", "#00FF7F" },
+                { "steelblue", "#4682B4" },
+                { "tan", "#D2B48C" },
+                { "thistle", "#D8BFD8" },
+                { "tomato", "#FF6347" },
+                { "turquoise", "#40E0D0" },
+                { "violet", "#EE82EE" },
+                { "wheat", "#F5DEB3" },
+                { "whitesmoke", "#F5F5F5" },
+                { "yellowgreen", "#9ACD32" }
+            }
+            .ToFrozenDictionary();
+    }
+}

+ 11 - 1
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -10,6 +10,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Skia;
@@ -124,7 +125,7 @@ internal class DocumentViewModelBuilder
         ImageEncoderUsed = encoder;
         return this;
     }
-    
+
     public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
     {
         UsesLegacyColorBlending = usesLegacyColorBlending;
@@ -370,6 +371,15 @@ internal class NodeGraphBuilder
         return node;
     }
 
+    public NodeBuilder WithNodeOfType<T>(out int id) where T : IReadOnlyNode
+    {
+        NodeBuilder builder = this.WithNodeOfType(typeof(T))
+            .WithId(AllNodes.Count);
+
+        id = AllNodes.Count;
+        return builder;
+    }
+
     internal class NodeBuilder
     {
         public int Id { get; set; }

+ 3 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -19,6 +19,7 @@ using PixiEditor.Models.ExtensionServices;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.Models.IO.PaletteParsers;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.Localization;
@@ -110,6 +111,8 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IoFileType, SvgFileType>()
             // Serialization Factories
             .AddAssemblyTypes<SerializationFactory>()
+            // Custom document builders
+            .AddSingleton<IDocumentBuilder, SvgDocumentBuilder>()
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

+ 5 - 0
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -92,4 +92,9 @@ internal class SupportedFilesHelper
         var any = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Any).GetFormattedTypes(true);
         return any.ToList();
     }
+
+    public static bool IsRasterFormat(string fileExtension)
+    {
+        return FileTypes.Any(i => i.Extensions.Contains(fileExtension) && i.SetKind == FileTypeDialogDataSet.SetKind.Image);
+    }
 }

+ 18 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -73,9 +73,24 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             memberCorners.Add(member.Id, targetCorners);
         }
 
-        ShapeCorners masterCorners = memberCorners.Count == 1
-            ? memberCorners.FirstOrDefault().Value
-            : new ShapeCorners(memberCorners.Values.Select(static c => c.AABBBounds).Aggregate((a, b) => a.Union(b)));
+        ShapeCorners masterCorners;
+        if (memberCorners.Count == 1)
+        {
+            masterCorners = memberCorners.FirstOrDefault().Value;
+        }
+        else
+        {
+            var aabbBounds = memberCorners.Values.Select(static c => c.AABBBounds);
+            var bounds = aabbBounds as RectD[] ?? aabbBounds.ToArray();
+            if (bounds.Length != 0)
+            {
+                masterCorners = new ShapeCorners(bounds.Aggregate((a, b) => a.Union(b)));
+            }
+            else
+            {
+                return ExecutionState.Error;
+            }
+        }
 
         if (masterCorners.AABBBounds.Width == 0 || masterCorners.AABBBounds.Height == 0)
         {

+ 3 - 1
src/PixiEditor/Models/Files/SvgFileType.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using Avalonia.Media;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
@@ -15,6 +16,7 @@ internal class SvgFileType : IoFileType
     public override string[] Extensions { get; } = new[] { ".svg" };
     public override string DisplayName { get; } = "Scalable Vector Graphics";
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Vector;
+    public override SolidColorBrush EditorColor { get; } = new SolidColorBrush(Color.FromRgb(0, 128, 0));
 
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
     {

+ 9 - 0
src/PixiEditor/Models/IO/CustomDocumentFormats/IDocumentBuilder.cs

@@ -0,0 +1,9 @@
+using PixiEditor.Helpers;
+
+namespace PixiEditor.Models.IO.CustomDocumentFormats;
+
+internal interface IDocumentBuilder
+{
+    public void Build(DocumentViewModelBuilder builder, string path);
+    public IReadOnlyCollection<string> Extensions { get; }
+}

+ 220 - 0
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -0,0 +1,220 @@
+using System.Diagnostics.CodeAnalysis;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Helpers;
+using PixiEditor.Parser.Graph;
+using PixiEditor.SVG;
+using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Enums;
+using PixiEditor.ViewModels.Tools.Tools;
+
+namespace PixiEditor.Models.IO.CustomDocumentFormats;
+
+internal class SvgDocumentBuilder : IDocumentBuilder
+{
+    public IReadOnlyCollection<string> Extensions { get; } = [".svg"];
+
+    public void Build(DocumentViewModelBuilder builder, string path)
+    {
+        string xml = File.ReadAllText(path);
+        SvgDocument document = SvgDocument.Parse(xml);
+
+        StyleContext styleContext = new(document);
+
+        builder.WithSize((int)document.ViewBox.Unit.Value.Value.Width, (int)document.ViewBox.Unit.Value.Value.Height)
+            .WithGraph(graph =>
+            {
+                int? lastId = null;
+                foreach (SvgElement element in document.Children)
+                {
+                    StyleContext style = styleContext.WithElement(element);
+                    if (element is SvgPrimitive primitive)
+                    {
+                        lastId = AddPrimitive(element, style, graph, lastId);
+                    }
+                    else if (element is SvgGroup group)
+                    {
+                        lastId = AddGroup(group, graph, style, lastId);
+                    }
+                }
+
+                graph.WithOutputNode(lastId, "Output");
+            });
+    }
+
+    [return: NotNull]
+    private int? AddPrimitive(SvgElement element, StyleContext styleContext,
+        NodeGraphBuilder graph,
+        int? lastId, string connectionName = "Background")
+    {
+        LocalizedString name = "";
+        ShapeVectorData shapeData = null;
+        if (element is SvgEllipse or SvgCircle)
+        {
+            shapeData = AddEllipse(element);
+            name = VectorEllipseToolViewModel.NewLayerKey;
+        }
+        else if (element is SvgLine line)
+        {
+            shapeData = AddLine(line);
+            name = VectorLineToolViewModel.NewLayerKey;
+        }
+        else if (element is SvgPath pathElement)
+        {
+            shapeData = AddPath(pathElement);
+            name = VectorPathToolViewModel.NewLayerKey;
+        }
+        else if (element is SvgRectangle rect)
+        {
+            shapeData = AddRect(rect);
+            name = VectorRectangleToolViewModel.NewLayerKey;
+        }
+
+        AddCommonShapeData(shapeData, styleContext);
+
+        NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<VectorLayerNode>(out int id)
+            .WithName(name)
+            .WithAdditionalData(new Dictionary<string, object>() { { "ShapeData", shapeData } });
+
+        if (lastId != null)
+        {
+            nBuilder.WithConnections([
+                new PropertyConnection()
+                {
+                    InputPropertyName = connectionName, OutputPropertyName = "Output", OutputNodeId = lastId.Value
+                }
+            ]);
+        }
+
+        lastId = id;
+        return lastId;
+    }
+
+    private int? AddGroup(SvgGroup group, NodeGraphBuilder graph, StyleContext style, int? lastId, string connectionName = "Background")
+    {
+        int? childId = null;
+        var connectTo = "Background";
+        
+        foreach (var child in group.Children)
+        {
+            StyleContext childStyle = style.WithElement(child);
+            
+            if (child is SvgPrimitive primitive)
+            {
+                childId = AddPrimitive(child, childStyle, graph, childId, connectTo);
+            }
+            else if (child is SvgGroup childGroup)
+            {
+                childId = AddGroup(childGroup, graph, childStyle, childId, connectTo);
+            }
+        }
+
+        NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<FolderNode>(out int id)
+            .WithName(group.Id.Unit != null ? group.Id.Unit.Value.Value : new LocalizedString("NEW_FOLDER"));
+
+        if (lastId != null)
+        {
+            nBuilder.WithConnections([
+                new PropertyConnection()
+                {
+                    InputPropertyName = connectionName, OutputPropertyName = "Output", OutputNodeId = lastId.Value
+                },
+                new PropertyConnection()
+                {
+                    InputPropertyName = "Content", OutputPropertyName = "Output", OutputNodeId = childId.Value
+                }
+            ]);
+        }
+
+        lastId = id;
+
+        return lastId;
+    }
+
+    private EllipseVectorData AddEllipse(SvgElement element)
+    {
+        if (element is SvgCircle circle)
+        {
+            return new EllipseVectorData(
+                new VecD(circle.Cx.Unit.Value.Value, circle.Cy.Unit.Value.Value),
+                new VecD(circle.R.Unit.Value.Value, circle.R.Unit.Value.Value));
+        }
+
+        if (element is SvgEllipse ellipse)
+        {
+            return new EllipseVectorData(
+                new VecD(ellipse.Cx.Unit.Value.Value, ellipse.Cy.Unit.Value.Value),
+                new VecD(ellipse.Rx.Unit.Value.Value, ellipse.Ry.Unit.Value.Value));
+        }
+
+        return null;
+    }
+
+    private LineVectorData AddLine(SvgLine element)
+    {
+        return new LineVectorData(
+            new VecD(element.X1.Unit.Value.Value, element.Y1.Unit.Value.Value),
+            new VecD(element.X2.Unit.Value.Value, element.Y2.Unit.Value.Value));
+    }
+
+    private PathVectorData AddPath(SvgPath element)
+    {
+        VectorPath path = VectorPath.FromSvgPath(element.PathData.Unit.Value.Value);
+
+        if (element.FillRule.Unit != null)
+        {
+            path.FillType = element.FillRule.Unit.Value.Value switch
+            {
+                SvgFillRule.EvenOdd => PathFillType.EvenOdd,
+                SvgFillRule.NonZero => PathFillType.Winding,
+                _ => PathFillType.Winding
+            };
+        }
+
+        return new PathVectorData(path);
+    }
+
+    private RectangleVectorData AddRect(SvgRectangle element)
+    {
+        return new RectangleVectorData(element.X.Unit.Value.Value, element.Y.Unit.Value.Value,
+            element.Width.Unit.Value.Value, element.Height.Unit.Value.Value);
+    }
+
+    private void AddCommonShapeData(ShapeVectorData? shapeData, StyleContext styleContext)
+    {
+        if (shapeData == null)
+        {
+            return;
+        }
+
+        bool hasFill = styleContext.Fill.Unit is { Color.A: > 0 };
+        bool hasStroke = styleContext.Stroke.Unit is { Color.A: > 0 };
+        bool hasTransform = styleContext.Transform.Unit is { MatrixValue.IsIdentity: false };
+
+        shapeData.Fill = hasFill;
+        if (hasFill)
+        {
+            var target = styleContext.Fill.Unit;
+            shapeData.FillColor = target.Value.Color;
+        }
+
+        if (hasStroke)
+        {
+            var targetColor = styleContext.Stroke.Unit;
+            var targetWidth = styleContext.StrokeWidth.Unit;
+            
+            shapeData.StrokeColor = targetColor.Value.Color;
+            shapeData.StrokeWidth = (float)targetWidth.Value.Value;
+        }
+
+        if (hasTransform)
+        {
+            var target = styleContext.Transform.Unit;
+            shapeData.TransformationMatrix = target.Value.MatrixValue;
+        }
+    }
+}

+ 0 - 16
src/PixiEditor/Models/IO/Importer.cs

@@ -151,22 +151,6 @@ internal class Importer : ObservableObject
         }
     }
 
-    public static Texture GetPreviewTexture(string path)
-    {
-        if (!IsSupportedFile(path))
-        {
-            throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED",
-                Path.GetExtension(path)));
-        }
-
-        if (Path.GetExtension(path) != ".pixi")
-            return Texture.Load(path);
-
-        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
-
-        return Texture.Load(PixiParser.ReadPreview(fileStream));
-    }
-
     public static Surface GetPreviewSurface(string path)
     {
         if (!IsSupportedFile(path))

+ 5 - 0
src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs

@@ -80,4 +80,9 @@ public class ByteBuilder
     {
         _data.AddRange(BitConverter.GetBytes(value));
     }
+
+    public void AddBool(bool value)
+    {
+        _data.Add(value ? (byte)1 : (byte)0);
+    }
 }

+ 9 - 0
src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs

@@ -104,4 +104,13 @@ public class ByteExtractor
         
         return value;
     }
+
+    public bool GetBool()
+    {
+        bool value = BitConverter.ToBoolean(_data, Position);
+        
+        Position += sizeof(bool);
+        
+        return value;
+    }
 }

+ 2 - 0
src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs

@@ -16,6 +16,7 @@ public class EllipseSerializationFactory : VectorShapeSerializationFactory<Ellip
     }
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor,
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out EllipseVectorData original)
@@ -26,6 +27,7 @@ public class EllipseSerializationFactory : VectorShapeSerializationFactory<Ellip
         original = new EllipseVectorData(center, radius)
         {
             StrokeColor = strokeColor,
+            Fill = fill,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
             TransformationMatrix = matrix

+ 1 - 0
src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs

@@ -16,6 +16,7 @@ internal class LineSerializationFactory : VectorShapeSerializationFactory<LineVe
     }
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor,
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out LineVectorData original)

+ 3 - 1
src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs

@@ -14,6 +14,7 @@ internal class PointsDataSerializationFactory : VectorShapeSerializationFactory<
     }
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor,
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out PointsVectorData original)
@@ -24,7 +25,8 @@ internal class PointsDataSerializationFactory : VectorShapeSerializationFactory<
             StrokeColor = strokeColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
 
         return true;

+ 3 - 1
src/PixiEditor/Models/Serialization/Factories/RectangleSerializationFactory.cs

@@ -17,6 +17,7 @@ internal class RectangleSerializationFactory : VectorShapeSerializationFactory<R
     }
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor,
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out RectangleVectorData original)
@@ -29,7 +30,8 @@ internal class RectangleSerializationFactory : VectorShapeSerializationFactory<R
             StrokeColor = strokeColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
 
         return true;

+ 3 - 1
src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs

@@ -30,6 +30,7 @@ internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<
     }
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor,
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out PathVectorData original)
@@ -50,7 +51,8 @@ internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<
             StrokeColor = strokeColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
 
         return true;

+ 18 - 1
src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs

@@ -11,6 +11,7 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         ByteBuilder builder = new ByteBuilder();
         builder.AddMatrix3X3(original.TransformationMatrix);
         builder.AddColor(original.StrokeColor);
+        builder.AddBool(original.Fill);
         builder.AddColor(original.FillColor);
         builder.AddFloat(original.StrokeWidth);
         
@@ -34,6 +35,7 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         
         Matrix3X3 matrix = extractor.GetMatrix3X3();
         Color strokeColor = extractor.GetColor();
+        bool fill = TryGetBool(extractor, serializerData);
         Color fillColor = extractor.GetColor();
         float strokeWidth;
         // Previous versions of the serializer saved stroke as int, and serializer data didn't exist
@@ -46,10 +48,25 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
             strokeWidth = extractor.GetFloat();
         }
 
-        return DeserializeVectorData(extractor, matrix, strokeColor, fillColor, strokeWidth, serializerData, out original);
+        return DeserializeVectorData(extractor, matrix, strokeColor, fill, fillColor, strokeWidth, serializerData, out original);
     }
     
     protected abstract bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
         Color fillColor, float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out T original);
+
+    private bool TryGetBool(ByteExtractor extractor, (string serializerName, string serializerVersion) serializerData)
+    {
+        // Previous versions didn't have fill bool
+        if (serializerData.serializerName == "PixiEditor")
+        {
+            if(Version.TryParse(serializerData.serializerVersion, out Version version) && version is { Major: 2, Minor: 0, Build: 0, Revision: < 35 })
+            {
+                return true;
+            }
+        }
+
+        return extractor.GetBool();
+    }
 }

+ 26 - 27
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -18,6 +18,7 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Handlers;
@@ -148,17 +149,22 @@ internal partial class DocumentViewModel
             elementToAdd = AddVectorPath(shapeData);
         }
 
-        if (vectorNode.ShapeData != null)
-        {
-            IReadOnlyShapeVectorData data = vectorNode.ShapeData;
-
-            if (data != null && elementToAdd is SvgPrimitive primitive)
-            {
-                Matrix3X3 transform = data.TransformationMatrix;
+        IReadOnlyShapeVectorData data = vectorNode.ShapeData;
 
-                transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
-                primitive.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
-            }
+        if (data != null && elementToAdd is SvgPrimitive primitive)
+        {
+            Matrix3X3 transform = data.TransformationMatrix;
+
+            transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
+            primitive.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
+
+            primitive.Fill.Unit = SvgColorUnit.FromRgba(data.FillColor.R, data.FillColor.G,
+                data.FillColor.B, data.Fill ? data.FillColor.A : 0);
+                
+            primitive.Stroke.Unit = SvgColorUnit.FromRgba(data.StrokeColor.R, data.StrokeColor.G,
+                data.StrokeColor.B, data.StrokeColor.A);
+                
+            primitive.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);
         }
 
         if (elementToAdd != null)
@@ -178,7 +184,7 @@ internal partial class DocumentViewModel
         line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
             lineData.StrokeColor.B, lineData.StrokeColor.A);
         line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth);
-        
+
         return line;
     }
 
@@ -189,11 +195,6 @@ internal partial class DocumentViewModel
         ellipse.Cy.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.Y);
         ellipse.Rx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.X);
         ellipse.Ry.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.Y);
-        ellipse.Fill.Unit = SvgColorUnit.FromRgba(ellipseData.FillColor.R, ellipseData.FillColor.G,
-            ellipseData.FillColor.B, ellipseData.FillColor.A);
-        ellipse.Stroke.Unit = SvgColorUnit.FromRgba(ellipseData.StrokeColor.R, ellipseData.StrokeColor.G,
-            ellipseData.StrokeColor.B, ellipseData.StrokeColor.A);
-        ellipse.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(ellipseData.StrokeWidth);
 
         return ellipse;
     }
@@ -212,11 +213,6 @@ internal partial class DocumentViewModel
 
         rect.Width.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.X);
         rect.Height.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.Y);
-        rect.Fill.Unit = SvgColorUnit.FromRgba(rectangleData.FillColor.R, rectangleData.FillColor.G,
-            rectangleData.FillColor.B, rectangleData.FillColor.A);
-        rect.Stroke.Unit = SvgColorUnit.FromRgba(rectangleData.StrokeColor.R, rectangleData.StrokeColor.G,
-            rectangleData.StrokeColor.B, rectangleData.StrokeColor.A);
-        rect.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(rectangleData.StrokeWidth);
 
         return rect;
     }
@@ -228,12 +224,15 @@ internal partial class DocumentViewModel
         {
             string pathData = data.Path.ToSvgPathData();
             path.PathData.Unit = new SvgStringUnit(pathData);
-
-            path.Fill.Unit =
-                SvgColorUnit.FromRgba(data.FillColor.R, data.FillColor.G, data.FillColor.B, data.FillColor.A);
-            path.Stroke.Unit = SvgColorUnit.FromRgba(data.StrokeColor.R, data.StrokeColor.G, data.StrokeColor.B,
-                data.StrokeColor.A);
-            path.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);
+            SvgFillRule fillRule = data.Path.FillType switch
+            {
+                PathFillType.EvenOdd => SvgFillRule.EvenOdd,
+                PathFillType.Winding => SvgFillRule.NonZero,
+                PathFillType.InverseWinding => SvgFillRule.NonZero,
+                PathFillType.InverseEvenOdd => SvgFillRule.EvenOdd,
+            };
+            
+            path.FillRule.Unit = new SvgEnumUnit<SvgFillRule>(fillRule);
         }
 
         return path;

+ 32 - 13
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -1,21 +1,10 @@
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using Avalonia;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Platform.Storage;
 using Avalonia.Threading;
-using ChunkyImageLib;
-using Newtonsoft.Json.Linq;
-using PixiEditor.ChangeableDocument.Changeables.Graph;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core;
-using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Exceptions;
 using PixiEditor.Extensions.Common.Localization;
@@ -30,9 +19,10 @@ using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserData;
 using Drawie.Numerics;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.OperatingSystem;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Graph;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views;
@@ -57,6 +47,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     }
 
     public RecentlyOpenedCollection RecentlyOpened { get; init; }
+    public IReadOnlyList<IDocumentBuilder> DocumentBuilders => documentBuilders;
+    
+    private List<IDocumentBuilder> documentBuilders;
 
     public FileViewModel(ViewModelMain owner)
         : base(owner)
@@ -70,6 +63,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
 
         PixiEditorSettings.File.MaxOpenedRecently.ValueChanged += (_, value) => UpdateMaxRecentlyOpened(value);
+        documentBuilders = owner.Services.GetServices<IDocumentBuilder>().ToList();
     }
 
     public void AddRecentlyOpened(string path)
@@ -209,6 +203,10 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             {
                 OpenDotPixi(path, associatePath);
             }
+            else if (IsCustomFormat(path))
+            {
+                OpenCustomFormat(path);
+            }
             else
             {
                 OpenRegularImage(path, associatePath);
@@ -223,6 +221,27 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
         }
     }
+    
+    private bool IsCustomFormat(string path)
+    {
+        string extension = Path.GetExtension(path);
+        return documentBuilders.Any(x => x.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
+    }
+    
+    private void OpenCustomFormat(string path)
+    {
+        IDocumentBuilder builder = documentBuilders.First(x => x.Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase));
+
+        if(!File.Exists(path))
+        {
+            NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
+            return;
+        }
+        
+        DocumentViewModel document = DocumentViewModel.Build(docBuilder => builder.Build(docBuilder, path));
+        AddDocumentViewModelToTheSystem(document);
+        AddRecentlyOpened(document.FullFilePath);
+    }
 
     /// <summary>
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system

+ 2 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -15,6 +15,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.C)]
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 {
+    public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME"; 
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
 
@@ -33,7 +34,7 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
 
-    public string? DefaultNewLayerName { get; } = new LocalizedString("NEW_ELLIPSE_LAYER_NAME");
+    public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
     public override void UseTool(VecD pos)
     {

+ 2 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -19,6 +19,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.L)]
 internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 {
+    public const string NewLayerKey = "NEW_LINE_LAYER_NAME";
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
     public override bool IsErasable => false;
@@ -34,7 +35,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     public override string DefaultIcon => PixiPerfectIcons.Line;
     public override Type[]? SupportedLayerTypes { get; } = [];
-    public string? DefaultNewLayerName { get; } = new LocalizedString("NEW_LINE_LAYER_NAME");
+    public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
     [Settings.Inherited] 
     public double ToolSize => GetValue<double>();

+ 2 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -15,12 +15,13 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.P)]
 internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
 {
+    public const string NewLayerKey = "DEFAULT_PATH_LAYER_NAME";
     public override string ToolNameLocalizationKey => "PATH_TOOL";
     public override Type[]? SupportedLayerTypes { get; } = [typeof(IVectorLayerHandler)];
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public override LocalizedString Tooltip => new LocalizedString("PATH_TOOL_TOOLTIP", Shortcut);
 
-    public string? DefaultNewLayerName => new LocalizedString("DEFAULT_PATH_LAYER_NAME");
+    public string? DefaultNewLayerName => new LocalizedString(NewLayerKey);
 
     public override string DefaultIcon => PixiPerfectIcons.VectorPen;
     public override bool StopsLinkedToolOnUse => false;

+ 3 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -15,6 +15,8 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.R)]
 internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHandler
 {
+    public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";
+    
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override bool IsErasable => false;
@@ -30,7 +32,7 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
     public override string DefaultIcon => PixiPerfectIcons.Square;
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
-    public string? DefaultNewLayerName { get; } = new LocalizedString("NEW_RECTANGLE_LAYER_NAME");
+    public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {

+ 42 - 63
src/PixiEditor/Views/Visuals/PixiFilePreviewImage.cs

@@ -1,7 +1,5 @@
-using System.Drawing;
-using Avalonia;
+using Avalonia;
 using Avalonia.Threading;
-using FFMpegCore.Enums;
 using Drawie.Backend.Core;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Helpers;
@@ -9,11 +7,10 @@ using PixiEditor.Models;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using PixiEditor.Parser;
-using Image = Avalonia.Controls.Image;
 
 namespace PixiEditor.Views.Visuals;
 
-internal class PixiFilePreviewImage : SurfaceControl
+internal class PixiFilePreviewImage : TextureControl
 {
     public static readonly StyledProperty<string> FilePathProperty =
         AvaloniaProperty.Register<PixiFilePreviewImage, string>(nameof(FilePath));
@@ -54,86 +51,63 @@ internal class PixiFilePreviewImage : SurfaceControl
         Task.Run(() => LoadImage(path));
     }
 
-    private void LoadImage(string path)
+    private async Task LoadImage(string path)
     {
-        var surface = LoadPreviewSurface(path);
+        string fileExtension = Path.GetExtension(path);
 
-        Dispatcher.UIThread.Post(() => SetImage(surface));
-    }
-
-    private void SetImage(Surface? surface)
-    {
-        Surface = surface!;
+        byte[] imageBytes;
 
-        if (surface != null)
+        bool isPixi = fileExtension == ".pixi";
+        if (isPixi)
         {
-            ImageSize = surface.Size;
+            await using FileStream fileStream = File.OpenRead(path);
+            imageBytes = await PixiParser.ReadPreviewAsync(fileStream);
         }
-    }
-
-    private static void OnFilePathChanged(PixiFilePreviewImage previewImage, AvaloniaPropertyChangedEventArgs args)
-    {
-        if (args.NewValue == null)
+        else if (SupportedFilesHelper.IsExtensionSupported(fileExtension) &&
+                 SupportedFilesHelper.IsRasterFormat(fileExtension))
+        {
+            imageBytes = await File.ReadAllBytesAsync(path);
+        }
+        else
         {
-            previewImage.Surface = null;
             return;
         }
 
-        previewImage.RunLoadImage();
+        Dispatcher.UIThread.Post(() =>
+        {
+            var surface = LoadTexture(imageBytes);
+            SetImage(surface);
+        });
     }
 
-    private Surface? LoadPreviewSurface(string filePath)
+    private void SetImage(Texture? texture)
     {
-        if (!File.Exists(filePath))
-        {
-            return null;
-        }
+        Texture = texture!;
 
-        var fileExtension = Path.GetExtension(filePath);
-
-        if (fileExtension == ".pixi")
+        if (texture != null)
         {
-            return LoadPixiPreview(filePath);
+            ImageSize = texture.Size;
         }
-
-        if (SupportedFilesHelper.IsExtensionSupported(fileExtension))
-        {
-            return LoadNonPixiPreview(filePath);
-        }
-
-        return null;
-
     }
 
-    private Surface LoadPixiPreview(string filePath)
+    private static void OnFilePathChanged(PixiFilePreviewImage previewImage, AvaloniaPropertyChangedEventArgs args)
     {
-        try
-        {
-            var loaded = Importer.GetPreviewSurface(filePath);
-
-            if (loaded.Size is { X: <= Constants.MaxPreviewWidth, Y: <= Constants.MaxPreviewHeight })
-            {
-                return loaded;
-            }
-
-            var downscaled = DownscaleSurface(loaded);
-            loaded.Dispose();
-            return downscaled;
-        }
-        catch
+        if (args.NewValue == null)
         {
-            SetCorrupt();
-            return null;
+            previewImage.Texture = null;
+            return;
         }
+
+        previewImage.RunLoadImage();
     }
 
-    private Surface LoadNonPixiPreview(string filePath)
+    private Texture LoadTexture(byte[] textureBytes)
     {
-        Surface loaded = null;
+        Texture loaded = null;
 
         try
         {
-            loaded = Surface.Load(filePath);
+            loaded = Texture.Load(textureBytes);
         }
         catch (RecoverableException)
         {
@@ -151,26 +125,31 @@ internal class PixiFilePreviewImage : SurfaceControl
         var downscaled = DownscaleSurface(loaded);
         loaded.Dispose();
         return downscaled;
-
     }
 
-    private static Surface DownscaleSurface(Surface surface)
+    private static Texture DownscaleSurface(Texture surface)
     {
         double factor = Math.Min(
             Constants.MaxPreviewWidth / (double)surface.Size.X,
             Constants.MaxPreviewHeight / (double)surface.Size.Y);
 
         var newSize = new VecI((int)(surface.Size.X * factor), (int)(surface.Size.Y * factor));
-        
+
         var scaledBitmap = surface.Resize(newSize, ResizeMethod.HighQuality);
 
         surface.Dispose();
         return scaledBitmap;
     }
-    
+
     // TODO: This does not actually set the dot to gray
     void SetCorrupt()
     {
         Dispatcher.UIThread.Post(() => Corrupt = true);
     }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        Texture?.Dispose();
+    }
 }

+ 0 - 2
src/PixiEditor/Views/Windows/HelloTherePopup.axaml

@@ -14,8 +14,6 @@
                          xmlns:newsFeed1="clr-namespace:PixiEditor.Views.NewsFeed"
                          xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
                          xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
-                         xmlns:skiaSharp="clr-namespace:SkiaSharp;assembly=SkiaSharp"
-                         xmlns:ui1="clr-namespace:PixiEditor.Helpers.UI"
                          xmlns:windows="clr-namespace:PixiEditor.Views.Windows"
                          mc:Ignorable="d"
                          Title="Hello there!" Height="680" Width="982" MinHeight="500" MinWidth="600"

+ 253 - 0
tests/PixiEditor.Tests/SvgTests.cs

@@ -0,0 +1,253 @@
+using System.Reflection;
+using System.Xml;
+using Drawie.Backend.Core.ColorsImpl;
+using PixiEditor.SVG;
+using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Utils;
+
+namespace PixiEditor.Tests;
+
+public class SvgTests
+{
+    [Fact]
+    public void TestThatEmptySvgIsParsedCorrectly()
+    {
+        string svg = "<svg></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        Assert.Empty(document.Children);
+    }
+
+    [Theory]
+    [InlineData("<svg viewBox=\"0 0 100 100\"></svg>", 0, 0, 100, 100)]
+    [InlineData("<svg width=\"100\" height=\"100\"></svg>", 0, 0, 100, 100)]
+    [InlineData("<svg x=\"0\" y=\"0\" width=\"100\" height=\"100\"></svg>", 0, 0, 100, 100)]
+    [InlineData("<svg viewBox=\"0 0 100 100\" width=\"50\" height=\"50\"></svg>", 0, 0, 50, 50)]
+    [InlineData("<svg viewBox=\"-50 -50 128 128\" width=\"100\" x=\"1\"></svg>", 1, -50, 100, 128)]
+    public void TestThatSvgBoundsAreParsedCorrectly(string svg, double x, double y, double width, double height)
+    {
+        SvgDocument document = SvgDocument.Parse(svg);
+        Assert.Equal(x, document.ViewBox.Unit.Value.Value.X);
+        Assert.Equal(y, document.ViewBox.Unit.Value.Value.Y);
+        Assert.Equal(width, document.ViewBox.Unit.Value.Value.Width);
+        Assert.Equal(height, document.ViewBox.Unit.Value.Value.Height);
+    }
+
+    [Theory]
+    [InlineData("<svg><rect/></svg>", 1)]
+    [InlineData("<svg><rect/><circle/></svg>", 2)]
+    [InlineData("<svg><rect/><circle/><ellipse/></svg>", 3)]
+    public void TestThatSvgElementsCountIsParsedCorrectly(string svg, int elements)
+    {
+        SvgDocument document = SvgDocument.Parse(svg);
+        Assert.Equal(elements, document.Children.Count);
+    }
+
+    [Theory]
+    [InlineData("<svg><rect/></svg>", "rect")]
+    [InlineData("<svg><circle/></svg>", "circle")]
+    [InlineData("<svg><ellipse/></svg>", "ellipse")]
+    [InlineData("<svg><someArbitraryElement/></svg>", null)]
+    public void TestThatSvgElementsAreParsedCorrectly(string svg, string? element)
+    {
+        SvgDocument document = SvgDocument.Parse(svg);
+        if (element == null)
+        {
+            Assert.Empty(document.Children);
+            return;
+        }
+
+        Assert.Equal(element, document.Children[0].TagName);
+    }
+
+    [Theory]
+    [InlineData("<svg><rect/></svg>", typeof(SvgRectangle))]
+    [InlineData("<svg><circle/></svg>", typeof(SvgCircle))]
+    [InlineData("<svg><ellipse/></svg>", typeof(SvgEllipse))]
+    [InlineData("<svg><g/></svg>", typeof(SvgGroup))]
+    [InlineData("<svg><line/></svg>", typeof(SvgLine))]
+    [InlineData("<svg><path/></svg>", typeof(SvgPath))]
+    [InlineData("<svg><mask/></svg>", typeof(SvgMask))]
+    [InlineData("<svg><image/></svg>", typeof(SvgImage))]
+    [InlineData("<svg><someArbitraryElement/></svg>", null)]
+    public void TestThatSvgElementsAreParsedToCorrectType(string svg, Type? elementType)
+    {
+        SvgDocument document = SvgDocument.Parse(svg);
+        if (elementType == null)
+        {
+            Assert.Empty(document.Children);
+            return;
+        }
+
+        Assert.IsType(elementType, document.Children[0]);
+    }
+
+    [Fact]
+    public void TestThatRectIsParsedCorrectly()
+    {
+        string svg = "<svg><rect x=\"10\" y=\"20\" width=\"30\" height=\"40\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgRectangle rect = (SvgRectangle)document.Children[0];
+
+        Assert.NotNull(rect);
+        Assert.NotNull(rect.X.Unit);
+        Assert.NotNull(rect.Y.Unit);
+        Assert.NotNull(rect.Width.Unit);
+        Assert.NotNull(rect.Height.Unit);
+
+        Assert.Equal(10, rect.X.Unit.Value.Value);
+        Assert.Equal(20, rect.Y.Unit.Value.Value);
+        Assert.Equal(30, rect.Width.Unit.Value.Value);
+        Assert.Equal(40, rect.Height.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatCircleIsParsedCorrectly()
+    {
+        string svg = "<svg><circle cx=\"10\" cy=\"20\" r=\"30\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgCircle circle = (SvgCircle)document.Children[0];
+
+        Assert.NotNull(circle);
+        Assert.NotNull(circle.Cx.Unit);
+        Assert.NotNull(circle.Cy.Unit);
+        Assert.NotNull(circle.R.Unit);
+
+        Assert.Equal(10, circle.Cx.Unit.Value.Value);
+        Assert.Equal(20, circle.Cy.Unit.Value.Value);
+        Assert.Equal(30, circle.R.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatEllipseIsParsedCorrectly()
+    {
+        string svg = "<svg><ellipse cx=\"10\" cy=\"20\" rx=\"30\" ry=\"40\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgEllipse ellipse = (SvgEllipse)document.Children[0];
+
+        Assert.NotNull(ellipse);
+        Assert.NotNull(ellipse.Cx.Unit);
+        Assert.NotNull(ellipse.Cy.Unit);
+        Assert.NotNull(ellipse.Rx.Unit);
+        Assert.NotNull(ellipse.Ry.Unit);
+
+        Assert.Equal(10, ellipse.Cx.Unit.Value.Value);
+        Assert.Equal(20, ellipse.Cy.Unit.Value.Value);
+        Assert.Equal(30, ellipse.Rx.Unit.Value.Value);
+        Assert.Equal(40, ellipse.Ry.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatGroupIsParsedCorrectly()
+    {
+        string svg = "<svg><g><rect/></g></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgGroup group = (SvgGroup)document.Children[0];
+
+        Assert.NotNull(group);
+        Assert.Single(group.Children);
+        Assert.IsType<SvgRectangle>(group.Children[0]);
+    }
+
+    [Fact]
+    public void TestThatLineIsParsedCorrectly()
+    {
+        string svg = "<svg><line x1=\"10\" y1=\"20\" x2=\"30\" y2=\"40\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgLine line = (SvgLine)document.Children[0];
+
+        Assert.NotNull(line);
+        Assert.NotNull(line.X1.Unit);
+        Assert.NotNull(line.Y1.Unit);
+        Assert.NotNull(line.X2.Unit);
+        Assert.NotNull(line.Y2.Unit);
+
+        Assert.Equal(10, line.X1.Unit.Value.Value);
+        Assert.Equal(20, line.Y1.Unit.Value.Value);
+        Assert.Equal(30, line.X2.Unit.Value.Value);
+        Assert.Equal(40, line.Y2.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatPathIsParsedCorrectly()
+    {
+        string svg = "<svg><path d=\"M10 20 L30 40\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgPath path = (SvgPath)document.Children[0];
+
+        Assert.NotNull(path);
+        Assert.NotNull(path.PathData.Unit);
+
+        Assert.Equal("M10 20 L30 40", path.PathData.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatMaskIsParsedCorrectly()
+    {
+        string svg = "<svg><mask><rect/></mask></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgMask mask = (SvgMask)document.Children[0];
+
+        Assert.NotNull(mask);
+        Assert.Single(mask.Children);
+        Assert.IsType<SvgRectangle>(mask.Children[0]);
+    }
+
+    [Fact]
+    public void TestThatImageIsParsedCorrectly()
+    {
+        string svg = "<svg><image x=\"10\" y=\"20\" width=\"30\" height=\"40\"/></svg>";
+        SvgDocument document = SvgDocument.Parse(svg);
+        SvgImage image = (SvgImage)document.Children[0];
+
+        Assert.NotNull(image);
+        Assert.NotNull(image.X.Unit);
+        Assert.NotNull(image.Y.Unit);
+        Assert.NotNull(image.Width.Unit);
+        Assert.NotNull(image.Height.Unit);
+
+        Assert.Equal(10, image.X.Unit.Value.Value);
+        Assert.Equal(20, image.Y.Unit.Value.Value);
+        Assert.Equal(30, image.Width.Unit.Value.Value);
+        Assert.Equal(40, image.Height.Unit.Value.Value);
+    }
+
+    [Fact]
+    public void TestThatAllAssemblySvgElementsParseData()
+    {
+        Assembly assembly = Assembly.GetAssembly(typeof(SvgElement))!;
+        Type[] types = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(SvgElement)) && !t.IsAbstract).ToArray();
+
+        foreach (Type type in types)
+        {
+            SvgElement element = (SvgElement)Activator.CreateInstance(type)!;
+            using MemoryStream stream = new();
+            using StreamWriter writer = new(stream);
+            writer.Write($"<svg><{element.TagName}/></svg>");
+            using XmlReader reader = XmlReader.Create(stream);
+            
+            element.ParseData(reader);
+        }
+    }
+
+    [Theory]
+    [InlineData("red")]
+    [InlineData("#ff0000")]
+    [InlineData("rgb(255, 0, 0)")]
+    [InlineData("hsl(0, 100%, 50%)")]
+    [InlineData("hsla(0, 100%, 50%, 255)")]
+    [InlineData("rgba(255, 0, 0, 255)")]
+    public void TestThatDifferentColorFormatsGetsParsedToTheSameRedValue(string colorInput)
+    {
+        if(SvgColorUtility.TryConvertStringToColor(colorInput, out Color color))
+        {
+            Assert.Equal(255, color.R);
+            Assert.Equal(0, color.G);
+            Assert.Equal(0, color.B);
+            Assert.Equal(255, color.A);
+        }
+        else
+        {
+            Assert.Fail();
+        }
+    }
+}