浏览代码

Merge branch 'master' into development

flabbet 8 月之前
父节点
当前提交
c71fd25e15
共有 77 个文件被更改,包括 2022 次插入244 次删除
  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. 5 1
      src/PixiEditor/Data/Localization/Languages/en.json
  37. 11 1
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  38. 3 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  39. 5 0
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  40. 18 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  41. 4 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  42. 3 1
      src/PixiEditor/Models/Files/SvgFileType.cs
  43. 1 0
      src/PixiEditor/Models/Handlers/Toolbars/IToolbar.cs
  44. 2 1
      src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs
  45. 9 0
      src/PixiEditor/Models/IO/CustomDocumentFormats/IDocumentBuilder.cs
  46. 220 0
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  47. 0 16
      src/PixiEditor/Models/IO/Importer.cs
  48. 5 0
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  49. 9 0
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  50. 6 2
      src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs
  51. 5 2
      src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs
  52. 7 3
      src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs
  53. 7 3
      src/PixiEditor/Models/Serialization/Factories/RectangleSerializationFactory.cs
  54. 148 7
      src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs
  55. 21 2
      src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs
  56. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  57. 26 27
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  58. 32 13
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  59. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  60. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  61. 22 5
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  62. 3 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  63. 13 4
      src/PixiEditor/Views/Input/ToolSettingColorPicker.axaml.cs
  64. 1 0
      src/PixiEditor/Views/Main/Tools/Toolbar.axaml
  65. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  66. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  67. 14 2
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  68. 26 5
      src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs
  69. 10 0
      src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs
  70. 2 0
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  71. 23 4
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  72. 1 1
      src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml
  73. 5 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml
  74. 1 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/FloatSettingView.axaml
  75. 42 63
      src/PixiEditor/Views/Visuals/PixiFilePreviewImage.cs
  76. 0 2
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml
  77. 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 Matrix3X3 TransformationMatrix { get; }
     public Color StrokeColor { get; }
     public Color StrokeColor { get; }
+    public bool Fill { get; }
     public Color FillColor { get; }
     public Color FillColor { get; }
     public float StrokeWidth { get; }
     public float StrokeWidth { get; }
     public RectD GeometryAABB { 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;
         Center = center;
         Size = size;
         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)
     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> Cy { get; } = new("cy");
 
 
     public SvgProperty<SvgNumericUnit> R { get; } = new("r");
     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> Cy { get; } = new("cy"); 
     
     
     public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx"); 
     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;
 using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 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> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     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;
 using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
@@ -19,4 +20,10 @@ public class SvgImage : SvgElement
     {
     {
         RequiredNamespaces.Add("xlink", "http://www.w3.org/1999/xlink");
         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> X2 { get; } = new("x2");
     public SvgProperty<SvgNumericUnit> Y2 { get; } = new("y2");
     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;
 using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
@@ -11,4 +12,10 @@ public class SvgMask() : SvgElement("mask"), IElementContainer
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
     public List<SvgElement> Children { get; } = new();
     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;
 namespace PixiEditor.SVG.Elements;
 
 
 public class SvgPath() : SvgPrimitive("path")
 public class SvgPath() : SvgPrimitive("path")
 {
 {
     public SvgProperty<SvgStringUnit> PathData { get; } = new("d");
     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;
 using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 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<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     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> 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 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;
 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();
         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 Drawie.Numerics;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG;
 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 RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string Version { get; set; } = "1.1";
     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 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()
     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();
         Dictionary<string, string> usedNamespaces = new();
 
 
@@ -24,20 +57,23 @@ public class SvgDocument(RectD viewBox) : IElementContainer
 
 
         foreach (var usedNamespace in usedNamespaces)
         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)
         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)
     private void GatherRequiredNamespaces(Dictionary<string, string> usedNamespaces, List<SvgElement> elements)
@@ -48,10 +84,39 @@ public class SvgDocument(RectD viewBox) : IElementContainer
             {
             {
                 GatherRequiredNamespaces(usedNamespaces, container.Children);
                 GatherRequiredNamespaces(usedNamespaces, container.Children);
             }
             }
+
             foreach (KeyValuePair<string, string> ns in child.RequiredNamespaces)
             foreach (KeyValuePair<string, string> ns in child.RequiredNamespaces)
             {
             {
                 usedNamespaces[ns.Key] = ns.Value;
                 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.Text;
+using System.Xml;
+using System.Xml.Linq;
+using PixiEditor.SVG.Exceptions;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
@@ -10,41 +13,86 @@ public class SvgElement(string tagName)
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public string TagName { get; } = tagName;
     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())
         foreach (var property in GetType().GetProperties())
         {
         {
             if (property.PropertyType.IsAssignableTo(typeof(SvgProperty)))
             if (property.PropertyType.IsAssignableTo(typeof(SvgProperty)))
             {
             {
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
                 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;
         get => (T?)base.Unit;
         set => base.Unit = value;
         set => base.Unit = value;
     }
     }
-
+    
     public SvgProperty(string svgName) : base(svgName)
     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 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)
     public SvgColorUnit(string value)
     {
     {
@@ -38,4 +56,19 @@ public struct SvgColorUnit : ISvgUnit
     {
     {
         return Value;
         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;
 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; }
     public T Value { get; set; }
 
 
@@ -13,6 +15,40 @@ public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
 
 
     public string ToXml()
     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();
         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;
         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)
     public static SvgLinkUnit FromElement(SvgElement element)
     {
     {
         return new SvgLinkUnit
         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 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 double Value { get; set; } = value;
 
 
     public static SvgNumericUnit FromUserUnits(double 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);
         string invariantValue = Value.ToString(CultureInfo.InvariantCulture);
         return $"{invariantValue}{PostFix}";
         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;
         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})";
         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 interface ISvgUnit
 {
 {
     public string ToXml();
     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();
+    }
+}

+ 5 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -783,5 +783,9 @@
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "FILL_MODE": "Fill mode",
   "FILL_MODE": "Fill mode",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
-  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using legacy blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate."
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using legacy blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
+  "FILL_TYPE_WINDING": "Winding",
+  "FILL_TYPE_EVEN_ODD": "Even Odd",
+  "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
+  "FILL_TYPE_INVERSE_EVEN_ODD": "Inverse Even Odd"
 }
 }

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

@@ -10,6 +10,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Skia;
 using PixiEditor.Parser.Skia;
@@ -124,7 +125,7 @@ internal class DocumentViewModelBuilder
         ImageEncoderUsed = encoder;
         ImageEncoderUsed = encoder;
         return this;
         return this;
     }
     }
-    
+
     public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
     public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
     {
     {
         UsesLegacyColorBlending = usesLegacyColorBlending;
         UsesLegacyColorBlending = usesLegacyColorBlending;
@@ -370,6 +371,15 @@ internal class NodeGraphBuilder
         return node;
         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
     internal class NodeBuilder
     {
     {
         public int Id { get; set; }
         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.Files;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.Models.IO.PaletteParsers;
 using PixiEditor.Models.IO.PaletteParsers;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.Localization;
 using PixiEditor.Models.Localization;
@@ -110,6 +111,8 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IoFileType, SvgFileType>()
             .AddSingleton<IoFileType, SvgFileType>()
             // Serialization Factories
             // Serialization Factories
             .AddAssemblyTypes<SerializationFactory>()
             .AddAssemblyTypes<SerializationFactory>()
+            // Custom document builders
+            .AddSingleton<IDocumentBuilder, SvgDocumentBuilder>()
             // Palette Parsers
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()
             .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);
         var any = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Any).GetFormattedTypes(true);
         return any.ToList();
         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);
             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)
         if (masterCorners.AABBBounds.Width == 0 || masterCorners.AABBBounds.Height == 0)
         {
         {

+ 4 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -17,6 +17,7 @@ using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.ViewModels.Tools.Tools;
 using PixiEditor.ViewModels.Tools.Tools;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.Views.Overlays.PathOverlay;
 using PixiEditor.Views.Overlays.PathOverlay;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
@@ -193,7 +194,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
     {
         if(startingPath == null)
         if(startingPath == null)
         {
         {
-            return new PathVectorData(new VectorPath() { FillType = vectorPathToolHandler.FillMode })
+            return new PathVectorData(new VectorPath() { FillType = (PathFillType)vectorPathToolHandler.FillMode })
             {
             {
                 StrokeWidth = (float)toolbar.ToolSize,
                 StrokeWidth = (float)toolbar.ToolSize,
                 StrokeColor = toolbar.StrokeColor.ToColor(),
                 StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -201,7 +202,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             };
             };
         }
         }
         
         
-        return new PathVectorData(new VectorPath(startingPath) { FillType = vectorPathToolHandler.FillMode })
+        return new PathVectorData(new VectorPath(startingPath) { FillType = (PathFillType)vectorPathToolHandler.FillMode })
         {
         {
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeColor = toolbar.StrokeColor.ToColor(),
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -264,6 +265,6 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
         toolbar.ToolSize = pathData.StrokeWidth;
         toolbar.ToolSize = pathData.StrokeWidth;
         toolbar.Fill = pathData.Fill;
         toolbar.Fill = pathData.Fill;
         toolbar.FillColor = pathData.FillColor.ToColor();
         toolbar.FillColor = pathData.FillColor.ToColor();
-        toolbar.GetSetting(nameof(VectorPathToolViewModel.FillMode)).Value = pathData.Path.FillType;
+        toolbar.GetSetting<EnumSettingViewModel<VectorPathFillType>>(nameof(VectorPathToolViewModel.FillMode)).Value = (VectorPathFillType)pathData.Path.FillType;
     }
     }
 }
 }

+ 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.Handlers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
@@ -15,6 +16,7 @@ internal class SvgFileType : IoFileType
     public override string[] Extensions { get; } = new[] { ".svg" };
     public override string[] Extensions { get; } = new[] { ".svg" };
     public override string DisplayName { get; } = "Scalable Vector Graphics";
     public override string DisplayName { get; } = "Scalable Vector Graphics";
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Vector;
     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)
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
     {
     {

+ 1 - 0
src/PixiEditor/Models/Handlers/Toolbars/IToolbar.cs

@@ -8,6 +8,7 @@ internal interface IToolbar : IHandler
 {
 {
     public void AddSetting(Setting setting);
     public void AddSetting(Setting setting);
     public Setting GetSetting(string name);
     public Setting GetSetting(string name);
+    public T GetSetting<T>(string name) where T : Setting;
     public IReadOnlyList<Setting> Settings { get; }
     public IReadOnlyList<Setting> Settings { get; }
     public void SaveToolbarSettings();
     public void SaveToolbarSettings();
     public void LoadSharedSettings();
     public void LoadSharedSettings();

+ 2 - 1
src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs

@@ -1,8 +1,9 @@
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
+using PixiEditor.ViewModels.Tools.Tools;
 
 
 namespace PixiEditor.Models.Handlers.Tools;
 namespace PixiEditor.Models.Handlers.Tools;
 
 
 internal interface IVectorPathToolHandler : IToolHandler
 internal interface IVectorPathToolHandler : IToolHandler
 {
 {
-    public PathFillType FillMode { get; }
+    public VectorPathFillType FillMode { get; }
 }
 }

+ 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)
     public static Surface GetPreviewSurface(string path)
     {
     {
         if (!IsSupportedFile(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));
         _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;
         return value;
     }
     }
+
+    public bool GetBool()
+    {
+        bool value = BitConverter.ToBoolean(_data, Position);
+        
+        Position += sizeof(bool);
+        
+        return value;
+    }
 }
 }

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

@@ -15,8 +15,11 @@ public class EllipseSerializationFactory : VectorShapeSerializationFactory<Ellip
         builder.AddVecD(original.Radius);
         builder.AddVecD(original.Radius);
     }
     }
 
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out EllipseVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out EllipseVectorData original)
     {
     {
         VecD center = extractor.GetVecD();
         VecD center = extractor.GetVecD();
         VecD radius = extractor.GetVecD();
         VecD radius = extractor.GetVecD();
@@ -24,6 +27,7 @@ public class EllipseSerializationFactory : VectorShapeSerializationFactory<Ellip
         original = new EllipseVectorData(center, radius)
         original = new EllipseVectorData(center, radius)
         {
         {
             StrokeColor = strokeColor,
             StrokeColor = strokeColor,
+            Fill = fill,
             FillColor = fillColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
             StrokeWidth = strokeWidth,
             TransformationMatrix = matrix
             TransformationMatrix = matrix

+ 5 - 2
src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs

@@ -15,8 +15,11 @@ internal class LineSerializationFactory : VectorShapeSerializationFactory<LineVe
         builder.AddVecD(original.End);
         builder.AddVecD(original.End);
     }
     }
 
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out LineVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out LineVectorData original)
     {
     {
         VecD start = extractor.GetVecD();
         VecD start = extractor.GetVecD();
         VecD end = extractor.GetVecD();
         VecD end = extractor.GetVecD();

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

@@ -13,8 +13,11 @@ internal class PointsDataSerializationFactory : VectorShapeSerializationFactory<
         builder.AddVecDList(original.Points);
         builder.AddVecDList(original.Points);
     }
     }
 
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out PointsVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out PointsVectorData original)
     {
     {
         List<VecD> points = extractor.GetVecDList();
         List<VecD> points = extractor.GetVecDList();
         original = new PointsVectorData(points)
         original = new PointsVectorData(points)
@@ -22,7 +25,8 @@ internal class PointsDataSerializationFactory : VectorShapeSerializationFactory<
             StrokeColor = strokeColor,
             StrokeColor = strokeColor,
             FillColor = fillColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
         };
 
 
         return true;
         return true;

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

@@ -16,8 +16,11 @@ internal class RectangleSerializationFactory : VectorShapeSerializationFactory<R
         builder.AddVecD(original.Size);
         builder.AddVecD(original.Size);
     }
     }
 
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out RectangleVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out RectangleVectorData original)
     {
     {
         VecD center = extractor.GetVecD();
         VecD center = extractor.GetVecD();
         VecD size = extractor.GetVecD();
         VecD size = extractor.GetVecD();
@@ -27,7 +30,8 @@ internal class RectangleSerializationFactory : VectorShapeSerializationFactory<R
             StrokeColor = strokeColor,
             StrokeColor = strokeColor,
             FillColor = fillColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
         };
 
 
         return true;
         return true;

+ 148 - 7
src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs

@@ -3,31 +3,172 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Views.Overlays.PathOverlay;
 
 
 namespace PixiEditor.Models.Serialization.Factories;
 namespace PixiEditor.Models.Serialization.Factories;
 
 
-internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<PathVectorData> 
+internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<PathVectorData>
 {
 {
     public override string DeserializationId { get; } = "PixiEditor.PathData";
     public override string DeserializationId { get; } = "PixiEditor.PathData";
 
 
     protected override void AddSpecificData(ByteBuilder builder, PathVectorData original)
     protected override void AddSpecificData(ByteBuilder builder, PathVectorData original)
     {
     {
-        builder.AddString(original.Path.ToSvgPathData());
+        if (original.Path == null)
+        {
+            return;
+        }
+
+        EditableVectorPath path = new EditableVectorPath(original.Path);
+
+        builder.AddInt((int)path.Path.FillType);
+        builder.AddInt(path.SubShapes.Count);
+
+        foreach (var subShape in path.SubShapes)
+        {
+            SerializeSubShape(builder, subShape);
+        }
     }
     }
 
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out PathVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        bool fill,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out PathVectorData original)
     {
     {
-        string path = extractor.GetString();
+        VectorPath path;
+        if (IsOldSerializer(serializerData))
+        {
+            string svgPath = extractor.GetString();
+            path = VectorPath.FromSvgPath(svgPath);
+        }
+        else
+        {
+            path = DeserializePath(extractor).ToVectorPath();
+        }
 
 
-        original = new PathVectorData(VectorPath.FromSvgPath(path))
+        original = new PathVectorData(path)
         {
         {
             StrokeColor = strokeColor,
             StrokeColor = strokeColor,
             FillColor = fillColor,
             FillColor = fillColor,
             StrokeWidth = strokeWidth,
             StrokeWidth = strokeWidth,
-            TransformationMatrix = matrix
+            TransformationMatrix = matrix,
+            Fill = fill
         };
         };
 
 
         return true;
         return true;
     }
     }
+
+    private void SerializeSubShape(ByteBuilder builder, SubShape subShape)
+    {
+        builder.AddInt(subShape.IsClosed ? 1 : 0);
+        builder.AddInt(subShape.Points.Count);
+
+        foreach (var point in subShape.Points)
+        {
+            builder.AddFloat(point.Position.X);
+            builder.AddFloat(point.Position.Y);
+
+            builder.AddInt(point.Verb.IsEmptyVerb() ? -1 : (int)point.Verb.VerbType);
+            builder.AddFloat(point.Verb.From.X);
+            builder.AddFloat(point.Verb.From.Y);
+            
+            builder.AddFloat(point.Verb.To.X);
+            builder.AddFloat(point.Verb.To.Y);
+
+            if (HasControlPoint1(point))
+            {
+                builder.AddFloat(point.Verb.ControlPoint1.Value.X);
+                builder.AddFloat(point.Verb.ControlPoint1.Value.Y);
+            }
+
+            if (HasControlPoint2(point))
+            {
+                builder.AddFloat(point.Verb.ControlPoint2.Value.X);
+                builder.AddFloat(point.Verb.ControlPoint2.Value.Y);
+            }
+
+            if (IsConic(point))
+            {
+                builder.AddFloat(point.Verb.ConicWeight);
+            }
+        }
+    }
+    
+    private EditableVectorPath DeserializePath(ByteExtractor extractor)
+    {
+        PathFillType fillType = (PathFillType)extractor.GetInt();
+        int subShapesCount = extractor.GetInt();
+        List<SubShape> subShapes = new List<SubShape>();
+
+        for (int i = 0; i < subShapesCount; i++)
+        {
+            SubShape subShape = DeserializeSubShape(extractor);
+            subShapes.Add(subShape);
+        }
+
+        return new EditableVectorPath(subShapes, fillType);
+    }
+    
+    private SubShape DeserializeSubShape(ByteExtractor extractor)
+    {
+        bool isClosed = extractor.GetInt() == 1;
+        int pointsCount = extractor.GetInt();
+        List<ShapePoint> points = new List<ShapePoint>();
+
+        for (int i = 0; i < pointsCount; i++)
+        {
+            VecF position = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            PathVerb verbType = (PathVerb)extractor.GetInt();
+            
+            VecF from = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            VecF to = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            
+            VecF? controlPoint1 = verbType is PathVerb.Cubic or PathVerb.Quad or PathVerb.Conic
+                ? new VecF(extractor.GetFloat(), extractor.GetFloat())
+                : null;
+            VecF? controlPoint2 = verbType is PathVerb.Cubic or PathVerb.Quad
+                ? new VecF(extractor.GetFloat(), extractor.GetFloat())
+                : null;
+            float conicWeight = verbType == PathVerb.Conic ? extractor.GetFloat() : 0;
+
+            Verb verb = new Verb(verbType, from, to, controlPoint1, controlPoint2, conicWeight);
+            
+            points.Add(new ShapePoint(position, points.Count, verb));
+        }
+
+        return new SubShape(points, isClosed);
+    }
+
+    private bool HasControlPoint1(ShapePoint point)
+    {
+        return (point.Verb.VerbType is PathVerb.Conic or PathVerb.Cubic or PathVerb.Quad) &&
+               point.Verb.ControlPoint1.HasValue;
+    }
+
+    private bool HasControlPoint2(ShapePoint point)
+    {
+        return (point.Verb.VerbType is PathVerb.Cubic or PathVerb.Quad) &&
+               point.Verb.ControlPoint2.HasValue;
+    }
+
+    private bool IsConic(ShapePoint point)
+    {
+        return point.Verb.VerbType == PathVerb.Conic;
+    }
+
+    private bool IsOldSerializer((string serializerName, string serializerVersion) serializerData)
+    {
+        if (string.IsNullOrEmpty(serializerData.serializerName) ||
+            string.IsNullOrEmpty(serializerData.serializerVersion))
+        {
+            return false;
+        }
+
+        if (Version.TryParse(serializerData.serializerVersion, out Version version))
+        {
+            return version is { Major: 2, Minor: 0, Build: 0, Revision: < 35 };
+        }
+
+        return false;
+    }
 }
 }

+ 21 - 2
src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs

@@ -11,6 +11,7 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         ByteBuilder builder = new ByteBuilder();
         ByteBuilder builder = new ByteBuilder();
         builder.AddMatrix3X3(original.TransformationMatrix);
         builder.AddMatrix3X3(original.TransformationMatrix);
         builder.AddColor(original.StrokeColor);
         builder.AddColor(original.StrokeColor);
+        builder.AddBool(original.Fill);
         builder.AddColor(original.FillColor);
         builder.AddColor(original.FillColor);
         builder.AddFloat(original.StrokeWidth);
         builder.AddFloat(original.StrokeWidth);
         
         
@@ -34,6 +35,7 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         
         
         Matrix3X3 matrix = extractor.GetMatrix3X3();
         Matrix3X3 matrix = extractor.GetMatrix3X3();
         Color strokeColor = extractor.GetColor();
         Color strokeColor = extractor.GetColor();
+        bool fill = TryGetBool(extractor, serializerData);
         Color fillColor = extractor.GetColor();
         Color fillColor = extractor.GetColor();
         float strokeWidth;
         float strokeWidth;
         // Previous versions of the serializer saved stroke as int, and serializer data didn't exist
         // Previous versions of the serializer saved stroke as int, and serializer data didn't exist
@@ -46,8 +48,25 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
             strokeWidth = extractor.GetFloat();
             strokeWidth = extractor.GetFloat();
         }
         }
 
 
-        return DeserializeVectorData(extractor, matrix, strokeColor, fillColor, strokeWidth, out original);
+        return DeserializeVectorData(extractor, matrix, strokeColor, fill, fillColor, strokeWidth, serializerData, out original);
     }
     }
     
     
-    protected abstract bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor, float strokeWidth, out T 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();
+    }
 }
 }

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.34")]
-[assembly: AssemblyFileVersion("2.0.0.34")]
+[assembly: AssemblyVersion("2.0.0.35")]
+[assembly: AssemblyFileVersion("2.0.0.35")]

+ 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;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
@@ -148,17 +149,22 @@ internal partial class DocumentViewModel
             elementToAdd = AddVectorPath(shapeData);
             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)
         if (elementToAdd != null)
@@ -178,7 +184,7 @@ internal partial class DocumentViewModel
         line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
         line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
             lineData.StrokeColor.B, lineData.StrokeColor.A);
             lineData.StrokeColor.B, lineData.StrokeColor.A);
         line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth);
         line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth);
-        
+
         return line;
         return line;
     }
     }
 
 
@@ -189,11 +195,6 @@ internal partial class DocumentViewModel
         ellipse.Cy.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.Y);
         ellipse.Cy.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.Y);
         ellipse.Rx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.X);
         ellipse.Rx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.X);
         ellipse.Ry.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.Y);
         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;
         return ellipse;
     }
     }
@@ -212,11 +213,6 @@ internal partial class DocumentViewModel
 
 
         rect.Width.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.X);
         rect.Width.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.X);
         rect.Height.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.Y);
         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;
         return rect;
     }
     }
@@ -228,12 +224,15 @@ internal partial class DocumentViewModel
         {
         {
             string pathData = data.Path.ToSvgPathData();
             string pathData = data.Path.ToSvgPathData();
             path.PathData.Unit = new SvgStringUnit(pathData);
             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;
         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;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using Avalonia.Threading;
 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;
-using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Exceptions;
 using PixiEditor.Exceptions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
@@ -30,9 +19,10 @@ using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserData;
 using PixiEditor.Models.UserData;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Graph;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views;
 using PixiEditor.Views;
@@ -57,6 +47,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     }
     }
 
 
     public RecentlyOpenedCollection RecentlyOpened { get; init; }
     public RecentlyOpenedCollection RecentlyOpened { get; init; }
+    public IReadOnlyList<IDocumentBuilder> DocumentBuilders => documentBuilders;
+    
+    private List<IDocumentBuilder> documentBuilders;
 
 
     public FileViewModel(ViewModelMain owner)
     public FileViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
@@ -70,6 +63,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
 
 
         PixiEditorSettings.File.MaxOpenedRecently.ValueChanged += (_, value) => UpdateMaxRecentlyOpened(value);
         PixiEditorSettings.File.MaxOpenedRecently.ValueChanged += (_, value) => UpdateMaxRecentlyOpened(value);
+        documentBuilders = owner.Services.GetServices<IDocumentBuilder>().ToList();
     }
     }
 
 
     public void AddRecentlyOpened(string path)
     public void AddRecentlyOpened(string path)
@@ -209,6 +203,10 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             {
             {
                 OpenDotPixi(path, associatePath);
                 OpenDotPixi(path, associatePath);
             }
             }
+            else if (IsCustomFormat(path))
+            {
+                OpenCustomFormat(path);
+            }
             else
             else
             {
             {
                 OpenRegularImage(path, associatePath);
                 OpenRegularImage(path, associatePath);
@@ -223,6 +221,27 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
             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>
     /// <summary>
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system
     /// 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)]
 [Command.Tool(Key = Key.C)]
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 {
 {
+    public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME"; 
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
     public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
 
 
@@ -33,7 +34,7 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     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)
     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)]
 [Command.Tool(Key = Key.L)]
 internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 {
 {
+    public const string NewLayerKey = "NEW_LINE_LAYER_NAME";
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
 
     public override bool IsErasable => false;
     public override bool IsErasable => false;
@@ -34,7 +35,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
 
     public override string DefaultIcon => PixiPerfectIcons.Line;
     public override string DefaultIcon => PixiPerfectIcons.Line;
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override Type[]? SupportedLayerTypes { get; } = [];
-    public string? DefaultNewLayerName { get; } = new LocalizedString("NEW_LINE_LAYER_NAME");
+    public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
 
     [Settings.Inherited] 
     [Settings.Inherited] 
     public double ToolSize => GetValue<double>();
     public double ToolSize => GetValue<double>();

+ 22 - 5
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -1,4 +1,5 @@
-using Avalonia.Input;
+using System.ComponentModel;
+using Avalonia.Input;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -14,12 +15,13 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.P)]
 [Command.Tool(Key = Key.P)]
 internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
 internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
 {
 {
+    public const string NewLayerKey = "DEFAULT_PATH_LAYER_NAME";
     public override string ToolNameLocalizationKey => "PATH_TOOL";
     public override string ToolNameLocalizationKey => "PATH_TOOL";
     public override Type[]? SupportedLayerTypes { get; } = [typeof(IVectorLayerHandler)];
     public override Type[]? SupportedLayerTypes { get; } = [typeof(IVectorLayerHandler)];
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public override LocalizedString Tooltip => new LocalizedString("PATH_TOOL_TOOLTIP", Shortcut);
     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 string DefaultIcon => PixiPerfectIcons.VectorPen;
     public override bool StopsLinkedToolOnUse => false;
     public override bool StopsLinkedToolOnUse => false;
@@ -33,10 +35,10 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     private LocalizedString actionDisplayShift;
     private LocalizedString actionDisplayShift;
     private LocalizedString actionDisplayCtrlShift;
     private LocalizedString actionDisplayCtrlShift;
 
 
-    [Settings.Enum("FILL_MODE", PathFillType.Winding)]
-    public PathFillType FillMode
+    [Settings.Enum("FILL_MODE", VectorPathFillType.Winding)]
+    public VectorPathFillType FillMode
     {
     {
-        get => GetValue<PathFillType>();
+        get => GetValue<VectorPathFillType>();
     }
     }
 
 
     public VectorPathToolViewModel()
     public VectorPathToolViewModel()
@@ -119,3 +121,18 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         OnSelected(false);
         OnSelected(false);
     }
     }
 }
 }
+
+enum VectorPathFillType
+{
+    [Description("FILL_TYPE_WINDING")]
+    
+    Winding,
+    [Description("FILL_TYPE_EVEN_ODD")]
+    EvenOdd,
+    
+    [Description("FILL_TYPE_INVERSE_WINDING")]
+    InverseWinding,
+    
+    [Description("FILL_TYPE_INVERSE_EVEN_ODD")]
+    InverseEvenOdd
+}

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

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

+ 13 - 4
src/PixiEditor/Views/Input/ToolSettingColorPicker.axaml.cs

@@ -1,25 +1,34 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
+using PixiEditor.Helpers.Behaviours;
 
 
 namespace PixiEditor.Views.Input;
 namespace PixiEditor.Views.Input;
 
 
 internal partial class ToolSettingColorPicker : UserControl
 internal partial class ToolSettingColorPicker : UserControl
 {
 {
-    public static readonly StyledProperty<Color> SelectedColorProperty = AvaloniaProperty.Register<ToolSettingColorPicker, Color>(
-        nameof(SelectedColor));
+    public static readonly StyledProperty<Color> SelectedColorProperty =
+        AvaloniaProperty.Register<ToolSettingColorPicker, Color>(
+            nameof(SelectedColor));
 
 
     public Color SelectedColor
     public Color SelectedColor
     {
     {
         get => GetValue(SelectedColorProperty);
         get => GetValue(SelectedColorProperty);
         set => SetValue(SelectedColorProperty, value);
         set => SetValue(SelectedColorProperty, value);
     }
     }
-    
+
     public ToolSettingColorPicker()
     public ToolSettingColorPicker()
     {
     {
         InitializeComponent();
         InitializeComponent();
         ColorPicker.SecondaryColor = Colors.Black;
         ColorPicker.SecondaryColor = Colors.Black;
+        ColorPicker.TemplateApplied += ColorPickerOnTemplateApplied;
     }
     }
-}
 
 
+    private void ColorPickerOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
+    {
+        ColorPicker.FindDescendantOfType<ToggleButton>().Focusable = false;
+    }
+}

+ 1 - 0
src/PixiEditor/Views/Main/Tools/Toolbar.axaml

@@ -12,6 +12,7 @@
             BorderBrush="{DynamicResource ThemeBorderMidBrush}"
             BorderBrush="{DynamicResource ThemeBorderMidBrush}"
             BorderThickness="{DynamicResource ThemeBorderThickness}"
             BorderThickness="{DynamicResource ThemeBorderThickness}"
             Cursor="Arrow"
             Cursor="Arrow"
+            IsHitTestVisible="True"
             Padding="5"
             Padding="5"
             Height="40"
             Height="40"
             HorizontalAlignment="Left"
             HorizontalAlignment="Left"

+ 1 - 1
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -162,7 +162,7 @@
                     ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}"
                     ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}"
                     VerticalAlignment="Center" />
                     VerticalAlignment="Center" />
             </Border>
             </Border>
-            <tools:ToolsPicker Grid.Row="2" 
+            <tools:ToolsPicker Grid.Row="2" IsHitTestVisible="True"
                 HorizontalAlignment="Left"
                 HorizontalAlignment="Left"
                 ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet, Mode=TwoWay}"
                 ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet, Mode=TwoWay}"
                                ToolSets="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.AllToolSets, Mode=OneWay}"
                                ToolSets="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.AllToolSets, Mode=OneWay}"

+ 1 - 1
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -394,7 +394,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
 
     private void Image_MouseDown(object? sender, PointerPressedEventArgs e)
     private void Image_MouseDown(object? sender, PointerPressedEventArgs e)
     {
     {
-        if (Document is null)
+        if (Document is null || e.Source != Scene)
             return;
             return;
 
 
         bool isMiddle = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
         bool isMiddle = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;

+ 14 - 2
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -111,6 +111,7 @@ internal class LineToolOverlay : Overlay
 
 
         startHandle = new AnchorHandle(this);
         startHandle = new AnchorHandle(this);
         startHandle.StrokePaint = blackPaint;
         startHandle.StrokePaint = blackPaint;
+        startHandle.OnPress += OnHandlePress;
         startHandle.OnDrag += StartHandleOnDrag;
         startHandle.OnDrag += StartHandleOnDrag;
         startHandle.OnHover += (handle, _) => Refresh();
         startHandle.OnHover += (handle, _) => Refresh();
         startHandle.OnRelease += OnHandleRelease;
         startHandle.OnRelease += OnHandleRelease;
@@ -119,6 +120,7 @@ internal class LineToolOverlay : Overlay
 
 
         endHandle = new AnchorHandle(this);
         endHandle = new AnchorHandle(this);
         endHandle.StrokePaint = blackPaint;
         endHandle.StrokePaint = blackPaint;
+        endHandle.OnPress += OnHandlePress;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.OnHover += (handle, _) => Refresh();
         endHandle.OnHover += (handle, _) => Refresh();
@@ -127,6 +129,7 @@ internal class LineToolOverlay : Overlay
 
 
         moveHandle = new TransformHandle(this);
         moveHandle = new TransformHandle(this);
         moveHandle.StrokePaint = blackPaint;
         moveHandle.StrokePaint = blackPaint;
+        moveHandle.OnPress += OnHandlePress;
         moveHandle.OnDrag += MoveHandleOnDrag;
         moveHandle.OnDrag += MoveHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         moveHandle.OnHover += (handle, _) => Refresh();
         moveHandle.OnHover += (handle, _) => Refresh();
@@ -192,10 +195,10 @@ internal class LineToolOverlay : Overlay
             var matrix = context.TotalMatrix;
             var matrix = context.TotalMatrix;
             VecD pos = matrix.MapPoint(lastMousePos);
             VecD pos = matrix.MapPoint(lastMousePos);
             context.SetMatrix(Matrix3X3.Identity);
             context.SetMatrix(Matrix3X3.Identity);
-            
+
             string length = $"L: {(mappedEnd - mappedStart).Length:0.#} px";
             string length = $"L: {(mappedEnd - mappedStart).Length:0.#} px";
             infoBox.DrawInfo(context, length, pos);
             infoBox.DrawInfo(context, length, pos);
-            
+
             context.RestoreToCount(toRestore);
             context.RestoreToCount(toRestore);
         }
         }
     }
     }
@@ -263,6 +266,15 @@ internal class LineToolOverlay : Overlay
         return final;
         return final;
     }
     }
 
 
+    private void OnHandlePress(Handle source, OverlayPointerArgs args)
+    {
+        movedWhileMouseDown = false;
+        mouseDownPos = args.Point;
+
+        lineStartOnMouseDown = LineStart;
+        lineEndOnMouseDown = LineEnd;
+    }
+
     private void MoveHandleOnDrag(Handle source, OverlayPointerArgs args)
     private void MoveHandleOnDrag(Handle source, OverlayPointerArgs args)
     {
     {
         var delta = args.Point - mouseDownPos;
         var delta = args.Point - mouseDownPos;

+ 26 - 5
src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs

@@ -5,9 +5,9 @@ namespace PixiEditor.Views.Overlays.PathOverlay;
 
 
 public class EditableVectorPath
 public class EditableVectorPath
 {
 {
-    private VectorPath path;
+    private VectorPath? path;
 
 
-    public VectorPath Path
+    public VectorPath? Path
     {
     {
         get => path;
         get => path;
         set
         set
@@ -23,6 +23,8 @@ public class EditableVectorPath
 
 
     public int TotalPoints => subShapes.Sum(x => x.Points.Count);
     public int TotalPoints => subShapes.Sum(x => x.Points.Count);
 
 
+    public PathFillType FillType { get; set; }
+
     public int ControlPointsCount
     public int ControlPointsCount
     {
     {
         get
         get
@@ -32,6 +34,12 @@ public class EditableVectorPath
         }
         }
     }
     }
 
 
+    public EditableVectorPath(IEnumerable<SubShape> subShapes, PathFillType fillType)
+    {
+        this.subShapes = new List<SubShape>(subShapes);
+        FillType = fillType;
+    }
+
     public EditableVectorPath(VectorPath path)
     public EditableVectorPath(VectorPath path)
     {
     {
         if (path != null)
         if (path != null)
@@ -47,8 +55,19 @@ public class EditableVectorPath
 
 
     public VectorPath ToVectorPath()
     public VectorPath ToVectorPath()
     {
     {
-        VectorPath newPath = new VectorPath(Path);
-        newPath.Reset(); // preserve fill type and other properties
+        VectorPath newPath;
+        if (Path != null)
+        {
+            newPath = new VectorPath(Path);
+            newPath.Reset(); // preserve fill type and other properties
+        }
+        else
+        {
+            newPath = new VectorPath();
+        }
+        
+        newPath.FillType = FillType;
+
         foreach (var subShape in subShapes)
         foreach (var subShape in subShapes)
         {
         {
             AddVerbToPath(CreateMoveToVerb(subShape), newPath);
             AddVerbToPath(CreateMoveToVerb(subShape), newPath);
@@ -127,7 +146,7 @@ public class EditableVectorPath
                     {
                     {
                         subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
                         subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
                         currentSubShapePoints.Clear();
                         currentSubShapePoints.Clear();
-                        
+
                         currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
                         currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
                     }
                     }
                     else
                     else
@@ -143,6 +162,8 @@ public class EditableVectorPath
 
 
             globalVerbIndex++;
             globalVerbIndex++;
         }
         }
+
+        FillType = from.FillType;
     }
     }
 
 
     private void AddVerbToPath(Verb verb, VectorPath newPath)
     private void AddVerbToPath(Verb verb, VectorPath newPath)

+ 10 - 0
src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs

@@ -78,6 +78,16 @@ public class Verb
         VerbType = null;
         VerbType = null;
     }
     }
     
     
+    public Verb(PathVerb verb, VecF from, VecF to, VecF? controlPoint1, VecF? controlPoint2, float conicWeight)
+    {
+        VerbType = verb;
+        From = from;
+        To = to;
+        ControlPoint1 = controlPoint1;
+        ControlPoint2 = controlPoint2;
+        ConicWeight = conicWeight;
+    }
+    
     public Verb((PathVerb verb, VecF[] points, float conicWeight) verbData)
     public Verb((PathVerb verb, VecF[] points, float conicWeight) verbData)
     {
     {
         VerbType = verbData.verb;
         VerbType = verbData.verb;

+ 2 - 0
src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -359,6 +359,8 @@ internal class SymmetryOverlay : Overlay
 
 
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
         }
         }
+        
+        Refresh();
     }
     }
 
 
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)

+ 23 - 4
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -306,13 +306,16 @@ internal class TransformOverlay : Overlay
         ForAllHandles<AnchorHandle>(x =>
         ForAllHandles<AnchorHandle>(x =>
         {
         {
             x.OnPress += OnAnchorHandlePressed;
             x.OnPress += OnAnchorHandlePressed;
+            x.OnDrag += OnAnchorHandleDrag;
             x.OnRelease += OnAnchorHandleReleased;
             x.OnRelease += OnAnchorHandleReleased;
         });
         });
 
 
         originHandle.OnPress += OnAnchorHandlePressed;
         originHandle.OnPress += OnAnchorHandlePressed;
+        originHandle.OnDrag += OnAnchorHandleDrag;
         originHandle.OnRelease += OnAnchorHandleReleased;
         originHandle.OnRelease += OnAnchorHandleReleased;
 
 
         moveHandle.OnPress += OnMoveHandlePressed;
         moveHandle.OnPress += OnMoveHandlePressed;
+        moveHandle.OnDrag += OnMoveHandleDrag;
         moveHandle.OnRelease += OnMoveHandleReleased;
         moveHandle.OnRelease += OnMoveHandleReleased;
 
 
         infoBox = new InfoBox();
         infoBox = new InfoBox();
@@ -506,6 +509,20 @@ internal class TransformOverlay : Overlay
         args.Handled = true;
         args.Handled = true;
     }
     }
 
 
+    private void OnAnchorHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        HandleCapturedAnchorMovement(args.Point);
+        lastPointerPos = args.Point;
+    }
+
+    private void OnMoveHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        HandleTransform(lastPointerPos);
+        Cursor = new Cursor(StandardCursorType.DragMove);
+        actuallyMoved = true;
+        lastPointerPos = args.Point;
+    }
+
     protected override void OnOverlayPointerMoved(OverlayPointerArgs e)
     protected override void OnOverlayPointerMoved(OverlayPointerArgs e)
     {
     {
         Cursor finalCursor = new Cursor(StandardCursorType.Arrow);
         Cursor finalCursor = new Cursor(StandardCursorType.Arrow);
@@ -522,7 +539,7 @@ internal class TransformOverlay : Overlay
 
 
         if (capturedAnchor is not null)
         if (capturedAnchor is not null)
         {
         {
-            HandleCapturedAnchorMovement(e);
+            HandleCapturedAnchorMovement(e.Point);
             return;
             return;
         }
         }
 
 
@@ -594,7 +611,7 @@ internal class TransformOverlay : Overlay
             TopLeft = scaled.TopLeft - new VecD(offsetToScale, offsetToScale),
             TopLeft = scaled.TopLeft - new VecD(offsetToScale, offsetToScale),
             TopRight = scaled.TopRight - new VecD(-offsetToScale, offsetToScale),
             TopRight = scaled.TopRight - new VecD(-offsetToScale, offsetToScale),
         };
         };
-        
+
         scaledCorners = scaledCorners.AsRotated(Corners.RectRotation, Corners.RectCenter);
         scaledCorners = scaledCorners.AsRotated(Corners.RectRotation, Corners.RectCenter);
 
 
         return base.TestHit(point) || scaledCorners.IsPointInside(point);
         return base.TestHit(point) || scaledCorners.IsPointInside(point);
@@ -719,7 +736,7 @@ internal class TransformOverlay : Overlay
         return true;
         return true;
     }
     }
 
 
-    private void HandleCapturedAnchorMovement(OverlayPointerArgs e)
+    private void HandleCapturedAnchorMovement(VecD point)
     {
     {
         if (capturedAnchor is null)
         if (capturedAnchor is null)
             throw new InvalidOperationException("No anchor is captured");
             throw new InvalidOperationException("No anchor is captured");
@@ -728,7 +745,7 @@ internal class TransformOverlay : Overlay
             (TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked))
             (TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked))
             return;
             return;
 
 
-        pos = e.Point;
+        pos = point;
 
 
         if (TransformHelper.IsCorner((Anchor)capturedAnchor))
         if (TransformHelper.IsCorner((Anchor)capturedAnchor))
         {
         {
@@ -1073,6 +1090,8 @@ internal class TransformOverlay : Overlay
 
 
         if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
         if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
             ActionCompleted.Execute(null);
+
+        IsSizeBoxEnabled = false;
     }
     }
 
 
     private Handle? GetSnapHandleOfOrigin()
     private Handle? GetSnapHandleOfOrigin()

+ 1 - 1
src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml

@@ -9,5 +9,5 @@
         <settings:BoolSettingViewModel/>
         <settings:BoolSettingViewModel/>
     </Design.DataContext>
     </Design.DataContext>
     
     
-    <CheckBox VerticalAlignment="Center" IsChecked="{Binding Value, Mode=TwoWay}"/>
+    <CheckBox VerticalAlignment="Center" Focusable="False" IsChecked="{Binding Value, Mode=TwoWay}"/>
 </UserControl>
 </UserControl>

+ 5 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml

@@ -22,5 +22,10 @@
                 <Setter Property="(ui:Translator.Key)" Value="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
                 <Setter Property="(ui:Translator.Key)" Value="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
             </ControlTheme>
             </ControlTheme>
         </ComboBox.ItemContainerTheme>
         </ComboBox.ItemContainerTheme>
+       <ComboBox.ItemTemplate>
+           <DataTemplate>
+               <TextBlock ui:Translator.Key="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
+           </DataTemplate>
+       </ComboBox.ItemTemplate>
     </ComboBox>
     </ComboBox>
 </UserControl>
 </UserControl>

+ 1 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/FloatSettingView.axaml

@@ -14,5 +14,6 @@
                        Min="{Binding Min}"
                        Min="{Binding Min}"
                        Max="{Binding Max}"
                        Max="{Binding Max}"
                        Margin="0,0,0,0" 
                        Margin="0,0,0,0" 
+                       FocusNext="False"
                        Width="40" />
                        Width="40" />
 </UserControl>
 </UserControl>

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

@@ -1,7 +1,5 @@
-using System.Drawing;
-using Avalonia;
+using Avalonia;
 using Avalonia.Threading;
 using Avalonia.Threading;
-using FFMpegCore.Enums;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
@@ -9,11 +7,10 @@ using PixiEditor.Models;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
-using Image = Avalonia.Controls.Image;
 
 
 namespace PixiEditor.Views.Visuals;
 namespace PixiEditor.Views.Visuals;
 
 
-internal class PixiFilePreviewImage : SurfaceControl
+internal class PixiFilePreviewImage : TextureControl
 {
 {
     public static readonly StyledProperty<string> FilePathProperty =
     public static readonly StyledProperty<string> FilePathProperty =
         AvaloniaProperty.Register<PixiFilePreviewImage, string>(nameof(FilePath));
         AvaloniaProperty.Register<PixiFilePreviewImage, string>(nameof(FilePath));
@@ -54,86 +51,63 @@ internal class PixiFilePreviewImage : SurfaceControl
         Task.Run(() => LoadImage(path));
         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;
             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
         try
         {
         {
-            loaded = Surface.Load(filePath);
+            loaded = Texture.Load(textureBytes);
         }
         }
         catch (RecoverableException)
         catch (RecoverableException)
         {
         {
@@ -151,26 +125,31 @@ internal class PixiFilePreviewImage : SurfaceControl
         var downscaled = DownscaleSurface(loaded);
         var downscaled = DownscaleSurface(loaded);
         loaded.Dispose();
         loaded.Dispose();
         return downscaled;
         return downscaled;
-
     }
     }
 
 
-    private static Surface DownscaleSurface(Surface surface)
+    private static Texture DownscaleSurface(Texture surface)
     {
     {
         double factor = Math.Min(
         double factor = Math.Min(
             Constants.MaxPreviewWidth / (double)surface.Size.X,
             Constants.MaxPreviewWidth / (double)surface.Size.X,
             Constants.MaxPreviewHeight / (double)surface.Size.Y);
             Constants.MaxPreviewHeight / (double)surface.Size.Y);
 
 
         var newSize = new VecI((int)(surface.Size.X * factor), (int)(surface.Size.Y * factor));
         var newSize = new VecI((int)(surface.Size.X * factor), (int)(surface.Size.Y * factor));
-        
+
         var scaledBitmap = surface.Resize(newSize, ResizeMethod.HighQuality);
         var scaledBitmap = surface.Resize(newSize, ResizeMethod.HighQuality);
 
 
         surface.Dispose();
         surface.Dispose();
         return scaledBitmap;
         return scaledBitmap;
     }
     }
-    
+
     // TODO: This does not actually set the dot to gray
     // TODO: This does not actually set the dot to gray
     void SetCorrupt()
     void SetCorrupt()
     {
     {
         Dispatcher.UIThread.Post(() => Corrupt = true);
         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:newsFeed1="clr-namespace:PixiEditor.Views.NewsFeed"
                          xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
                          xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
                          xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
                          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"
                          xmlns:windows="clr-namespace:PixiEditor.Views.Windows"
                          mc:Ignorable="d"
                          mc:Ignorable="d"
                          Title="Hello there!" Height="680" Width="982" MinHeight="500" MinWidth="600"
                          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();
+        }
+    }
+}