From aafea14480804e77655b2519ddf8b75994c47ae1 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Thu, 7 Feb 2019 21:45:39 +0000 Subject: [PATCH] improved path/shape support --- Scratch/source.svg | 8 +- src/SixLabors.Svg/RenderTree/Extensions.cs | 86 ++++ src/SixLabors.Svg/RenderTree/ISvgShape.cs | 11 + src/SixLabors.Svg/RenderTree/SvgDocument.cs | 8 +- src/SixLabors.Svg/RenderTree/SvgElement.cs | 5 + src/SixLabors.Svg/RenderTree/SvgEllipse.cs | 34 +- src/SixLabors.Svg/RenderTree/SvgPaint.cs | 231 ++++++++++ src/SixLabors.Svg/RenderTree/SvgPath.cs | 127 ++++++ .../RenderTree/SvgPathOperation.cs | 401 ++++++++++++++++++ src/SixLabors.Svg/RenderTree/SvgRect.cs | 53 +-- src/SixLabors.Svg/RenderTree/SvgUnitValue.cs | 285 ++++--------- src/SixLabors.Svg/SixLabors.Svg.csproj | 5 +- .../SixLabors.Svg.Tests.csproj | 3 +- 13 files changed, 998 insertions(+), 259 deletions(-) create mode 100644 src/SixLabors.Svg/RenderTree/Extensions.cs create mode 100644 src/SixLabors.Svg/RenderTree/ISvgShape.cs create mode 100644 src/SixLabors.Svg/RenderTree/SvgPaint.cs create mode 100644 src/SixLabors.Svg/RenderTree/SvgPath.cs create mode 100644 src/SixLabors.Svg/RenderTree/SvgPathOperation.cs 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 @@  - + + + + + @@ -14,5 +18,5 @@ - + \ 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.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); - - - return new SolidBrush(color); - - } - } internal struct SvgUnitValue { public static readonly SvgUnitValue Unset = default(SvgUnitValue); + public static readonly SvgUnitValue Zero = new SvgUnitValue(0, Units.undefined); + public static readonly SvgUnitValue One = new SvgUnitValue(1, Units.undefined); public bool IsSet { get; } public float Value { get; } @@ -272,7 +64,7 @@ namespace SixLabors.Svg.Dom } } - public static SvgUnitValue Parse(string val) + public static SvgUnitValue? Parse(string val) { val = val?.Trim() ?? ""; if (!string.IsNullOrWhiteSpace(val)) @@ -314,7 +106,7 @@ namespace SixLabors.Svg.Dom } } - return Unset; + return null; } public enum Units @@ -333,4 +125,71 @@ namespace SixLabors.Svg.Dom } + internal struct SvgLineCap + { + public static readonly SvgLineCap Unset = default(SvgLineCap); + public static readonly SvgLineCap Butt = new SvgLineCap(EndCapStyle.Butt); + public static readonly SvgLineCap Round = new SvgLineCap(EndCapStyle.Round); + public static readonly SvgLineCap Square = new SvgLineCap(EndCapStyle.Square); + + public bool IsSet { get; } + + public EndCapStyle Style { get; private set; } + + public SvgLineCap(EndCapStyle style) + { + this.IsSet = true; + this.Style = style; + } + + public static SvgLineCap? Parse(string val) + { + val = (val?.Trim().ToLower() ?? ""); + switch (val) + { + case "butt": + return SvgLineCap.Butt; + case "round": + return SvgLineCap.Round; + case "square": + return SvgLineCap.Square; + default: + return null; + } + } + } + internal struct SvgLineJoin + { + public static readonly SvgLineJoin Unset = default(SvgLineJoin); + public static readonly SvgLineJoin Miter = new SvgLineJoin(JointStyle.Miter); + public static readonly SvgLineJoin Round = new SvgLineJoin(JointStyle.Round); + public static readonly SvgLineJoin Square = new SvgLineJoin(JointStyle.Square); + + public bool IsSet { get; } + + public JointStyle Style { get; private set; } + + public SvgLineJoin(JointStyle style) + { + this.IsSet = true; + this.Style = style; + } + + public static SvgLineJoin? Parse(string val) + { + val = (val?.Trim().ToLower() ?? ""); + switch (val) + { + case "miter": + return SvgLineJoin.Miter; + case "round": + return SvgLineJoin.Round; + case "bevel": + return SvgLineJoin.Square; + default: + return null; + } + } + } + } diff --git a/src/SixLabors.Svg/SixLabors.Svg.csproj b/src/SixLabors.Svg/SixLabors.Svg.csproj index c9c0160..c21a16b 100644 --- a/src/SixLabors.Svg/SixLabors.Svg.csproj +++ b/src/SixLabors.Svg/SixLabors.Svg.csproj @@ -1,12 +1,15 @@  - netstandard1.3 + netstandard2.0 + 7.2 + + \ No newline at end of file diff --git a/tests/SixLabors.Svg.Tests/SixLabors.Svg.Tests.csproj b/tests/SixLabors.Svg.Tests/SixLabors.Svg.Tests.csproj index 2a25901..c202731 100644 --- a/tests/SixLabors.Svg.Tests/SixLabors.Svg.Tests.csproj +++ b/tests/SixLabors.Svg.Tests/SixLabors.Svg.Tests.csproj @@ -1,7 +1,8 @@  - netcoreapp1.1 + netcoreapp2.1 + 7.2