Browse Source

Initial parser

flabbet 7 months ago
parent
commit
918d7fefcf

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 5e6f8e7762ea56e299bf6ab1a880996a7b856fef
+Subproject commit 0b3b4813ee0002ae7655788c2cc430ca1b58ebd4

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

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

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

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

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

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

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

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

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

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

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

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

+ 4 - 0
src/PixiEditor.SVG/Elements/SvgPath.cs

@@ -5,4 +5,8 @@ namespace PixiEditor.SVG.Elements;
 public class SvgPath() : SvgPrimitive("path")
 {
     public SvgProperty<SvgStringUnit> PathData { get; } = new("d");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return PathData;
+    }
 }

+ 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");
-}

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

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

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

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

+ 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)
+    {
+    }
+}

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

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

+ 6 - 0
src/PixiEditor.SVG/SvgDocument.cs

@@ -40,6 +40,12 @@ public class SvgDocument(RectD viewBox) : IElementContainer
         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)
     {
         foreach (SvgElement child in elements)

+ 60 - 2
src/PixiEditor.SVG/SvgElement.cs

@@ -1,4 +1,6 @@
 using System.Text;
+using System.Xml;
+using PixiEditor.SVG.Exceptions;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
@@ -10,6 +12,7 @@ public class SvgElement(string tagName)
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public string TagName { get; } = tagName;
 
+
     public string ToXml()
     {
         StringBuilder builder = new();
@@ -29,7 +32,7 @@ public class SvgElement(string tagName)
                 }
             }
         }
-        
+
         if (this is not IElementContainer container)
         {
             builder.Append(" />");
@@ -41,10 +44,65 @@ public class SvgElement(string tagName)
             {
                 builder.AppendLine(child.ToXml());
             }
-            
+
             builder.Append($"</{TagName}>");
         }
 
         return builder.ToString();
     }
+
+    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
+        {
+            SvgProperty matchingProperty = properties.FirstOrDefault(x =>
+                string.Equals(x.SvgName, reader.Name, StringComparison.OrdinalIgnoreCase));
+            if (matchingProperty != null)
+            {
+                ParseAttribute(matchingProperty, reader);
+            }
+        } 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 unit;
+    }
 }

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

@@ -0,0 +1,169 @@
+using System.Xml;
+using System.Xml.Linq;
+using Drawie.Numerics;
+using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Features;
+
+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) },
+    };
+
+    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;
+        }
+
+        RectD bounds = ParseBounds(reader);
+        SvgDocument svgDocument = new(bounds);
+
+        while (reader.Read())
+        {
+            if (reader.NodeType == XmlNodeType.Element)
+            {
+                SvgElement? element = ParseElement(reader);
+                if (element != null)
+                {
+                    svgDocument.Children.Add(element);
+
+                    if (element is IElementContainer container)
+                    {
+                        ParseChildren(reader, container, element.TagName);
+                    }
+                }
+            }
+        }
+
+        return svgDocument;
+    }
+
+    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);
+    }
+}

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

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

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

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

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

@@ -2,7 +2,7 @@
 
 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; }
 
@@ -15,4 +15,12 @@ public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
     {
         return Value.ToString().ToKebabCase();
     }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        if (Enum.TryParse(readerValue.FromKebabToTitleCase(), out T result))
+        {
+            Value = result;
+        }
+    }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,132 @@
+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))
+                    {
+                        colorValues[i - 1] = (byte)(alpha * 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
+        {
+            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();
+    }
+}

+ 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.X);
+        Assert.Equal(y, document.ViewBox.Y);
+        Assert.Equal(width, document.ViewBox.Width);
+        Assert.Equal(height, document.ViewBox.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%, 1)")]
+    [InlineData("rgba(255, 0, 0, 1)")]
+    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();
+        }
+    }
+}