diff --git a/Scratch/source.svg b/Scratch/source.svg
index 053a59f..335f8af 100644
--- a/Scratch/source.svg
+++ b/Scratch/source.svg
@@ -1,6 +1,10 @@
\ No newline at end of file
diff --git a/src/SixLabors.Svg/RenderTree/Extensions.cs b/src/SixLabors.Svg/RenderTree/Extensions.cs
new file mode 100644
index 0000000..dff32ae
--- /dev/null
+++ b/src/SixLabors.Svg/RenderTree/Extensions.cs
@@ -0,0 +1,86 @@
+using AngleSharp.Svg.Dom;
+using SixLabors.ImageSharp;
+using SixLabors.Shapes;
+using AngleSharp.Dom;
+using System.Linq;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.Svg.Dom
+{
+ internal static class Extensions
+ {
+ public static IPath GenerateStroke(this IPath path, Image img, ISvgShape shape) where TPixel : struct, IPixel
+ {
+ var strokeWidth = shape.StrokeWidth.AsPixelXAxis(img);
+ if (strokeWidth == 0)
+ {
+ return null;
+ }
+
+ return Outliner.GenerateOutline(path, strokeWidth, shape.StrokeLineJoin.Style, shape.StrokeLineCap.Style);
+ }
+
+ public static string GetAttributeValueSelfOrGroup(this ISvgElement element, string attribute)
+ {
+ var val = element.Attributes[attribute]?.Value;
+
+ if (val == null)
+ {
+ val = element.Ancestors().Where(x => x.TagName == "g").FirstOrDefault()?.GetAttributeValueSelfOrGroup(attribute);
+ }
+
+ return val;
+ }
+
+ public static SvgPaint GetPaint(this ISvgElement element, string attribute, string defaultPaintValue, string defaultOpacityValue)
+ {
+ var paint = element.GetAttributeValueSelfOrGroup(attribute) ?? defaultPaintValue;
+ var opacity = element.GetAttributeValueSelfOrGroup(attribute + "-opacity") ?? defaultOpacityValue;
+
+ return SvgPaint.Parse(paint, opacity);
+ }
+
+ public static SvgUnitValue GetUnitValue(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.TryGetUnitValue(attribute, defaultValue);
+
+ return val ?? SvgUnitValue.Unset;
+ }
+ public static SvgUnitValue? TryGetUnitValue(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.GetAttributeValueSelfOrGroup(attribute) ?? defaultValue;
+
+ return SvgUnitValue.Parse(val);
+ }
+
+ public static SvgLineCap GetLineCap(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.TryGetLineCap(attribute, defaultValue);
+
+ return val ?? SvgLineCap.Unset;
+ }
+
+ public static SvgLineCap? TryGetLineCap(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.GetAttributeValueSelfOrGroup(attribute) ?? defaultValue;
+
+ return SvgLineCap.Parse(val);
+ }
+
+
+ public static SvgLineJoin GetLineJoin(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.TryGetLineJoin(attribute, defaultValue);
+
+ return val ?? SvgLineJoin.Unset;
+ }
+
+ public static SvgLineJoin? TryGetLineJoin(this ISvgElement element, string attribute, string defaultValue = null)
+ {
+ var val = element.GetAttributeValueSelfOrGroup(attribute) ?? defaultValue;
+
+ return SvgLineJoin.Parse(val);
+ }
+
+ }
+}
diff --git a/src/SixLabors.Svg/RenderTree/ISvgShape.cs b/src/SixLabors.Svg/RenderTree/ISvgShape.cs
new file mode 100644
index 0000000..882a345
--- /dev/null
+++ b/src/SixLabors.Svg/RenderTree/ISvgShape.cs
@@ -0,0 +1,11 @@
+namespace SixLabors.Svg.Dom
+{
+ internal interface ISvgShape
+ {
+ SvgPaint Fill { get; }
+ SvgPaint Stroke { get; }
+ SvgUnitValue StrokeWidth { get; }
+ SvgLineCap StrokeLineCap { get; }
+ SvgLineJoin StrokeLineJoin { get; }
+ }
+}
diff --git a/src/SixLabors.Svg/RenderTree/SvgDocument.cs b/src/SixLabors.Svg/RenderTree/SvgDocument.cs
index 10df19e..f8cfc4b 100644
--- a/src/SixLabors.Svg/RenderTree/SvgDocument.cs
+++ b/src/SixLabors.Svg/RenderTree/SvgDocument.cs
@@ -30,10 +30,10 @@ namespace SixLabors.Svg.Dom
{
var document = new SvgDocument()
{
- X = SvgUnitValue.Parse(element.Attributes["x"]?.Value),
- Y = SvgUnitValue.Parse(element.Attributes["y"]?.Value),
- Width = SvgUnitValue.Parse(element.Attributes["width"]?.Value),
- Height = SvgUnitValue.Parse(element.Attributes["height"]?.Value),
+ X = element.GetUnitValue("x", "0"),
+ Y = element.GetUnitValue("y", "0"),
+ Width = element.GetUnitValue("width"),
+ Height = element.GetUnitValue("height")
};
var children = element.Children.OfType();
diff --git a/src/SixLabors.Svg/RenderTree/SvgElement.cs b/src/SixLabors.Svg/RenderTree/SvgElement.cs
index 82fec7c..d33ea25 100644
--- a/src/SixLabors.Svg/RenderTree/SvgElement.cs
+++ b/src/SixLabors.Svg/RenderTree/SvgElement.cs
@@ -21,6 +21,11 @@ namespace SixLabors.Svg.Dom
case "circle":
case "ellipse":
return await SvgEllipse.LoadAsync(element);
+ case "line":
+ case "polyline":
+ case "polygon":
+ case "path":
+ return await SvgPath.LoadAsync(element);
default:
return null;
}
diff --git a/src/SixLabors.Svg/RenderTree/SvgEllipse.cs b/src/SixLabors.Svg/RenderTree/SvgEllipse.cs
index eaa5cc0..ef73a97 100644
--- a/src/SixLabors.Svg/RenderTree/SvgEllipse.cs
+++ b/src/SixLabors.Svg/RenderTree/SvgEllipse.cs
@@ -3,11 +3,10 @@ using System.Threading.Tasks;
using AngleSharp.Svg.Dom;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
-using SixLabors.Shapes;
namespace SixLabors.Svg.Dom
{
- internal sealed class SvgEllipse : SvgElement
+ internal sealed class SvgEllipse : SvgElement, ISvgShape
{
public SvgUnitValue X { get; private set; }
@@ -17,25 +16,30 @@ namespace SixLabors.Svg.Dom
public SvgPaint Fill { get; private set; }
public SvgPaint Stroke { get; private set; }
public SvgUnitValue StrokeWidth { get; private set; }
+ public SvgLineCap StrokeLineCap { get; private set; }
+ public SvgLineJoin StrokeLineJoin { get; private set; }
public static Task LoadAsync(ISvgElement element)
{
var ellipse = new SvgEllipse()
{
- Fill = SvgPaint.Parse(element.Attributes["fill"]?.Value ?? "Black", element.Attributes["fill-opacity"]?.Value ?? "1"),
- Stroke = SvgPaint.Parse(element.Attributes["stroke"]?.Value ?? "None", element.Attributes["stroke-opacity"]?.Value ?? "1"),
- StrokeWidth = SvgUnitValue.Parse(element.Attributes["stroke-width"]?.Value ?? "1"),
- X = SvgUnitValue.Parse(element.Attributes["cx"]?.Value),
- Y = SvgUnitValue.Parse(element.Attributes["cy"]?.Value)
+ Fill = element.GetPaint("fill", "Black", "1"),
+ Stroke = element.GetPaint("stroke", "None", "1"),
+ StrokeWidth = element.GetUnitValue("stroke-width", "1"),
+ StrokeLineCap = element.GetLineCap("stroke-linecap", "butt"),
+ StrokeLineJoin = element.GetLineJoin("stroke-linejoin", "miter"),
+ X = element.GetUnitValue("cx"),
+ Y = element.GetUnitValue("cy"),
};
+
if (element.TagName == "circle")
{
- ellipse.RadiusY = ellipse.RadiusX = SvgUnitValue.Parse(element.Attributes["r"]?.Value ?? "0");
+ ellipse.RadiusY = ellipse.RadiusX = element.GetUnitValue("r", "0");
}
else
{
- ellipse.RadiusY = SvgUnitValue.Parse(element.Attributes["ry"]?.Value ?? "0");
- ellipse.RadiusX = SvgUnitValue.Parse(element.Attributes["rx"]?.Value ?? "0");
+ ellipse.RadiusX = element.GetUnitValue("rx", "0");
+ ellipse.RadiusY = element.GetUnitValue("ry", "0");
}
return Task.FromResult(ellipse);
@@ -47,17 +51,19 @@ namespace SixLabors.Svg.Dom
var fillBrush = Fill.AsBrush();
var strokeBrush = Stroke.AsBrush();
- var strokeWidth = this.StrokeWidth.AsPixelXAxis(image);
image.Mutate(x =>
{
if (fillBrush != null)
{
x = x.Fill(fillBrush, rect);
}
- if (strokeBrush != null && strokeWidth > 0)
+ if (strokeBrush != null)
{
- var outline = Outliner.GenerateOutline(rect, strokeWidth);
- x = x.Fill(strokeBrush, outline);
+ var outline = rect.GenerateStroke(image, this);
+ if (outline != null)
+ {
+ x = x.Fill(strokeBrush, outline);
+ }
}
});
}
diff --git a/src/SixLabors.Svg/RenderTree/SvgPaint.cs b/src/SixLabors.Svg/RenderTree/SvgPaint.cs
new file mode 100644
index 0000000..c733ecc
--- /dev/null
+++ b/src/SixLabors.Svg/RenderTree/SvgPaint.cs
@@ -0,0 +1,231 @@
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Collections.Generic;
+
+namespace SixLabors.Svg.Dom
+{
+ internal struct SvgPaint
+ {
+ public static readonly SvgPaint Unset = default(SvgPaint);
+
+ internal SvgPaint(string value, float opacity)
+ {
+ Value = value;
+ Opacity = opacity;
+ }
+
+ public string Value { get; }
+
+ public float Opacity { get; }
+
+ public static SvgPaint Parse(string val, string opacity)
+ {
+ val = val?.Trim() ?? "";
+ opacity = opacity?.Trim() ?? "";
+
+ float opacityVal = 1;
+ if (float.TryParse(opacity, out float op))
+ {
+ opacityVal = op;
+ if (opacityVal < 0)
+ {
+ opacityVal = 0;
+ }
+ if (opacityVal > 1)
+ {
+ opacityVal = 1;
+ }
+ }
+
+ return new SvgPaint(val, opacityVal);
+
+ }
+
+ private TPixel? GetNamedColor(string name) where TPixel : struct, IPixel
+ {
+ var dict = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["AliceBlue"] = NamedColors.AliceBlue,
+ ["MistyRose"] = NamedColors.MistyRose,
+ ["Moccasin"] = NamedColors.Moccasin,
+ ["NavajoWhite"] = NamedColors.NavajoWhite,
+ ["Navy"] = NamedColors.Navy,
+ ["OldLace"] = NamedColors.OldLace,
+ ["Olive"] = NamedColors.Olive,
+ ["MintCream"] = NamedColors.MintCream,
+ ["OliveDrab"] = NamedColors.OliveDrab,
+ ["OrangeRed"] = NamedColors.OrangeRed,
+ ["Orchid"] = NamedColors.Orchid,
+ ["PaleGoldenrod"] = NamedColors.PaleGoldenrod,
+ ["PaleGreen"] = NamedColors.PaleGreen,
+ ["PaleTurquoise"] = NamedColors.PaleTurquoise,
+ ["PaleVioletRed"] = NamedColors.PaleVioletRed,
+ ["Orange"] = NamedColors.Orange,
+ ["PapayaWhip"] = NamedColors.PapayaWhip,
+ ["MidnightBlue"] = NamedColors.MidnightBlue,
+ ["MediumTurquoise"] = NamedColors.MediumTurquoise,
+ ["LightSteelBlue"] = NamedColors.LightSteelBlue,
+ ["LightYellow"] = NamedColors.LightYellow,
+ ["Lime"] = NamedColors.Lime,
+ ["LimeGreen"] = NamedColors.LimeGreen,
+ ["Linen"] = NamedColors.Linen,
+ ["Magenta"] = NamedColors.Magenta,
+ ["MediumVioletRed"] = NamedColors.MediumVioletRed,
+ ["Maroon"] = NamedColors.Maroon,
+ ["MediumBlue"] = NamedColors.MediumBlue,
+ ["MediumOrchid"] = NamedColors.MediumOrchid,
+ ["MediumPurple"] = NamedColors.MediumPurple,
+ ["MediumSeaGreen"] = NamedColors.MediumSeaGreen,
+ ["MediumSlateBlue"] = NamedColors.MediumSlateBlue,
+ ["MediumSpringGreen"] = NamedColors.MediumSpringGreen,
+ ["MediumAquamarine"] = NamedColors.MediumAquamarine,
+ ["LightSlateGray"] = NamedColors.LightSlateGray,
+ ["PeachPuff"] = NamedColors.PeachPuff,
+ ["Pink"] = NamedColors.Pink,
+ ["SpringGreen"] = NamedColors.SpringGreen,
+ ["SteelBlue"] = NamedColors.SteelBlue,
+ ["Tan"] = NamedColors.Tan,
+ ["Teal"] = NamedColors.Teal,
+ ["Thistle"] = NamedColors.Thistle,
+ ["Tomato"] = NamedColors.Tomato,
+ ["Snow"] = NamedColors.Snow,
+ ["Transparent"] = NamedColors.Transparent,
+ ["Violet"] = NamedColors.Violet,
+ ["Wheat"] = NamedColors.Wheat,
+ ["White"] = NamedColors.White,
+ ["WhiteSmoke"] = NamedColors.WhiteSmoke,
+ ["Yellow"] = NamedColors.Yellow,
+ ["YellowGreen"] = NamedColors.YellowGreen,
+ ["Turquoise"] = NamedColors.Turquoise,
+ ["Peru"] = NamedColors.Peru,
+ ["SlateGray"] = NamedColors.SlateGray,
+ ["SkyBlue"] = NamedColors.SkyBlue,
+ ["Plum"] = NamedColors.Plum,
+ ["PowderBlue"] = NamedColors.PowderBlue,
+ ["Purple"] = NamedColors.Purple,
+ ["RebeccaPurple"] = NamedColors.RebeccaPurple,
+ ["Red"] = NamedColors.Red,
+ ["RosyBrown"] = NamedColors.RosyBrown,
+ ["SlateBlue"] = NamedColors.SlateBlue,
+ ["RoyalBlue"] = NamedColors.RoyalBlue,
+ ["Salmon"] = NamedColors.Salmon,
+ ["SandyBrown"] = NamedColors.SandyBrown,
+ ["SeaGreen"] = NamedColors.SeaGreen,
+ ["SeaShell"] = NamedColors.SeaShell,
+ ["Sienna"] = NamedColors.Sienna,
+ ["Silver"] = NamedColors.Silver,
+ ["SaddleBrown"] = NamedColors.SaddleBrown,
+ ["LightSkyBlue"] = NamedColors.LightSkyBlue,
+ ["LightSeaGreen"] = NamedColors.LightSeaGreen,
+ ["LightSalmon"] = NamedColors.LightSalmon,
+ ["Crimson"] = NamedColors.Crimson,
+ ["Cyan"] = NamedColors.Cyan,
+ ["DarkBlue"] = NamedColors.DarkBlue,
+ ["DarkCyan"] = NamedColors.DarkCyan,
+ ["DarkGoldenrod"] = NamedColors.DarkGoldenrod,
+ ["DarkGray"] = NamedColors.DarkGray,
+ ["Cornsilk"] = NamedColors.Cornsilk,
+ ["DarkGreen"] = NamedColors.DarkGreen,
+ ["DarkMagenta"] = NamedColors.DarkMagenta,
+ ["DarkOliveGreen"] = NamedColors.DarkOliveGreen,
+ ["DarkOrange"] = NamedColors.DarkOrange,
+ ["DarkOrchid"] = NamedColors.DarkOrchid,
+ ["DarkRed"] = NamedColors.DarkRed,
+ ["DarkSalmon"] = NamedColors.DarkSalmon,
+ ["DarkKhaki"] = NamedColors.DarkKhaki,
+ ["DarkSeaGreen"] = NamedColors.DarkSeaGreen,
+ ["CornflowerBlue"] = NamedColors.CornflowerBlue,
+ ["Chocolate"] = NamedColors.Chocolate,
+ ["AntiqueWhite"] = NamedColors.AntiqueWhite,
+ ["Aqua"] = NamedColors.Aqua,
+ ["Aquamarine"] = NamedColors.Aquamarine,
+ ["Azure"] = NamedColors.Azure,
+ ["Beige"] = NamedColors.Beige,
+ ["Bisque"] = NamedColors.Bisque,
+ ["Coral"] = NamedColors.Coral,
+ ["Black"] = NamedColors.Black,
+ ["Blue"] = NamedColors.Blue,
+ ["BlueViolet"] = NamedColors.BlueViolet,
+ ["Brown"] = NamedColors.Brown,
+ ["BurlyWood"] = NamedColors.BurlyWood,
+ ["CadetBlue"] = NamedColors.CadetBlue,
+ ["Chartreuse"] = NamedColors.Chartreuse,
+ ["BlanchedAlmond"] = NamedColors.BlanchedAlmond,
+ ["DarkSlateBlue"] = NamedColors.DarkSlateBlue,
+ ["DarkSlateGray"] = NamedColors.DarkSlateGray,
+ ["DarkTurquoise"] = NamedColors.DarkTurquoise,
+ ["Indigo"] = NamedColors.Indigo,
+ ["Ivory"] = NamedColors.Ivory,
+ ["Khaki"] = NamedColors.Khaki,
+ ["Lavender"] = NamedColors.Lavender,
+ ["LavenderBlush"] = NamedColors.LavenderBlush,
+ ["LawnGreen"] = NamedColors.LawnGreen,
+ ["IndianRed"] = NamedColors.IndianRed,
+ ["LemonChiffon"] = NamedColors.LemonChiffon,
+ ["LightCoral"] = NamedColors.LightCoral,
+ ["LightCyan"] = NamedColors.LightCyan,
+ ["LightGoldenrodYellow"] = NamedColors.LightGoldenrodYellow,
+ ["LightGray"] = NamedColors.LightGray,
+ ["LightGreen"] = NamedColors.LightGreen,
+ ["LightPink"] = NamedColors.LightPink,
+ ["LightBlue"] = NamedColors.LightBlue,
+ ["HotPink"] = NamedColors.HotPink,
+ ["Honeydew"] = NamedColors.Honeydew,
+ ["GreenYellow"] = NamedColors.GreenYellow,
+ ["DarkViolet"] = NamedColors.DarkViolet,
+ ["DeepPink"] = NamedColors.DeepPink,
+ ["DeepSkyBlue"] = NamedColors.DeepSkyBlue,
+ ["DimGray"] = NamedColors.DimGray,
+ ["DodgerBlue"] = NamedColors.DodgerBlue,
+ ["Firebrick"] = NamedColors.Firebrick,
+ ["FloralWhite"] = NamedColors.FloralWhite,
+ ["ForestGreen"] = NamedColors.ForestGreen,
+ ["Fuchsia"] = NamedColors.Fuchsia,
+ ["Gainsboro"] = NamedColors.Gainsboro,
+ ["GhostWhite"] = NamedColors.GhostWhite,
+ ["Gold"] = NamedColors.Gold,
+ ["Goldenrod"] = NamedColors.Goldenrod,
+ ["Gray"] = NamedColors.Gray,
+ ["Green"] = NamedColors.Green,
+ };
+
+ if (dict.TryGetValue(name, out var pixel))
+ {
+ return pixel;
+ }
+ return null;
+ }
+ public IBrush AsBrush() where TPixel : struct, IPixel
+ {
+ // lets asume the brush is a color for now
+ // TODO update ColourBuilder to expose named colors
+ var value = this.Value;
+ if (string.IsNullOrWhiteSpace(value) || value.Equals("None", StringComparison.OrdinalIgnoreCase))
+ {
+ // if a null prush is returned we should skip
+ return null;
+ }
+
+
+ var color = GetNamedColor(Value) ?? ColorBuilder.FromHex(value);
+
+
+ if(Opacity == 0)
+ {
+ // no-op push here to efficienty sake
+ color = NamedColors.Transparent;
+ } else if (Opacity != 1)
+ {
+ var vector = color.ToScaledVector4();
+ vector.W *= Opacity;
+ color.FromScaledVector4(vector);
+ }
+
+ return new SolidBrush(color);
+
+ }
+ }
+
+
+}
diff --git a/src/SixLabors.Svg/RenderTree/SvgPath.cs b/src/SixLabors.Svg/RenderTree/SvgPath.cs
new file mode 100644
index 0000000..4f85916
--- /dev/null
+++ b/src/SixLabors.Svg/RenderTree/SvgPath.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AngleSharp.Svg.Dom;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.Shapes;
+
+namespace SixLabors.Svg.Dom
+{
+ internal sealed class SvgPath : SvgElement, ISvgShape
+ {
+ public IPath Path { get; private set; }
+
+ public SvgPaint Fill { get; private set; }
+ public SvgPaint Stroke { get; private set; }
+ public SvgUnitValue StrokeWidth { get; private set; }
+ public SvgLineCap StrokeLineCap { get; private set; }
+ public SvgLineJoin StrokeLineJoin { get; private set; }
+
+ public IEnumerable PathOperations { get; private set; }
+ public static Task LoadAsync(ISvgElement element)
+ {
+ var path = new SvgPath()
+ {
+ Fill = element.GetPaint("fill", "Black", "1"),
+ Stroke = element.GetPaint("stroke", "None", "1"),
+ StrokeWidth = element.GetUnitValue("stroke-width", "1"),
+ StrokeLineCap = element.GetLineCap("stroke-linecap", "butt"),
+ StrokeLineJoin = element.GetLineJoin("stroke-linejoin", "miter"),
+ };
+
+ if (element.TagName == "line")
+ {
+ path.PathOperations = new[] {
+ SvgPathOperation.MoveTo(element.GetUnitValue("x1", "0"), element.GetUnitValue("y1", "0"), false),
+ SvgPathOperation.LineTo(element.GetUnitValue("x2", "0"), element.GetUnitValue("y2", "0"), false),
+ };
+ }
+ else if (element.TagName == "polyline" || element.TagName == "polygon")
+ {
+ var pathData = element.GetAttributeValueSelfOrGroup("points");
+ path.PathOperations = ParsePoints(pathData, element.TagName == "polygon");
+ }
+ else if (element.TagName == "path")
+ {
+ var pathData = element.GetAttributeValueSelfOrGroup("d");
+ path.PathOperations = SvgPathOperation.Parse(pathData);
+ }
+
+ if (path.PathOperations == null)
+ {
+ return Task.FromResult(null);
+ }
+
+ return Task.FromResult(path);
+ }
+
+ private static IEnumerable ParsePoints(string pathData, bool closePath)
+ {
+ if (pathData != null && !pathData.Equals("none", System.StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = pathData.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x =>
+ {
+ if (int.TryParse(x, out var i))
+ {
+ return i;
+ }
+ else
+ { return (int?)null; }
+ }).Where(x => x != null).Select(x => x.Value).ToArray();
+ if (parts.Length > 4)
+ {
+ var len = parts.Length / 2;
+ if (closePath)
+ {
+ len++;
+ }
+
+ var ops = new SvgPathOperation[len];
+ ops[0] = SvgPathOperation.MoveTo(new SvgUnitValue(parts[0], SvgUnitValue.Units.undefined), new SvgUnitValue(parts[1], SvgUnitValue.Units.undefined), false);
+
+ for (var i = 2; i < parts.Length; i += 2)
+ {
+ ops[i / 2] = SvgPathOperation.LineTo(new SvgUnitValue(parts[i], SvgUnitValue.Units.undefined), new SvgUnitValue(parts[i + 1], SvgUnitValue.Units.undefined), false);
+ }
+
+ if (closePath)
+ {
+ ops[len - 1] = SvgPathOperation.ClosePath();
+ }
+ return ops;
+ }
+ }
+
+ return null;
+ }
+
+ internal override void RenderTo(Image image)
+ {
+ var rect = this.PathOperations.GeneratePath(image);
+
+ var fillBrush = Fill.AsBrush();
+ var strokeBrush = Stroke.AsBrush();
+ image.Mutate(x =>
+ {
+ if (fillBrush != null)
+ {
+ x = x.Fill(fillBrush, rect);
+ }
+ if (strokeBrush != null)
+ {
+ var outline = rect.GenerateStroke(image, this);
+ if (outline != null)
+ {
+ x = x.Fill(strokeBrush, outline);
+ }
+ }
+ });
+ }
+
+
+ }
+
+
+}
diff --git a/src/SixLabors.Svg/RenderTree/SvgPathOperation.cs b/src/SixLabors.Svg/RenderTree/SvgPathOperation.cs
new file mode 100644
index 0000000..85eec50
--- /dev/null
+++ b/src/SixLabors.Svg/RenderTree/SvgPathOperation.cs
@@ -0,0 +1,401 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.Primitives;
+using SixLabors.Shapes;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SixLabors.Svg.Dom
+{
+ internal struct SvgPathOperation
+ {
+ public Operations Op { get; private set; }
+ public bool IsRelative { get; private set; }
+
+ public SvgUnitValue? X { get; private set; }
+ public SvgUnitValue? Y { get; private set; }
+
+ public SvgUnitValue? X1 { get; private set; }
+ public SvgUnitValue? Y1 { get; private set; }
+
+ public SvgUnitValue? X2 { get; private set; }
+ public SvgUnitValue? Y2 { get; private set; }
+
+ public static IEnumerable Parse(string path)
+ {
+ List operations = new List();
+ char cmd = ' ';
+ for (var i = 0; i < path.Length; i++)
+ {
+ switch (path[i])
+ {
+ case 'Z':
+ case 'z':
+ {
+ operations.Add(SvgPathOperation.ClosePath());
+ }
+ break;
+ case 'M':
+ case 'm':
+ {
+ var relative = path[i] == 'm';
+ if (!TryReadFloat(in path, ref i, out var x) || !TryReadFloat(in path, ref i, out var y))
+ {
+ return null;
+ }
+
+ operations.Add(SvgPathOperation.MoveTo(x, y, relative));
+ while (TryReadFloat(in path, ref i, out x) && TryReadFloat(in path, ref i, out y))
+ {
+ operations.Add(SvgPathOperation.LineTo(x, y, relative));
+ }
+ }
+ break;
+ case 'L':
+ case 'l':
+ {
+ var relative = path[i] == 'l';
+ while (TryReadFloat(in path, ref i, out var x) && TryReadFloat(in path, ref i, out var y))
+ {
+ operations.Add(SvgPathOperation.LineTo(x, y, relative));
+ }
+ }
+ break;
+ case 'Q':
+ case 'q':
+ {
+ var relative = path[i] == 'q';
+ while (TryReadFloat(in path, ref i, out var x1) && TryReadFloat(in path, ref i, out var y1)
+ && TryReadFloat(in path, ref i, out var x) && TryReadFloat(in path, ref i, out var y))
+ {
+ operations.Add(SvgPathOperation.QuadraticTo(x1, y1, x, y, relative));
+ }
+ }
+ break;
+ case 'T':
+ case 't':
+ {
+ var relative = path[i] == 't';
+ while (TryReadFloat(in path, ref i, out var x) && TryReadFloat(in path, ref i, out var y))
+ {
+ SvgUnitValue x1;
+ SvgUnitValue y1;
+ var last = operations.Last();
+
+ if (last.Op != Operations.QuadraticTo)
+ {
+ x1 = last.X.Value;
+ y1 = last.Y.Value;
+ }
+ else
+ {
+ x1 = new SvgUnitValue(last.X.Value.Value + (last.X.Value.Value - last.X1.Value.Value), SvgUnitValue.Units.undefined);
+ y1 = new SvgUnitValue(last.Y.Value.Value + (last.Y.Value.Value - last.Y1.Value.Value), SvgUnitValue.Units.undefined);
+ }
+
+ operations.Add(SvgPathOperation.QuadraticTo(x1, y1, x, y, relative));
+ }
+ }
+ break;
+ case 'C':
+ case 'c':
+ {
+ var relative = path[i] == 'c';
+ while (TryReadFloat(in path, ref i, out var x1) && TryReadFloat(in path, ref i, out var y1)
+ && TryReadFloat(in path, ref i, out var x2) && TryReadFloat(in path, ref i, out var y2)
+ && TryReadFloat(in path, ref i, out var x) && TryReadFloat(in path, ref i, out var y))
+ {
+ operations.Add(SvgPathOperation.CubicTo(x1, y1, x2, y2, x, y, relative));
+ }
+ }
+ break;
+ case 'S':
+ case 's':
+ {
+ var relative = path[i] == 's';
+ while (TryReadFloat(in path, ref i, out var x) && TryReadFloat(in path, ref i, out var y)
+ && TryReadFloat(in path, ref i, out var x2) && TryReadFloat(in path, ref i, out var y2))
+ {
+ SvgUnitValue x1;
+ SvgUnitValue y1;
+ var last = operations.Last();
+
+ if (last.Op != Operations.CubicTo)
+ {
+ x1 = last.X.Value;
+ y1 = last.Y.Value;
+ }
+ else
+ {
+ x1 = new SvgUnitValue(last.X.Value.Value + (last.X.Value.Value - last.X2.Value.Value), SvgUnitValue.Units.undefined);
+ y1 = new SvgUnitValue(last.Y.Value.Value + (last.Y.Value.Value - last.Y2.Value.Value), SvgUnitValue.Units.undefined);
+ }
+
+ operations.Add(SvgPathOperation.CubicTo(x1, y1, x2, y2, x, y, relative));
+ }
+ }
+ break;
+ default:
+ if (char.IsWhiteSpace(path[i]) || path[i] == ',' || path[i] == ' ')
+ {
+ continue;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ }
+ return operations;
+ }
+
+ private static bool TryReadFloat(in string str, ref int position, out SvgUnitValue value)
+ {
+ // look for the first character that can be ins float
+ int floatStart = 0;
+
+ for (position++; position < str.Length; position++)
+ {
+ if (floatStart == 0)
+ {
+ if (char.IsWhiteSpace(str[position]) || str[position] == ',')
+ {
+ continue;
+ }
+ // we can skip 'whitespace'
+ }
+ if (char.IsDigit(str[position]) || str[position] == '-' || str[position] == '.')
+ {
+ if (floatStart == 0) { floatStart = position; }
+ }
+ else
+ {
+ position--;
+ break;
+ }
+ }
+
+ if (floatStart == 0)
+ {
+
+ value = SvgUnitValue.Unset;
+ return false;
+ }
+
+ if (position == str.Length)
+ {
+ value = new SvgUnitValue(float.Parse(str.Substring(floatStart)), SvgUnitValue.Units.undefined);
+ }
+ else
+ {
+ value = new SvgUnitValue(float.Parse(str.Substring(floatStart, position - floatStart + 1)), SvgUnitValue.Units.undefined);
+ }
+
+ return true;
+ }
+
+
+ public enum Operations
+ {
+ MoveTo,
+ LineTo,
+ ClosePath,
+ QuadraticTo,
+ CubicTo
+ }
+
+ private static SvgPathOperation CubicTo(SvgUnitValue x1, SvgUnitValue y1, SvgUnitValue x2, SvgUnitValue y2, SvgUnitValue x, SvgUnitValue y, bool relative)
+ {
+ return new SvgPathOperation()
+ {
+ IsRelative = relative,
+ Op = Operations.CubicTo,
+ X = x,
+ Y = y,
+ X1 = x1,
+ Y1 = y1,
+ X2 = x2,
+ Y2 = y2
+ };
+ }
+ private static SvgPathOperation QuadraticTo(SvgUnitValue x1, SvgUnitValue y1, SvgUnitValue x, SvgUnitValue y, bool relative)
+ {
+ return new SvgPathOperation()
+ {
+ IsRelative = relative,
+ Op = Operations.QuadraticTo,
+ X = x,
+ Y = y,
+ X1 = x1,
+ Y1 = y1
+ };
+ }
+
+ public static SvgPathOperation MoveTo(SvgUnitValue x, SvgUnitValue y, bool relative)
+ {
+ return new SvgPathOperation()
+ {
+ IsRelative = relative,
+ Op = Operations.MoveTo,
+ X = x,
+ Y = y
+ };
+ }
+ public static SvgPathOperation LineTo(SvgUnitValue x, SvgUnitValue y, bool relative)
+ {
+ return new SvgPathOperation()
+ {
+ IsRelative = relative,
+ Op = Operations.LineTo,
+ X = x,
+ Y = y
+ };
+ }
+
+ internal static SvgPathOperation ClosePath()
+ {
+ return new SvgPathOperation()
+ {
+ Op = Operations.ClosePath,
+ };
+ }
+ }
+
+ internal static class SvgPathOperationExtensions
+ {
+ private static PointF Point(Image img, PointF currentPoint, SvgUnitValue x, SvgUnitValue y) where TPixel : struct, IPixel
+ {
+ var p = new PointF(x.AsPixelXAxis(img), y.AsPixelYAxis(img));
+ return currentPoint + p;
+ }
+ public static PointF PointMain(this SvgPathOperation operation, Image img, PointF currentPoint) where TPixel : struct, IPixel
+ {
+ return Point(img, operation.IsRelative ? currentPoint : default, operation.X.Value, operation.Y.Value);
+ }
+ public static PointF Point1(this SvgPathOperation operation, Image img, PointF currentPoint) where TPixel : struct, IPixel
+ {
+ return Point(img, operation.IsRelative ? currentPoint : default, operation.X1.Value, operation.Y1.Value);
+ }
+ public static PointF Point2(this SvgPathOperation operation, Image img, PointF currentPoint) where TPixel : struct, IPixel
+ {
+ return Point(img, operation.IsRelative ? currentPoint : default, operation.X2.Value, operation.Y2.Value);
+ }
+
+ public static IPath GeneratePath(this IEnumerable operations, Image img) where TPixel : struct, IPixel
+ {
+ var pb = new PathRenderer();
+
+ foreach (var op in operations)
+ {
+ switch (op.Op)
+ {
+ case SvgPathOperation.Operations.MoveTo:
+ pb.MoveTo(op.PointMain(img, pb.CurrentPoint));
+ break;
+ case SvgPathOperation.Operations.LineTo:
+ pb.LineTo(op.PointMain(img, pb.CurrentPoint));
+ break;
+ case SvgPathOperation.Operations.QuadraticTo:
+ pb.QuadraticBezierTo(op.Point1(img, pb.CurrentPoint), op.PointMain(img, pb.CurrentPoint));
+ break;
+ case SvgPathOperation.Operations.CubicTo:
+ pb.CubicBezierTo(op.Point1(img, pb.CurrentPoint), op.Point2(img, pb.CurrentPoint), op.PointMain(img, pb.CurrentPoint));
+ break;
+ case SvgPathOperation.Operations.ClosePath:
+ pb.Close();
+ break;
+ default:
+ break;
+ }
+ }
+
+ return pb.Path();
+ }
+
+ internal class PathRenderer
+ {
+ ///
+ /// The builder. TODO: Should this be a property?
+ ///
+ // ReSharper disable once InconsistentNaming
+ private readonly PathBuilder builder;
+ private readonly List paths = new List();
+ private PointF currentPoint = default(PointF);
+ private PointF initalPoint = default(PointF);
+
+ public PointF CurrentPoint => currentPoint;
+ public PointF InitalPoint => initalPoint;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathRenderer()
+ {
+ // glyphs are renderd realative to bottom left so invert the Y axis to allow it to render on top left origin surface
+ this.builder = new PathBuilder();
+ }
+
+ ///
+ /// Gets the paths that have been rendered by this.
+ ///
+ public IPath Path()
+ {
+ return this.builder.Build();
+ }
+
+ ///
+ /// Draws a cubic bezier from the current point to the
+ ///
+ /// The second control point.
+ /// The third control point.
+ /// The point.
+ public void CubicBezierTo(PointF secondControlPoint, PointF thirdControlPoint, PointF point)
+ {
+ this.builder.AddBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point);
+ this.currentPoint = point;
+ }
+
+ ///
+ /// Draws a line from the current point to the .
+ ///
+ /// The point.
+ public void LineTo(PointF point)
+ {
+ this.builder.AddLine(this.currentPoint, point);
+ this.currentPoint = point;
+ }
+
+ ///
+ /// Moves to current point to the supplied vector.
+ ///
+ /// The point.
+ public void MoveTo(PointF point)
+ {
+ this.builder.StartFigure();
+ this.currentPoint = point;
+ this.initalPoint = point;
+ }
+
+ ///
+ /// Draws a quadratics bezier from the current point to the
+ ///
+ /// The second control point.
+ /// The point.
+ public void QuadraticBezierTo(PointF secondControlPoint, PointF point)
+ {
+ this.builder.AddBezier(this.currentPoint, secondControlPoint, point);
+ this.currentPoint = point;
+ }
+
+ internal void Close()
+ {
+ this.LineTo(initalPoint);
+ //this.builder.CloseFigure();
+ //this.currentPoint = this.initalPoint;
+ }
+ }
+ }
+
+}
diff --git a/src/SixLabors.Svg/RenderTree/SvgRect.cs b/src/SixLabors.Svg/RenderTree/SvgRect.cs
index 008b477..4e1826b 100644
--- a/src/SixLabors.Svg/RenderTree/SvgRect.cs
+++ b/src/SixLabors.Svg/RenderTree/SvgRect.cs
@@ -2,38 +2,45 @@
using System.Numerics;
using System.Threading.Tasks;
using AngleSharp.Svg.Dom;
+using AngleSharp.Css.Dom;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.Shapes;
namespace SixLabors.Svg.Dom
{
- internal sealed class SvgRect : SvgElement
- {
+ internal sealed class SvgRect : SvgElement, ISvgShape
+ {
public SvgUnitValue X { get; private set; }
public SvgUnitValue Y { get; private set; }
public SvgUnitValue Width { get; private set; }
public SvgUnitValue Height { get; private set; }
public SvgPaint Fill { get; private set; }
public SvgPaint Stroke { get; private set; }
+ public SvgLineCap StrokeLineCap { get; private set; }
+ public SvgLineJoin StrokeLineJoin { get; private set; }
public SvgUnitValue StrokeWidth { get; private set; }
public SvgUnitValue RadiusX { get; private set; }
public SvgUnitValue RadiusY { get; private set; }
public static Task LoadAsync(ISvgElement element)
{
+ var rx = element.TryGetUnitValue("rx");
+ var ry = element.TryGetUnitValue("ry");
return Task.FromResult(new SvgRect()
{
- Fill = SvgPaint.Parse(element.Attributes["fill"]?.Value ?? "Black", element.Attributes["fill-opacity"]?.Value ?? "1"),
- Stroke = SvgPaint.Parse(element.Attributes["stroke"]?.Value ?? "None", element.Attributes["stroke-opacity"]?.Value ?? "1"),
- StrokeWidth = SvgUnitValue.Parse(element.Attributes["stroke-width"]?.Value ?? "1"),
- X = SvgUnitValue.Parse(element.Attributes["x"]?.Value),
- Y = SvgUnitValue.Parse(element.Attributes["y"]?.Value),
- RadiusX = SvgUnitValue.Parse(element.Attributes["rx"]?.Value ?? element.Attributes["ry"]?.Value ?? "0"),
- RadiusY = SvgUnitValue.Parse(element.Attributes["ry"]?.Value ?? element.Attributes["rx"]?.Value ?? "0"),
- Width = SvgUnitValue.Parse(element.Attributes["width"]?.Value),
- Height = SvgUnitValue.Parse(element.Attributes["height"]?.Value),
+ Fill = element.GetPaint("fill", "Black", "1"),
+ Stroke = element.GetPaint("stroke", "None", "1"),
+ StrokeWidth = element.GetUnitValue("stroke-width", "1"),
+ StrokeLineCap = element.GetLineCap("stroke-linecap", "butt"),
+ StrokeLineJoin = element.GetLineJoin("stroke-linejoin", "miter"),
+ X = element.GetUnitValue("x"),
+ Y = element.GetUnitValue("y"),
+ RadiusX = rx ?? ry ?? SvgUnitValue.Zero,
+ RadiusY = ry ?? rx ?? SvgUnitValue.Zero,
+ Width = element.GetUnitValue("width"),
+ Height = element.GetUnitValue("height")
});
}
@@ -51,17 +58,19 @@ namespace SixLabors.Svg.Dom
var fillBrush = Fill.AsBrush();
var strokeBrush = Stroke.AsBrush();
- var strokeWidth = this.StrokeWidth.AsPixelXAxis(image);
image.Mutate(x =>
{
if (fillBrush != null)
{
x = x.Fill(fillBrush, rect);
}
- if (strokeBrush != null && strokeWidth > 0)
+ if (strokeBrush != null)
{
- var outline = Outliner.GenerateOutline(rect, strokeWidth);
- x = x.Fill(strokeBrush, outline);
+ var outline = rect.GenerateStroke(image, this);
+ if (outline != null)
+ {
+ x = x.Fill(strokeBrush, outline);
+ }
}
});
}
@@ -83,19 +92,15 @@ namespace SixLabors.Svg.Dom
//lets make 3 more positioned correctly, we can do that by translating the orgional artound the center of the image
var center = new Vector2(imageWidth / 2F, imageHeight / 2F);
- float rightPos = imageWidth - cornerToptLeft.Bounds.Width + 1;
- float bottomPos = imageHeight - cornerToptLeft.Bounds.Height + 1;
+ float rightPos = imageWidth - cornerToptLeft.Bounds.Width ;
+ float bottomPos = imageHeight - cornerToptLeft.Bounds.Height ;
// move it across the widthof the image - the width of the shape
- IPath cornerTopRight = cornerToptLeft.RotateDegree(90).Translate(rightPos - 0.5f, 0);
- IPath cornerBottomLeft = cornerToptLeft.RotateDegree(-90).Translate(0, bottomPos - 0.5f);
- IPath cornerBottomRight = cornerToptLeft.RotateDegree(180).Translate(rightPos - 0.5f, bottomPos - 0.5f);
+ IPath cornerTopRight = cornerToptLeft.RotateDegree(90).Translate(rightPos, 0);
+ IPath cornerBottomLeft = cornerToptLeft.RotateDegree(-90).Translate(0, bottomPos);
+ IPath cornerBottomRight = cornerToptLeft.RotateDegree(180).Translate(rightPos, bottomPos);
return new PathCollection(cornerToptLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight);
}
-
-
}
-
-
}
diff --git a/src/SixLabors.Svg/RenderTree/SvgUnitValue.cs b/src/SixLabors.Svg/RenderTree/SvgUnitValue.cs
index 6628479..a8015df 100644
--- a/src/SixLabors.Svg/RenderTree/SvgUnitValue.cs
+++ b/src/SixLabors.Svg/RenderTree/SvgUnitValue.cs
@@ -1,226 +1,18 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
+using SixLabors.Shapes;
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace SixLabors.Svg.Dom
{
- internal struct SvgPaint
- {
- public static readonly SvgPaint Unset = default(SvgPaint);
- internal SvgPaint(string value, float opacity)
- {
- Value = value;
- Opacity = opacity;
- }
-
- public string Value { get; }
-
- public float Opacity { get; }
-
- public static SvgPaint Parse(string val, string opacity)
- {
- val = val?.Trim() ?? "";
- opacity = opacity?.Trim() ?? "";
-
- float opacityVal = 1;
- if (float.TryParse(opacity, out float op))
- {
- opacityVal = op;
- if (opacityVal < 0)
- {
- opacityVal = 0;
- }
- if (opacityVal > 1)
- {
- opacityVal = 1;
- }
- }
-
- return new SvgPaint(val, opacityVal);
-
- }
-
- private TPixel? GetNamedColor(string name) where TPixel : struct, IPixel
- {
- var dict = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- ["AliceBlue"] = NamedColors.AliceBlue,
- ["MistyRose"] = NamedColors.MistyRose,
- ["Moccasin"] = NamedColors.Moccasin,
- ["NavajoWhite"] = NamedColors.NavajoWhite,
- ["Navy"] = NamedColors.Navy,
- ["OldLace"] = NamedColors.OldLace,
- ["Olive"] = NamedColors.Olive,
- ["MintCream"] = NamedColors.MintCream,
- ["OliveDrab"] = NamedColors.OliveDrab,
- ["OrangeRed"] = NamedColors.OrangeRed,
- ["Orchid"] = NamedColors.Orchid,
- ["PaleGoldenrod"] = NamedColors.PaleGoldenrod,
- ["PaleGreen"] = NamedColors.PaleGreen,
- ["PaleTurquoise"] = NamedColors.PaleTurquoise,
- ["PaleVioletRed"] = NamedColors.PaleVioletRed,
- ["Orange"] = NamedColors.Orange,
- ["PapayaWhip"] = NamedColors.PapayaWhip,
- ["MidnightBlue"] = NamedColors.MidnightBlue,
- ["MediumTurquoise"] = NamedColors.MediumTurquoise,
- ["LightSteelBlue"] = NamedColors.LightSteelBlue,
- ["LightYellow"] = NamedColors.LightYellow,
- ["Lime"] = NamedColors.Lime,
- ["LimeGreen"] = NamedColors.LimeGreen,
- ["Linen"] = NamedColors.Linen,
- ["Magenta"] = NamedColors.Magenta,
- ["MediumVioletRed"] = NamedColors.MediumVioletRed,
- ["Maroon"] = NamedColors.Maroon,
- ["MediumBlue"] = NamedColors.MediumBlue,
- ["MediumOrchid"] = NamedColors.MediumOrchid,
- ["MediumPurple"] = NamedColors.MediumPurple,
- ["MediumSeaGreen"] = NamedColors.MediumSeaGreen,
- ["MediumSlateBlue"] = NamedColors.MediumSlateBlue,
- ["MediumSpringGreen"] = NamedColors.MediumSpringGreen,
- ["MediumAquamarine"] = NamedColors.MediumAquamarine,
- ["LightSlateGray"] = NamedColors.LightSlateGray,
- ["PeachPuff"] = NamedColors.PeachPuff,
- ["Pink"] = NamedColors.Pink,
- ["SpringGreen"] = NamedColors.SpringGreen,
- ["SteelBlue"] = NamedColors.SteelBlue,
- ["Tan"] = NamedColors.Tan,
- ["Teal"] = NamedColors.Teal,
- ["Thistle"] = NamedColors.Thistle,
- ["Tomato"] = NamedColors.Tomato,
- ["Snow"] = NamedColors.Snow,
- ["Transparent"] = NamedColors.Transparent,
- ["Violet"] = NamedColors.Violet,
- ["Wheat"] = NamedColors.Wheat,
- ["White"] = NamedColors.White,
- ["WhiteSmoke"] = NamedColors.WhiteSmoke,
- ["Yellow"] = NamedColors.Yellow,
- ["YellowGreen"] = NamedColors.YellowGreen,
- ["Turquoise"] = NamedColors.Turquoise,
- ["Peru"] = NamedColors.Peru,
- ["SlateGray"] = NamedColors.SlateGray,
- ["SkyBlue"] = NamedColors.SkyBlue,
- ["Plum"] = NamedColors.Plum,
- ["PowderBlue"] = NamedColors