Browse Source

Implemented gradients saving

Krzysztof Krysiński 4 months ago
parent
commit
d1542956d3
31 changed files with 395 additions and 52 deletions
  1. 1 1
      src/Drawie
  2. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  3. 33 0
      src/PixiEditor.SVG/DefStorage.cs
  4. 19 0
      src/PixiEditor.SVG/Elements/SvgClipPath.cs
  5. 3 4
      src/PixiEditor.SVG/Elements/SvgLinearGradient.cs
  6. 3 3
      src/PixiEditor.SVG/Elements/SvgRadialGradient.cs
  7. 0 7
      src/PixiEditor.SVG/Enums/SvgGradientUnit.cs
  8. 11 0
      src/PixiEditor.SVG/Enums/SvgRelativityUnit.cs
  9. 22 13
      src/PixiEditor.SVG/SvgDocument.cs
  10. 5 5
      src/PixiEditor.SVG/SvgElement.cs
  11. 2 1
      src/PixiEditor.SVG/SvgParser.cs
  12. 1 1
      src/PixiEditor.SVG/Units/SvgColorUnit.cs
  13. 1 1
      src/PixiEditor.SVG/Units/SvgEnumUnit.cs
  14. 1 1
      src/PixiEditor.SVG/Units/SvgLinkUnit.cs
  15. 1 1
      src/PixiEditor.SVG/Units/SvgNumericUnit.cs
  16. 99 3
      src/PixiEditor.SVG/Units/SvgPaintServerUnit.cs
  17. 1 1
      src/PixiEditor.SVG/Units/SvgRectUnit.cs
  18. 1 1
      src/PixiEditor.SVG/Units/SvgStringUnit.cs
  19. 1 1
      src/PixiEditor.SVG/Units/SvgStyleUnit.cs
  20. 1 1
      src/PixiEditor.SVG/Units/SvgTransformUnit.cs
  21. 1 1
      src/PixiEditor.SVG/Units/SvgUnit.cs
  22. 4 1
      src/PixiEditor/Models/Files/PixiFileType.cs
  23. 79 0
      src/PixiEditor/Models/Serialization/Factories/Paintables/GradientPaintableSerializationFactory.cs
  24. 26 0
      src/PixiEditor/Models/Serialization/Factories/Paintables/LinearGradientSerializationFactory.cs
  25. 24 0
      src/PixiEditor/Models/Serialization/Factories/Paintables/RadialGradientSerializationFactory.cs
  26. 22 0
      src/PixiEditor/Models/Serialization/Factories/Paintables/SweepGradientSerializationFactory.cs
  27. 2 2
      src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs
  28. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  29. 14 0
      tests/PixiEditor.Tests/SerializationTests.cs
  30. 1 1
      tests/PixiEditor.Tests/SvgTests.cs
  31. 14 0
      tests/PixiEditorTests.sln

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 411b72fe4a39f0c9dd6f8ea61dbad7e6875f60b6
+Subproject commit 90d6ea44b3829eeb6bc945a0e5e59935d3ce2daa

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


+ 33 - 0
src/PixiEditor.SVG/DefStorage.cs

@@ -0,0 +1,33 @@
+using PixiEditor.SVG.Elements;
+
+namespace PixiEditor.SVG;
+
+public class DefStorage
+{
+    public SvgDocument Root { get; }
+    public SvgDefs? Defs { get; private set; }
+
+    private int _idCounter = 0;
+    public DefStorage(SvgDocument root)
+    {
+        Root = root;
+    }
+
+    public void AddDef(SvgElement def)
+    {
+        if (Defs == null)
+        {
+            Defs = new SvgDefs();
+            Root.Defs = Defs;
+        }
+
+        Defs.Children.Add(def);
+        _idCounter++;
+    }
+
+    public string GetNextId()
+    {
+        return _idCounter.ToString();
+    }
+}
+

+ 19 - 0
src/PixiEditor.SVG/Elements/SvgClipPath.cs

@@ -0,0 +1,19 @@
+using System.Xml;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgClipPath() : SvgElement("clipPath"), IElementContainer
+{
+    public List<SvgElement> Children { get; } = new();
+
+    public SvgProperty<SvgEnumUnit<SvgRelativityUnit>> ClipPathUnits { get; } = new("clipPathUnits");
+
+    public override void ParseData(XmlReader reader, SvgDefs defs)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { ClipPathUnits };
+        ParseAttributes(properties, reader, defs);
+    }
+}

+ 3 - 4
src/PixiEditor.SVG/Elements/SvgLinearGradient.cs

@@ -18,7 +18,7 @@ public class SvgLinearGradient() : SvgElement("linearGradient"), IElementContain
     public SvgProperty<SvgNumericUnit> X2 { get; } = new("x2");
     public SvgProperty<SvgNumericUnit> Y2 { get; } = new("y2");
     public SvgProperty<SvgEnumUnit<SvgSpreadMethod>> SpreadMethod { get; } = new("spreadMethod");
-    public SvgProperty<SvgEnumUnit<SvgGradientUnit>> GradientUnits { get; } = new("gradientUnits");
+    public SvgProperty<SvgEnumUnit<SvgRelativityUnit>> GradientUnits { get; } = new("gradientUnits");
 
     public override void ParseData(XmlReader reader, SvgDefs defs)
     {
@@ -58,13 +58,12 @@ public class SvgLinearGradient() : SvgElement("linearGradient"), IElementContain
             }
         }
 
-        var unit = GetUnit(GradientUnits)?.Value ?? SvgGradientUnit.ObjectBoundingBox;
+        var unit = GetUnit(GradientUnits)?.Value ?? SvgRelativityUnit.ObjectBoundingBox;
         var transform = GetUnit(GradientTransform)?.MatrixValue ?? Matrix3X3.Identity;
 
-        // TODO: Implement gradient transform, spread method and gradient units
         return new LinearGradientPaintable(start, end, gradientStops)
         {
-            AbsoluteValues = unit == SvgGradientUnit.UserSpaceOnUse,
+            AbsoluteValues = unit == SvgRelativityUnit.UserSpaceOnUse,
             Transform = transform
         };
     }

+ 3 - 3
src/PixiEditor.SVG/Elements/SvgRadialGradient.cs

@@ -19,7 +19,7 @@ public class SvgRadialGradient() : SvgElement("radialGradient"), IElementContain
     public SvgProperty<SvgNumericUnit> Fx { get; } = new("fx");
     public SvgProperty<SvgNumericUnit> Fy { get; } = new("fy");
     public SvgProperty<SvgEnumUnit<SvgSpreadMethod>> SpreadMethod { get; } = new("spreadMethod");
-    public SvgProperty<SvgEnumUnit<SvgGradientUnit>> GradientUnits { get; } = new("gradientUnits");
+    public SvgProperty<SvgEnumUnit<SvgRelativityUnit>> GradientUnits { get; } = new("gradientUnits");
 
     public override void ParseData(XmlReader reader, SvgDefs defs)
     {
@@ -61,13 +61,13 @@ public class SvgRadialGradient() : SvgElement("radialGradient"), IElementContain
             }
         }
 
-        var unit = GetUnit(GradientUnits)?.Value ?? SvgGradientUnit.ObjectBoundingBox;
+        var unit = GetUnit(GradientUnits)?.Value ?? SvgRelativityUnit.ObjectBoundingBox;
         var transform = GetUnit(GradientTransform)?.MatrixValue ?? Matrix3X3.Identity;
 
         RadialGradientPaintable radialGradientPaintable =
             new(center, radius, gradientStops)
             {
-                AbsoluteValues = unit == SvgGradientUnit.UserSpaceOnUse,
+                AbsoluteValues = unit == SvgRelativityUnit.UserSpaceOnUse,
                 Transform = transform
             };
 

+ 0 - 7
src/PixiEditor.SVG/Enums/SvgGradientUnit.cs

@@ -1,7 +0,0 @@
-namespace PixiEditor.SVG.Enums;
-
-public enum SvgGradientUnit
-{
-    UserSpaceOnUse,
-    ObjectBoundingBox
-}

+ 11 - 0
src/PixiEditor.SVG/Enums/SvgRelativityUnit.cs

@@ -0,0 +1,11 @@
+using PixiEditor.SVG.Attributes;
+
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgRelativityUnit
+{
+    [SvgValue("userSpaceOnUse")]
+    UserSpaceOnUse,
+    [SvgValue("objectBoundingBox")]
+    ObjectBoundingBox
+}

+ 22 - 13
src/PixiEditor.SVG/SvgDocument.cs

@@ -69,16 +69,25 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
 
         GatherRequiredNamespaces(usedNamespaces, Children);
 
-        foreach (var usedNamespace in usedNamespaces)
+        DefStorage defs = new(this);
+
+        AppendProperties(document.Root, defs);
+
+
+        foreach (SvgElement child in Children)
         {
-            document.Root.Add(new XAttribute(XNamespace.Xmlns + usedNamespace.Key, usedNamespace.Value));
+            document.Root.Add(child.ToXml(ns, defs));
         }
 
-        AppendProperties(document.Root);
+        if (Defs?.Children.Count > 0)
+        {
+            document.Root.Add(Defs.ToXml(ns, defs));
+            GatherRequiredNamespaces(usedNamespaces, Defs.Children);
+        }
 
-        foreach (SvgElement child in Children)
+        foreach (var usedNamespace in usedNamespaces)
         {
-            document.Root.Add(child.ToXml(ns));
+            document.Root.Add(new XAttribute(XNamespace.Xmlns + usedNamespace.Key, usedNamespace.Value));
         }
 
         return document.ToString();
@@ -106,41 +115,41 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         }
     }
 
-    private void AppendProperties(XElement? root)
+    private void AppendProperties(XElement? root, DefStorage defs)
     {
         if (ViewBox.Unit != null)
         {
-            root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml()));
+            root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml(defs)));
         }
 
         if (Fill.Unit != null)
         {
-            root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml()));
+            root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml(defs)));
         }
 
         if (Stroke.Unit != null)
         {
-            root.Add(new XAttribute("stroke", Stroke.Unit.Value.ToXml()));
+            root.Add(new XAttribute("stroke", Stroke.Unit.Value.ToXml(defs)));
         }
 
         if (StrokeWidth.Unit != null)
         {
-            root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml()));
+            root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml(defs)));
         }
 
         if (Transform.Unit != null)
         {
-            root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
+            root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml(defs)));
         }
 
         if (StrokeLineCap.Unit != null)
         {
-            root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml()));
+            root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml(defs)));
         }
 
         if (StrokeLineJoin.Unit != null)
         {
-            root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml()));
+            root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml(defs)));
         }
     }
 }

+ 5 - 5
src/PixiEditor.SVG/SvgElement.cs

@@ -16,7 +16,7 @@ public class SvgElement(string tagName)
 
     public SvgProperty<SvgStyleUnit> Style { get; } = new("style");
 
-    public XElement ToXml(XNamespace nameSpace)
+    public XElement ToXml(XNamespace nameSpace, DefStorage defs)
     {
         XElement element = new XElement(nameSpace + TagName);
 
@@ -29,18 +29,18 @@ public class SvgElement(string tagName)
                 {
                     if (string.IsNullOrEmpty(prop.SvgName))
                     {
-                        element.Value = prop.Unit.ToXml();
+                        element.Value = prop.Unit.ToXml(defs);
                     }
                     else
                     {
                         if (!string.IsNullOrEmpty(prop.NamespaceName))
                         {
                             XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
-                            element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                            element.Add(new XAttribute(name, prop.Unit.ToXml(defs)));
                         }
                         else
                         {
-                            element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                            element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml(defs)));
                         }
                     }
                 }
@@ -51,7 +51,7 @@ public class SvgElement(string tagName)
         {
             foreach (SvgElement child in container.Children)
             {
-                element.Add(child.ToXml(nameSpace));
+                element.Add(child.ToXml(nameSpace, defs));
             }
         }
 

+ 2 - 1
src/PixiEditor.SVG/SvgParser.cs

@@ -25,7 +25,8 @@ public class SvgParser
         { "linearGradient", typeof(SvgLinearGradient) },
         { "radialGradient", typeof(SvgRadialGradient) },
         { "stop", typeof(SvgStop) },
-        { "defs", typeof(SvgDefs) }
+        { "defs", typeof(SvgDefs) },
+        { "clipPath", typeof(SvgClipPath) }
     };
 
     public string Source { get; set; }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgColorUnit.cs

@@ -53,7 +53,7 @@ public struct SvgColorUnit : ISvgUnit
         return new SvgColorUnit($"hsla({h},{s}%,{l}%,{a})");
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         return Value;
     }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgEnumUnit.cs

@@ -14,7 +14,7 @@ public struct SvgEnumUnit<T> : ISvgUnit where T : struct, Enum
         Value = value;
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         FieldInfo field = Value.GetType().GetField(Value.ToString());
         SvgValueAttribute attribute = field.GetCustomAttribute<SvgValueAttribute>();

+ 1 - 1
src/PixiEditor.SVG/Units/SvgLinkUnit.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.SVG.Units;
 public struct SvgLinkUnit : ISvgUnit
 {
     public string? ObjectReference { get; set; } 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         return ObjectReference != null ? $"url(#{ObjectReference}" : string.Empty;
     }

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

@@ -57,7 +57,7 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
         return new SvgNumericUnit(value, "%");
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         string invariantValue = Value.ToString(CultureInfo.InvariantCulture);
         return $"{invariantValue}{PostFix}";

+ 99 - 3
src/PixiEditor.SVG/Units/SvgPaintServerUnit.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 
 namespace PixiEditor.SVG.Units;
@@ -21,9 +22,14 @@ public struct SvgPaintServerUnit : ISvgUnit
         return new SvgPaintServerUnit(new ColorPaintable(color));
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
-        throw new NotImplementedException();
+        if (LinksTo != null)
+        {
+            return LinksTo.Value.ToXml(defs);
+        }
+
+        return TrySerialize(Paintable, defs);
     }
 
     public void ValuesFromXml(string readerValue, SvgDefs defs)
@@ -40,10 +46,100 @@ public struct SvgPaintServerUnit : ISvgUnit
         }
         else
         {
-            if(defs.TryFindElement(LinksTo.Value.ObjectReference, out SvgElement? element) && element is IPaintServer server)
+            if (defs.TryFindElement(LinksTo.Value.ObjectReference, out SvgElement? element) &&
+                element is IPaintServer server)
             {
                 Paintable = server.GetPaintable();
             }
         }
     }
+
+    private string TrySerialize(Paintable paintable, DefStorage defs)
+    {
+        if (paintable is ColorPaintable colorPaintable)
+        {
+            return colorPaintable.Color.ToRgbHex();
+        }
+
+        if (paintable is GradientPaintable gradientPaintable)
+        {
+            return TrySerializeGradient(gradientPaintable, defs);
+        }
+
+        return "";
+    }
+
+    private string TrySerializeGradient(GradientPaintable gradientPaintable, DefStorage defs)
+    {
+        switch (gradientPaintable)
+        {
+            case LinearGradientPaintable linearGradientPaintable:
+                return CreateLinearGradient(linearGradientPaintable, defs);
+            case RadialGradientPaintable radialGradientPaintable:
+                return CreateRadialGradient(radialGradientPaintable, defs);
+            default:
+                return "";
+        }
+    }
+
+    private string CreateLinearGradient(LinearGradientPaintable linearGradientPaintable, DefStorage defs)
+    {
+        SvgLinearGradient linearGradient = new SvgLinearGradient();
+        linearGradient.Id.Unit = new SvgStringUnit($"linearGradient{defs.GetNextId()}");
+        linearGradient.X1.Unit = new SvgNumericUnit(linearGradientPaintable.Start.X, "");
+        linearGradient.Y1.Unit = new SvgNumericUnit(linearGradientPaintable.Start.Y, "");
+        linearGradient.X2.Unit = new SvgNumericUnit(linearGradientPaintable.End.X, "");
+        linearGradient.Y2.Unit = new SvgNumericUnit(linearGradientPaintable.End.Y, "");
+        if (linearGradientPaintable.AbsoluteValues)
+        {
+            linearGradient.GradientUnits.Unit = new SvgEnumUnit<SvgRelativityUnit>(SvgRelativityUnit.UserSpaceOnUse);
+        }
+
+        if (linearGradientPaintable.Transform is { IsIdentity: false })
+        {
+            linearGradient.GradientTransform.Unit = new SvgTransformUnit(linearGradientPaintable.Transform.Value);
+        }
+
+        foreach (var stop in linearGradientPaintable.GradientStops)
+        {
+            SvgStop svgStop = new SvgStop();
+            svgStop.Offset.Unit = new SvgNumericUnit(stop.Offset * 100, "%");
+            svgStop.StopColor.Unit = new SvgColorUnit(stop.Color.ToRgbHex());
+            svgStop.StopOpacity.Unit = new SvgNumericUnit(stop.Color.A / 255.0, "");
+            linearGradient.Children.Add(svgStop);
+        }
+
+        defs.AddDef(linearGradient);
+        return $"url(#{linearGradient.Id.Unit.Value.Value})";
+    }
+
+    private string CreateRadialGradient(RadialGradientPaintable radialGradientPaintable, DefStorage defs)
+    {
+        SvgRadialGradient radialGradient = new SvgRadialGradient();
+        radialGradient.Id.Unit = new SvgStringUnit($"radialGradient{defs.GetNextId()}");
+        radialGradient.Cx.Unit = new SvgNumericUnit(radialGradientPaintable.Center.X, "");
+        radialGradient.Cy.Unit = new SvgNumericUnit(radialGradientPaintable.Center.Y, "");
+        radialGradient.R.Unit = new SvgNumericUnit(radialGradientPaintable.Radius, "");
+        if (radialGradientPaintable.AbsoluteValues)
+        {
+            radialGradient.GradientUnits.Unit = new SvgEnumUnit<SvgRelativityUnit>(SvgRelativityUnit.UserSpaceOnUse);
+        }
+
+        if (radialGradientPaintable.Transform is { IsIdentity: false })
+        {
+            radialGradient.GradientTransform.Unit = new SvgTransformUnit(radialGradientPaintable.Transform.Value);
+        }
+
+        foreach (var stop in radialGradientPaintable.GradientStops)
+        {
+            SvgStop svgStop = new SvgStop();
+            svgStop.Offset.Unit = new SvgNumericUnit(stop.Offset * 100, "%");
+            svgStop.StopColor.Unit = new SvgColorUnit(stop.Color.ToRgbHex());
+            svgStop.StopOpacity.Unit = new SvgNumericUnit(stop.Color.A / 255.0, "");
+            radialGradient.Children.Add(svgStop);
+        }
+
+        defs.AddDef(radialGradient);
+        return $"url(#{radialGradient.Id.Unit.Value.Value})";
+    }
 }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgRectUnit.cs

@@ -6,7 +6,7 @@ namespace PixiEditor.SVG.Units;
 public struct SvgRectUnit(RectD rect) : ISvgUnit
 {
     public RectD Value { get; set; } = rect;
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         return $"{Value.X} {Value.Y} {Value.Width} {Value.Height}";
     }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgStringUnit.cs

@@ -10,7 +10,7 @@ public struct SvgStringUnit : ISvgUnit
     }
 
     public string Value { get; set; }
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         return Value;
     }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgStyleUnit.cs

@@ -37,7 +37,7 @@ public struct SvgStyleUnit : ISvgUnit
         }
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         return Value;
     }

+ 1 - 1
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -18,7 +18,7 @@ public struct SvgTransformUnit : ISvgUnit
         MatrixValue = matrixValue;
     }
 
-    public string ToXml()
+    public string ToXml(DefStorage defs)
     {
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
         string translateY = MatrixValue.TransY.ToString(CultureInfo.InvariantCulture);

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

@@ -4,6 +4,6 @@ namespace PixiEditor.SVG.Units;
 
 public interface ISvgUnit
 {
-    public string ToXml();
+    public string ToXml(DefStorage defs);
     public void ValuesFromXml(string readerValue, SvgDefs defs);
 }

+ 4 - 1
src/PixiEditor/Models/Files/PixiFileType.cs

@@ -2,6 +2,8 @@
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
+using PixiEditor.Helpers;
+using PixiEditor.Models.ExceptionHandling;
 using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Models.Files;
@@ -32,8 +34,9 @@ internal class PixiFileType : IoFileType
         {
             return SaveResult.IoError;
         }
-        catch
+        catch (Exception e)
         {
+            CrashHelper.SendExceptionInfo(e);
             return SaveResult.UnknownError;
         }
 

+ 79 - 0
src/PixiEditor/Models/Serialization/Factories/Paintables/GradientPaintableSerializationFactory.cs

@@ -0,0 +1,79 @@
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories.Paintables;
+
+internal abstract class GradientPaintableSerializationFactory<T> : SerializationFactory<byte[], T>,
+    IPaintableSerializationFactory
+    where T : GradientPaintable
+{
+    public override byte[] Serialize(T original)
+    {
+        ByteBuilder builder = new();
+        Serialize(original, builder);
+
+        return builder.Build();
+    }
+
+    public override bool TryDeserialize(object serialized, out T original,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        if (serialized is not byte[] bytes)
+        {
+            original = null!;
+            return false;
+        }
+
+        ByteExtractor extractor = new(bytes);
+        original = TryDeserialize(extractor) as T;
+
+        return true;
+    }
+
+    public Paintable TryDeserialize(ByteExtractor extractor) => TryDeserializeGradient(extractor);
+    public void Serialize(Paintable paintable, ByteBuilder builder) => SerializeGradient((T)paintable, builder);
+
+    protected void SerializeGradient(T paintable, ByteBuilder builder)
+    {
+        builder.AddBool(paintable.AbsoluteValues);
+        bool hasTransform = paintable.Transform.HasValue && paintable.Transform.Value != Matrix3X3.Identity;
+        builder.AddBool(hasTransform);
+        if (hasTransform)
+        {
+            builder.AddMatrix3X3(paintable.Transform.Value);
+        }
+
+        builder.AddInt(paintable.GradientStops?.Count ?? 0);
+        foreach (var stop in paintable.GradientStops)
+        {
+            builder.AddColor(stop.Color);
+            builder.AddDouble(stop.Offset);
+        }
+
+        SerializeSpecificGradient(paintable, builder);
+    }
+
+    protected T TryDeserializeGradient(ByteExtractor extractor)
+    {
+        bool absoluteValues = extractor.GetBool();
+        Matrix3X3? transform = null;
+        if (extractor.GetBool())
+        {
+            transform = extractor.GetMatrix3X3();
+        }
+
+        int stopsCount = extractor.GetInt();
+        List<GradientStop> stops = new();
+        for (int i = 0; i < stopsCount; i++)
+        {
+            stops.Add(new GradientStop(extractor.GetColor(), extractor.GetDouble()));
+        }
+
+        T paintable = DeserializeGradient(absoluteValues, transform, stops, extractor);
+        return paintable;
+    }
+
+    protected abstract void SerializeSpecificGradient(T paintable, ByteBuilder builder);
+    protected abstract T DeserializeGradient(bool absoluteValues, Matrix3X3? transform, List<GradientStop> stops,
+        ByteExtractor extractor);
+}

+ 26 - 0
src/PixiEditor/Models/Serialization/Factories/Paintables/LinearGradientSerializationFactory.cs

@@ -0,0 +1,26 @@
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories.Paintables;
+
+internal class LinearGradientSerializationFactory : GradientPaintableSerializationFactory<LinearGradientPaintable>
+{
+    public override string DeserializationId { get; } = "PixiEditor.LinearGradientPaintable";
+
+
+    protected override void SerializeSpecificGradient(LinearGradientPaintable paintable, ByteBuilder builder)
+    {
+        builder.AddVecD(paintable.Start);
+        builder.AddVecD(paintable.End);
+    }
+
+    protected override LinearGradientPaintable DeserializeGradient(bool absoluteValues, Matrix3X3? transform,
+        List<GradientStop> stops, ByteExtractor extractor)
+    {
+        VecD start = extractor.GetVecD();
+        VecD end = extractor.GetVecD();
+
+        return new LinearGradientPaintable(start, end, stops) { AbsoluteValues = absoluteValues, Transform = transform };
+    }
+}

+ 24 - 0
src/PixiEditor/Models/Serialization/Factories/Paintables/RadialGradientSerializationFactory.cs

@@ -0,0 +1,24 @@
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories.Paintables;
+
+internal class RadialGradientSerializationFactory : GradientPaintableSerializationFactory<RadialGradientPaintable>
+{
+    public override string DeserializationId { get; } = "PixiEditor.RadialGradientPaintable";
+    protected override void SerializeSpecificGradient(RadialGradientPaintable paintable, ByteBuilder builder)
+    {
+        builder.AddVecD(paintable.Center);
+        builder.AddDouble(paintable.Radius);
+    }
+
+    protected override RadialGradientPaintable DeserializeGradient(bool absoluteValues, Matrix3X3? transform, List<GradientStop> stops,
+        ByteExtractor extractor)
+    {
+        VecD center = extractor.GetVecD();
+        double radius = extractor.GetDouble();
+
+        return new RadialGradientPaintable(center, radius, stops) { AbsoluteValues = absoluteValues, Transform = transform };
+    }
+}

+ 22 - 0
src/PixiEditor/Models/Serialization/Factories/Paintables/SweepGradientSerializationFactory.cs

@@ -0,0 +1,22 @@
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories.Paintables;
+
+internal class SweepGradientSerializationFactory : GradientPaintableSerializationFactory<SweepGradientPaintable>
+{
+    public override string DeserializationId { get; } = "PixiEditor.SweepGradientPaintable";
+    protected override void SerializeSpecificGradient(SweepGradientPaintable paintable, ByteBuilder builder)
+    {
+        builder.AddVecD(paintable.Center);
+    }
+
+    protected override SweepGradientPaintable DeserializeGradient(bool absoluteValues, Matrix3X3? transform, List<GradientStop> stops,
+        ByteExtractor extractor)
+    {
+        VecD center = extractor.GetVecD();
+
+        return new SweepGradientPaintable(center, stops) { AbsoluteValues = absoluteValues, Transform = transform };
+    }
+}

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

@@ -9,7 +9,7 @@ namespace PixiEditor.Models.Serialization.Factories;
 
 public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<byte[], T> where T : ShapeVectorData
 {
-    private static List<SerializationFactory> paintableFactories;
+    private static List<SerializationFactory>? paintableFactories = null;
     private static List<SerializationFactory> PaintableFactories => paintableFactories ??= GatherPaintableFactories();
 
     private static List<SerializationFactory> GatherPaintableFactories()
@@ -20,7 +20,7 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
 
         foreach (Type type in types)
         {
-            if (type.IsSubclassOf(typeof(IPaintableSerializationFactory)))
+            if (type.IsAssignableTo(typeof(IPaintableSerializationFactory)) && type is { IsAbstract: false, IsInterface: false })
             {
                 factories.Add((SerializationFactory)Activator.CreateInstance(type));
             }

+ 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
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.60")]
-[assembly: AssemblyFileVersion("2.0.0.60")]
+[assembly: AssemblyVersion("2.0.0.61")]
+[assembly: AssemblyFileVersion("2.0.0.61")]

+ 14 - 0
tests/PixiEditor.Tests/SerializationTests.cs

@@ -1,5 +1,8 @@
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Skia;
+using DrawiEngine;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Models.Serialization;
@@ -11,6 +14,17 @@ namespace PixiEditor.Tests;
 
 public class SerializationTests
 {
+    public SerializationTests()
+    {
+        if (DrawingBackendApi.HasBackend)
+        {
+            return;
+        }
+
+        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
+    }
+
     [Fact]
     public void TestThatAllPaintablesHaveFactories()
     {

+ 1 - 1
tests/PixiEditor.Tests/SvgTests.cs

@@ -225,7 +225,7 @@ public class SvgTests
             writer.Write($"<svg><{element.TagName}/></svg>");
             using XmlReader reader = XmlReader.Create(stream);
             
-            element.ParseData(reader);
+            element.ParseData(reader, new SvgDefs());
         }
     }
 

+ 14 - 0
tests/PixiEditorTests.sln

@@ -113,6 +113,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia.Ope
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi.OpenGl", "..\src\Drawie\src\Drawie.RenderApi.OpenGl\Drawie.RenderApi.OpenGl.csproj", "{924CA5E4-F579-435F-B39A-11802F9B1390}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.AvaloniaUI", "..\src\ColorPicker\src\ColorPicker.AvaloniaUI\ColorPicker.AvaloniaUI.csproj", "{1B9B2155-9D9C-4259-8015-4F06F944812C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.Models", "..\src\ColorPicker\src\ColorPicker.Models\ColorPicker.Models.csproj", "{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -322,6 +326,14 @@ Global
 		{924CA5E4-F579-435F-B39A-11802F9B1390}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{924CA5E4-F579-435F-B39A-11802F9B1390}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{924CA5E4-F579-435F-B39A-11802F9B1390}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1B9B2155-9D9C-4259-8015-4F06F944812C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1B9B2155-9D9C-4259-8015-4F06F944812C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1B9B2155-9D9C-4259-8015-4F06F944812C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1B9B2155-9D9C-4259-8015-4F06F944812C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{0EF3CAB9-7361-472C-8789-D17D4EA2DEBB} = {D914C08C-5F1A-4E13-AAA6-F25E8C9748E2}
@@ -376,5 +388,7 @@ Global
 		{D00D836B-6EAD-4E86-8F6A-A0FE10CC8A8C} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{404E524C-A719-4B3F-981E-98A0EC33E3F2} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{924CA5E4-F579-435F-B39A-11802F9B1390} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{1B9B2155-9D9C-4259-8015-4F06F944812C} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 	EndGlobalSection
 EndGlobal