From d3ae3c03eab317c34ecc9bb96d1653db994a66ac Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 2 Mar 2022 18:56:03 +0000 Subject: [PATCH 1/6] wip - missing arc cleanup --- src/ImageSharp.Drawing/Shapes/Path.cs | 236 +++++++++++++++++++ src/ImageSharp.Drawing/Shapes/PathBuilder.cs | 88 +++++++ 2 files changed, 324 insertions(+) diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index cb97d1e5..4b41516a 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; @@ -116,5 +117,240 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath }; + + public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) + { + value = null; + + var builder = new PathBuilder(); + + //parse svg + PointF first = PointF.Empty; + PointF c = PointF.Empty; + PointF lastc = PointF.Empty; + // stackalloc ??? + var points = new PointF[3].AsSpan(); + + char op = '\0'; + char previousOp = '\0'; + bool relative = false; + while (true) + { + data = data.TrimStart(); + if (data.Length == 0) + { + break; + } + + char ch = data[0]; + if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') + { + // are we are the end of the string or we are at the end of the path + if (data.Length == 0 || op == 'Z') + { + return false; + } + } + else if (IsSeperator(ch)) + { + data = TrimSeperator(data); + } + else + { + op = ch; + relative = false; + if (char.IsLower(op)) + { + op = char.ToUpper(op); + relative = true; + } + + data = TrimSeperator(data.Slice(1)); + } + switch (op) + { + case 'M': + data = FindPoints(data, points, 1, relative, c); + builder.MoveTo(points[0]); + previousOp = '\0'; + op = 'L'; + c = points[0]; + break; + case 'L': + data = FindPoints(data, points, 1, relative, c); + builder.LineTo(points[0]); + c = points[0]; + break; + case 'H': + { + data = FindScaler(data, out float x); + if (relative) + { + x += c.X; + } + + builder.LineTo(x, c.Y); + c.X = x; + } + + break; + case 'V': + { + data = FindScaler(data, out float y); + if (relative) + { + y += c.Y; + } + + builder.LineTo(c.X, y); + c.Y = y; + } + break; + case 'C': + data = FindPoints(data, points, 3, relative, c); + builder.CubicBezierTo(points[0], points[1], points[2]); + lastc = points[1]; + c = points[2]; + break; + case 'S': + data = FindPoints(data, points, 2, relative, c); + points[0] = c; + if (previousOp == 'C' || previousOp == 'S') + { + points[0].X -= lastc.X - c.X; + points[0].Y -= lastc.Y - c.Y; + } + builder.CubicBezierTo(points[0], points[1], points[2]); + lastc = points[1]; + c = points[2]; + break; + case 'Q': // Quadratic Bezier Curve + data = FindPoints(data, points, 2, relative, c); + builder.QuadraticBezierTo(points[0], points[1]); + lastc = points[0]; + c = points[1]; + break; + case 'T': + data = FindPoints(data, points.Slice(1), 1, relative, c); + points[0] = c; + if (previousOp is 'Q' or 'T') + { + points[0].X -= lastc.X - c.X; + points[0].Y -= lastc.Y - c.Y; + } + + builder.QuadraticBezierTo(points[0], points[1]); + lastc = points[0]; + c = points[1]; + break; + case 'A': + { + data = FindScaler(data, out float radiiX); + data = TrimSeperator(data); + data = FindScaler(data, out float radiiY); + data = TrimSeperator(data); + data = FindScaler(data, out float angle); + data = TrimSeperator(data); + data = FindScaler(data, out float largeArc); + data = TrimSeperator(data); + data = FindScaler(data, out float sweep); + + data = FindPoint(data, out var point, relative, c); + if (data.Length > 0) + { + builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point); + c = point; + } + } + break; + case 'Z': + builder.CloseFigure(); + c = first; + break; + case '~': + { + SkPoint args[2]; + data = find_points(data, args, 2, false, nullptr); + path.moveTo(args[0].fX, args[0].fY); + path.lineTo(args[1].fX, args[1].fY); + } + break; + default: + return false; + } + if (previousOp == 0) + { + first = c; + } + previousOp = op; + } + + return true; + + static bool IsSeperator(char ch) + => char.IsWhiteSpace(ch) || ch == ','; + + static ReadOnlySpan TrimSeperator(ReadOnlySpan data) + { + if (data.Length == 0) + { + return data; + } + + int idx = 0; + for (; idx < data.Length; idx++) + { + if (!IsSeperator(data[idx])) + { + break; + } + } + + return data.Slice(idx); + } + + + static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bool isRelative, in PointF relative) + { + str = FindScaler(str, out float x); + str = FindScaler(str, out float y); + if (isRelative) + { + x += relative.X; + y += relative.Y; + } + + value = new PointF(x, y); + return str; + } + + static ReadOnlySpan FindPoints(ReadOnlySpan str, Span value, int count, bool isRelative, in PointF relative) + { + for (int i = 0; i < value.Length && i < count; i++) + { + str = FindPoint(str, out value[i], isRelative, relative); + } + + return str; + } + + static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) + { + str = str.TrimStart(); + scaler = 0; + + for (var i = 0; i < str.Length; i++) + { + if (IsSeperator(str[i])) + { + scaler = float.Parse(str.Slice(0, i)); + str = str.Slice(i); + } + } + + // we concumed eveything + return ReadOnlySpan.Empty; + } + } } } diff --git a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs index 35676065..5754a7a8 100644 --- a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs @@ -108,6 +108,15 @@ public void MoveTo(Vector2 point) public PathBuilder LineTo(PointF point) => this.AddLine(this.currentPoint, point); + /// + /// Draws the line connecting the current the current point to the new point. + /// + /// The x. + /// The y. + /// The + public PathBuilder LineTo(float x, float y) + => this.LineTo(new PointF(x, y)); + /// /// Adds the line connecting the current point to the new point. /// @@ -217,6 +226,85 @@ public PathBuilder AddBezier(PointF startPoint, PointF controlPoint, PointF endP public PathBuilder AddBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint) => this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint)); + public PathBuilder ArcTo(float rx/*radiusX*/, float ry/*radiusY*/, float phi/*rotation*/, bool fa /*largeArc*/, bool fs /*sweep*/, PointF point) + { + var x1 = currentPoint.X; + var y1 = currentPoint.Y; + var x2 = point.X; + var y2 = point.Y; + + static float pow(float n) => MathF.Pow(n, 2); + static float vectorAngle(float ux, float uy, float vx, float vy) + { + var sign = ux * vy - uy * vx < 0 ? -1 : 1; + var ua = MathF.Sqrt(ux * ux + uy * uy); + var va = MathF.Sqrt(vx * vx + vy * vy); + var dot = ux * vx + uy * vy; + + return sign * MathF.Acos(dot / (ua * va)); + } + + static float deg(float rad) => rad * 180 / MathF.PI; + + var sinphi = MathF.Sin(phi); + var cosphi = MathF.Cos(phi); + + // Step 1: simplify through translation/rotation + var x = cosphi * (x1 - x2) / 2 + sinphi * (y1 - y2) / 2; + var y = -sinphi * (x1 - x2) / 2 + cosphi * (y1 - y2) / 2; + + var px = pow(x); + var py = pow(y); + var prx = pow(rx); + var pry = pow(ry); + + // correct of out-of-range radii + var L = px / prx + py / pry; + + if (L > 1) + { + rx = MathF.Sqrt(L) * MathF.Abs(rx); + ry = MathF.Sqrt(L) * MathF.Abs(ry); + } + else + { + rx = MathF.Abs(rx); + ry = MathF.Abs(ry); + } + + // Step 2 + 3: compute center + var sign = fa == fs ? -1 : 1; + var M = MathF.Sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign; + + var _cx = M * (rx * y) / ry; + var _cy = M * (-ry * x) / rx; + + var cx = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2; + var cy = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2; + + // Step 4: compute θ and dθ + var theta = vectorAngle(1, 0, (x - _cx) / rx, (y - _cy) / ry); + + var _dTheta = deg(vectorAngle( + (x - _cx) / rx, + (y - _cy) / ry, + (-x - _cx) / rx, + (-y - _cy) / ry + )) % 360; + + if (!fs && _dTheta > 0) + { + _dTheta -= 360; + } + + if (fs && _dTheta < 0) + { + _dTheta += 360; + } + + return this.AddEllipticalArc(new PointF(cx, cy), rx, ry, 0, phi, _dTheta); + } + /// /// Adds an elliptical arc to the current figure /// From 43874c3de571d447c0fc033980dda9f5233e3c06 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 13 Apr 2022 17:42:23 +0100 Subject: [PATCH 2/6] Complete port of svg path parser --- src/ImageSharp.Drawing/Shapes/Path.cs | 155 ++++++++++-------- src/ImageSharp.Drawing/Shapes/PathBuilder.cs | 79 --------- .../Shapes/SvgPath.cs | 33 ++++ 3 files changed, 123 insertions(+), 144 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index 4b41516a..34c15419 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing { @@ -118,18 +119,33 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath }; + /// + /// Converts an svg path into a Path + /// + /// data + /// path + /// true if successful + public static bool TryParseSvgPath(string data, out IPath value) + => TryParseSvgPath(data.AsSpan(), out value); + + /// + /// Converts an svg path into a Path + /// + /// data + /// path + /// true if successful public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) { value = null; var builder = new PathBuilder(); - //parse svg PointF first = PointF.Empty; PointF c = PointF.Empty; PointF lastc = PointF.Empty; - // stackalloc ??? - var points = new PointF[3].AsSpan(); + PointF point1; + PointF point2; + PointF point3; char op = '\0'; char previousOp = '\0'; @@ -167,22 +183,22 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) data = TrimSeperator(data.Slice(1)); } + switch (op) { case 'M': - data = FindPoints(data, points, 1, relative, c); - builder.MoveTo(points[0]); + data = FindPoint(data, out point1, relative, c); + builder.MoveTo(point1); previousOp = '\0'; op = 'L'; - c = points[0]; + c = point1; break; case 'L': - data = FindPoints(data, points, 1, relative, c); - builder.LineTo(points[0]); - c = points[0]; + data = FindPoint(data, out point1, relative, c); + builder.LineTo(point1); + c = point1; break; case 'H': - { data = FindScaler(data, out float x); if (relative) { @@ -191,11 +207,8 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) builder.LineTo(x, c.Y); c.X = x; - } - - break; + break; case 'V': - { data = FindScaler(data, out float y); if (relative) { @@ -204,47 +217,50 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) builder.LineTo(c.X, y); c.Y = y; - } - break; + break; case 'C': - data = FindPoints(data, points, 3, relative, c); - builder.CubicBezierTo(points[0], points[1], points[2]); - lastc = points[1]; - c = points[2]; + data = FindPoint(data, out point1, relative, c); + data = FindPoint(data, out point2, relative, c); + data = FindPoint(data, out point3, relative, c); + builder.CubicBezierTo(point1, point2, point3); + lastc = point2; + c = point3; break; case 'S': - data = FindPoints(data, points, 2, relative, c); - points[0] = c; - if (previousOp == 'C' || previousOp == 'S') + data = FindPoint(data, out point2, relative, c); + data = FindPoint(data, out point3, relative, c); + point1 = c; + if (previousOp is 'C' or 'S') { - points[0].X -= lastc.X - c.X; - points[0].Y -= lastc.Y - c.Y; + point1.X -= lastc.X - c.X; + point1.Y -= lastc.Y - c.Y; } - builder.CubicBezierTo(points[0], points[1], points[2]); - lastc = points[1]; - c = points[2]; + + builder.CubicBezierTo(point1, point2, point3); + lastc = point2; + c = point3; break; case 'Q': // Quadratic Bezier Curve - data = FindPoints(data, points, 2, relative, c); - builder.QuadraticBezierTo(points[0], points[1]); - lastc = points[0]; - c = points[1]; + data = FindPoint(data, out point1, relative, c); + data = FindPoint(data, out point2, relative, c); + builder.QuadraticBezierTo(point1, point2); + lastc = point2; + c = point2; break; case 'T': - data = FindPoints(data, points.Slice(1), 1, relative, c); - points[0] = c; + data = FindPoint(data, out point2, relative, c); + point1 = c; if (previousOp is 'Q' or 'T') { - points[0].X -= lastc.X - c.X; - points[0].Y -= lastc.Y - c.Y; + point1.X -= lastc.X - c.X; + point1.Y -= lastc.Y - c.Y; } - builder.QuadraticBezierTo(points[0], points[1]); - lastc = points[0]; - c = points[1]; + builder.QuadraticBezierTo(point1, point2); + lastc = point1; + c = point2; break; case 'A': - { data = FindScaler(data, out float radiiX); data = TrimSeperator(data); data = FindScaler(data, out float radiiY); @@ -261,30 +277,31 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point); c = point; } - } - break; + + break; case 'Z': builder.CloseFigure(); c = first; break; case '~': - { - SkPoint args[2]; - data = find_points(data, args, 2, false, nullptr); - path.moveTo(args[0].fX, args[0].fY); - path.lineTo(args[1].fX, args[1].fY); - } - break; + data = FindPoint(data, out point1, relative, c); + data = FindPoint(data, out point2, relative, c); + builder.MoveTo(point1); + builder.LineTo(point2); + break; default: return false; } + if (previousOp == 0) { first = c; } + previousOp = op; } + value = builder.Build(); return true; static bool IsSeperator(char ch) @@ -309,7 +326,6 @@ static ReadOnlySpan TrimSeperator(ReadOnlySpan data) return data.Slice(idx); } - static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bool isRelative, in PointF relative) { str = FindScaler(str, out float x); @@ -324,33 +340,42 @@ static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bo return str; } - static ReadOnlySpan FindPoints(ReadOnlySpan str, Span value, int count, bool isRelative, in PointF relative) - { - for (int i = 0; i < value.Length && i < count; i++) - { - str = FindPoint(str, out value[i], isRelative, relative); - } - - return str; - } - static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) { - str = str.TrimStart(); + str = TrimSeperator(str); scaler = 0; for (var i = 0; i < str.Length; i++) { - if (IsSeperator(str[i])) + if (IsSeperator(str[i]) || i == str.Length) { - scaler = float.Parse(str.Slice(0, i)); + scaler = ParseFloat(str.Slice(0, i)); str = str.Slice(i); + return str; } } - // we concumed eveything - return ReadOnlySpan.Empty; + if (str.Length > 0) + { + scaler = ParseFloat(str); + } + + str = ReadOnlySpan.Empty; + return str; + } + +#if !NETCOREAPP2_1_OR_GREATER + static unsafe float ParseFloat(ReadOnlySpan str) + { + fixed (char* p = str) + { + return float.Parse(new string(p, 0, str.Length)); + } } +#else + static float ParseFloat(ReadOnlySpan str) + => float.Parse(str); +#endif } } } diff --git a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs index 8da9f202..263f5407 100644 --- a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs @@ -223,85 +223,6 @@ public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, Po public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint) => this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint)); - public PathBuilder ArcTo(float rx/*radiusX*/, float ry/*radiusY*/, float phi/*rotation*/, bool fa /*largeArc*/, bool fs /*sweep*/, PointF point) - { - var x1 = currentPoint.X; - var y1 = currentPoint.Y; - var x2 = point.X; - var y2 = point.Y; - - static float pow(float n) => MathF.Pow(n, 2); - static float vectorAngle(float ux, float uy, float vx, float vy) - { - var sign = ux * vy - uy * vx < 0 ? -1 : 1; - var ua = MathF.Sqrt(ux * ux + uy * uy); - var va = MathF.Sqrt(vx * vx + vy * vy); - var dot = ux * vx + uy * vy; - - return sign * MathF.Acos(dot / (ua * va)); - } - - static float deg(float rad) => rad * 180 / MathF.PI; - - var sinphi = MathF.Sin(phi); - var cosphi = MathF.Cos(phi); - - // Step 1: simplify through translation/rotation - var x = cosphi * (x1 - x2) / 2 + sinphi * (y1 - y2) / 2; - var y = -sinphi * (x1 - x2) / 2 + cosphi * (y1 - y2) / 2; - - var px = pow(x); - var py = pow(y); - var prx = pow(rx); - var pry = pow(ry); - - // correct of out-of-range radii - var L = px / prx + py / pry; - - if (L > 1) - { - rx = MathF.Sqrt(L) * MathF.Abs(rx); - ry = MathF.Sqrt(L) * MathF.Abs(ry); - } - else - { - rx = MathF.Abs(rx); - ry = MathF.Abs(ry); - } - - // Step 2 + 3: compute center - var sign = fa == fs ? -1 : 1; - var M = MathF.Sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign; - - var _cx = M * (rx * y) / ry; - var _cy = M * (-ry * x) / rx; - - var cx = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2; - var cy = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2; - - // Step 4: compute θ and dθ - var theta = vectorAngle(1, 0, (x - _cx) / rx, (y - _cy) / ry); - - var _dTheta = deg(vectorAngle( - (x - _cx) / rx, - (y - _cy) / ry, - (-x - _cx) / rx, - (-y - _cy) / ry - )) % 360; - - if (!fs && _dTheta > 0) - { - _dTheta -= 360; - } - - if (fs && _dTheta < 0) - { - _dTheta += 360; - } - - return this.AddEllipticalArc(new PointF(cx, cy), rx, ry, 0, phi, _dTheta); - } - /// /// /// Adds an elliptical arc to the current figure. The arc curves from the last point to , diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs new file mode 100644 index 00000000..a8fb8424 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes +{ + public class SvgPath + { + [Theory] + [WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] + [WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M10,350 l 50,-25 a25,25 -30 0,1 50,-25 l 50,-25 a25,50 -30 0,1 50,-25 l 50,-25 a25,75 -30 0,1 50,-25 l 50,-25 a25,100 -30 0,1 50,-25 l 50,-25", "bumpy")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M300,200 h-150 a150,150 0 1,0 150,-150 z", "pie_small")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")] + public void RenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) + where TPixel : unmanaged, IPixel + { + var parsed = Path.TryParseSvgPath(svgPath, out var path); + Assert.True(parsed); + + provider.RunValidatingProcessorTest( + c => c.Fill(Color.White).Draw(Color.Red, 5, path), + new { type = exampleImageKey }); + } + } +} From 0422bc875cf80e579a7b8280c6d3ba231f93b08d Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Thu, 14 Apr 2022 11:35:45 +0100 Subject: [PATCH 3/6] svg path tests --- src/ImageSharp.Drawing/Shapes/Path.cs | 2 +- tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs | 10 ++++------ .../RenderSvgPath_Rgba32_Blank100x100_type-arrows.png | 3 +++ .../RenderSvgPath_Rgba32_Blank110x50_type-wave.png | 3 +++ .../RenderSvgPath_Rgba32_Blank110x70_type-zag.png | 3 +++ .../RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png | 3 +++ ...erSvgPath_Rgba32_Blank500x400_type-chopped_oval.png | 3 +++ .../RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png | 3 +++ ...enderSvgPath_Rgba32_Blank500x400_type-pie_small.png | 3 +++ 9 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png create mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index 34c15419..f0242f7d 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -244,7 +244,7 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) data = FindPoint(data, out point1, relative, c); data = FindPoint(data, out point2, relative, c); builder.QuadraticBezierTo(point1, point2); - lastc = point2; + lastc = point1; c = point2; break; case 'T': diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs index a8fb8424..07896c07 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs @@ -1,10 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -14,11 +10,13 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Shapes public class SvgPath { [Theory] - [WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] - [WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")] + [WithBlankImage(110, 70, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] + [WithBlankImage(110, 50, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")] [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M10,350 l 50,-25 a25,25 -30 0,1 50,-25 l 50,-25 a25,50 -30 0,1 50,-25 l 50,-25 a25,75 -30 0,1 50,-25 l 50,-25 a25,100 -30 0,1 50,-25 l 50,-25", "bumpy")] [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M300,200 h-150 a150,150 0 1,0 150,-150 z", "pie_small")] [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")] + [WithBlankImage(100, 100, PixelTypes.Rgba32, @"M50,50 L50,20 L80,50 z M40,60 L40,90 L10,60 z", "arrows")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, @"M 10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10", "chopped_oval")] public void RenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) where TPixel : unmanaged, IPixel { diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png new file mode 100644 index 00000000..45f63f96 --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c531c3075c647406f3712bb395c5424d0ba85e15c1bb70104fcd4fa9cdaef538 +size 999 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png new file mode 100644 index 00000000..e03e5198 --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:151efe25575386b70f469a834ebeb6e3fc1511605ded38e67a0037efe63558df +size 1162 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png new file mode 100644 index 00000000..e955d615 --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f494915daf93b3fa6f8bc6be08cdb410615a9708a89e5542b64f0f73c172466 +size 922 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png new file mode 100644 index 00000000..3f7f7b3a --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c23cb1535af5bf55296410c887534f9f07a4d8c35d4873f12477c7817802a6c +size 10078 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png new file mode 100644 index 00000000..754e01e9 --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49ae0db9378f7885d8d7aee40f0efa8bd4a03ca1617641fa173f5d145cd880b4 +size 5682 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png new file mode 100644 index 00000000..6945aa04 --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27962fba1de95c0f9399281791995312dbf0f2d037d349c053c1be46523a6cae +size 5147 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png new file mode 100644 index 00000000..f486a36c --- /dev/null +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:884b371be09bf37bb0f9a8c4b7a7f73509aa0389dd8c09e6361b66b070146e75 +size 9466 From a50a5be3d931a611955360bb4092f4c0c0543d7e Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Thu, 14 Apr 2022 12:30:03 +0100 Subject: [PATCH 4/6] make it a tiny bit more forgivving for full framework errors --- tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs index 07896c07..b8eb43bf 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -25,7 +26,8 @@ public void RenderSvgPath(TestImageProvider provider, string svg provider.RunValidatingProcessorTest( c => c.Fill(Color.White).Draw(Color.Red, 5, path), - new { type = exampleImageKey }); + new { type = exampleImageKey }, + comparer: ImageComparer.TolerantPercentage(0.002f)); } } } From a31cb87ea039232b10b807738b7ff705bf34e869 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 15 Apr 2022 17:27:45 +1000 Subject: [PATCH 5/6] Cleanup --- src/ImageSharp.Drawing/Shapes/Path.cs | 40 +++++++++---------- .../Drawing/FillPathTests.cs | 8 ++-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index f0242f7d..258595c0 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing { @@ -161,15 +159,15 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) char ch = data[0]; if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') { - // are we are the end of the string or we are at the end of the path + // Are we are the end of the string or we are at the end of the path? if (data.Length == 0 || op == 'Z') { return false; } } - else if (IsSeperator(ch)) + else if (IsSeparator(ch)) { - data = TrimSeperator(data); + data = TrimSeparator(data); } else { @@ -181,7 +179,7 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) relative = true; } - data = TrimSeperator(data.Slice(1)); + data = TrimSeparator(data.Slice(1)); } switch (op) @@ -262,16 +260,16 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) break; case 'A': data = FindScaler(data, out float radiiX); - data = TrimSeperator(data); + data = TrimSeparator(data); data = FindScaler(data, out float radiiY); - data = TrimSeperator(data); + data = TrimSeparator(data); data = FindScaler(data, out float angle); - data = TrimSeperator(data); + data = TrimSeparator(data); data = FindScaler(data, out float largeArc); - data = TrimSeperator(data); + data = TrimSeparator(data); data = FindScaler(data, out float sweep); - data = FindPoint(data, out var point, relative, c); + data = FindPoint(data, out PointF point, relative, c); if (data.Length > 0) { builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point); @@ -304,10 +302,10 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) value = builder.Build(); return true; - static bool IsSeperator(char ch) + static bool IsSeparator(char ch) => char.IsWhiteSpace(ch) || ch == ','; - static ReadOnlySpan TrimSeperator(ReadOnlySpan data) + static ReadOnlySpan TrimSeparator(ReadOnlySpan data) { if (data.Length == 0) { @@ -317,7 +315,7 @@ static ReadOnlySpan TrimSeperator(ReadOnlySpan data) int idx = 0; for (; idx < data.Length; idx++) { - if (!IsSeperator(data[idx])) + if (!IsSeparator(data[idx])) { break; } @@ -326,7 +324,7 @@ static ReadOnlySpan TrimSeperator(ReadOnlySpan data) return data.Slice(idx); } - static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bool isRelative, in PointF relative) + static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bool isRelative, PointF relative) { str = FindScaler(str, out float x); str = FindScaler(str, out float y); @@ -342,16 +340,15 @@ static ReadOnlySpan FindPoint(ReadOnlySpan str, out PointF value, bo static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) { - str = TrimSeperator(str); + str = TrimSeparator(str); scaler = 0; - for (var i = 0; i < str.Length; i++) + for (int i = 0; i < str.Length; i++) { - if (IsSeperator(str[i]) || i == str.Length) + if (IsSeparator(str[i]) || i == str.Length) { scaler = ParseFloat(str.Slice(0, i)); - str = str.Slice(i); - return str; + return str.Slice(i); } } @@ -360,8 +357,7 @@ static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) scaler = ParseFloat(str); } - str = ReadOnlySpan.Empty; - return str; + return ReadOnlySpan.Empty; } #if !NETCOREAPP2_1_OR_GREATER diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs index c9dcedc7..258bd618 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs @@ -23,7 +23,7 @@ public void FillPathSVGArcs(TestImageProvider provider) pb.MoveTo(new Vector2(80, 80)) .ArcTo(45, 45, 0, false, false, new Vector2(125, 125)) .LineTo(new Vector2(125, 80)) - .LineTo(new Vector2(80, 80)); + .CloseFigure(); IPath path = pb.Build(); @@ -31,7 +31,7 @@ public void FillPathSVGArcs(TestImageProvider provider) pb.MoveTo(new Vector2(230, 80)) .ArcTo(45, 45, 0, true, false, new Vector2(275, 125)) .LineTo(new Vector2(275, 80)) - .LineTo(new Vector2(230, 80)); + .CloseFigure(); IPath path2 = pb.Build(); @@ -39,7 +39,7 @@ public void FillPathSVGArcs(TestImageProvider provider) pb.MoveTo(new Vector2(80, 230)) .ArcTo(45, 45, 0, false, true, new Vector2(125, 275)) .LineTo(new Vector2(125, 230)) - .LineTo(new Vector2(80, 230)); + .CloseFigure(); IPath path3 = pb.Build(); @@ -47,7 +47,7 @@ public void FillPathSVGArcs(TestImageProvider provider) pb.MoveTo(new Vector2(230, 230)) .ArcTo(45, 45, 0, true, true, new Vector2(275, 275)) .LineTo(new Vector2(275, 230)) - .LineTo(new Vector2(230, 230)); + .CloseFigure(); IPath path4 = pb.Build(); From bc53f7dddf0d119abfd36a864814e95a3004f16c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 15 Apr 2022 17:35:12 +1000 Subject: [PATCH 6/6] Fix intellisense docs. --- src/ImageSharp.Drawing/Shapes/Path.cs | 92 ++++++++++++++------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index 258595c0..20910f2b 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -118,21 +118,27 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath }; /// - /// Converts an svg path into a Path + /// Converts an SVG path string into an . /// - /// data - /// path - /// true if successful - public static bool TryParseSvgPath(string data, out IPath value) - => TryParseSvgPath(data.AsSpan(), out value); + /// The string containing the SVG path data. + /// + /// When this method returns, contains the logic path converted from the given SVG path string; otherwise, . + /// This parameter is passed uninitialized. + /// + /// if the input value can be parsed and converted; otherwise, . + public static bool TryParseSvgPath(string svgPath, out IPath value) + => TryParseSvgPath(svgPath.AsSpan(), out value); /// - /// Converts an svg path into a Path + /// Converts an SVG path string into an . /// - /// data - /// path - /// true if successful - public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) + /// The string containing the SVG path data. + /// + /// When this method returns, contains the logic path converted from the given SVG path string; otherwise, . + /// This parameter is passed uninitialized. + /// + /// if the input value can be parsed and converted; otherwise, . + public static bool TryParseSvgPath(ReadOnlySpan svgPath, out IPath value) { value = null; @@ -150,24 +156,24 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) bool relative = false; while (true) { - data = data.TrimStart(); - if (data.Length == 0) + svgPath = svgPath.TrimStart(); + if (svgPath.Length == 0) { break; } - char ch = data[0]; + char ch = svgPath[0]; if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') { // Are we are the end of the string or we are at the end of the path? - if (data.Length == 0 || op == 'Z') + if (svgPath.Length == 0 || op == 'Z') { return false; } } else if (IsSeparator(ch)) { - data = TrimSeparator(data); + svgPath = TrimSeparator(svgPath); } else { @@ -179,25 +185,25 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) relative = true; } - data = TrimSeparator(data.Slice(1)); + svgPath = TrimSeparator(svgPath.Slice(1)); } switch (op) { case 'M': - data = FindPoint(data, out point1, relative, c); + svgPath = FindPoint(svgPath, out point1, relative, c); builder.MoveTo(point1); previousOp = '\0'; op = 'L'; c = point1; break; case 'L': - data = FindPoint(data, out point1, relative, c); + svgPath = FindPoint(svgPath, out point1, relative, c); builder.LineTo(point1); c = point1; break; case 'H': - data = FindScaler(data, out float x); + svgPath = FindScaler(svgPath, out float x); if (relative) { x += c.X; @@ -207,7 +213,7 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) c.X = x; break; case 'V': - data = FindScaler(data, out float y); + svgPath = FindScaler(svgPath, out float y); if (relative) { y += c.Y; @@ -217,16 +223,16 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) c.Y = y; break; case 'C': - data = FindPoint(data, out point1, relative, c); - data = FindPoint(data, out point2, relative, c); - data = FindPoint(data, out point3, relative, c); + svgPath = FindPoint(svgPath, out point1, relative, c); + svgPath = FindPoint(svgPath, out point2, relative, c); + svgPath = FindPoint(svgPath, out point3, relative, c); builder.CubicBezierTo(point1, point2, point3); lastc = point2; c = point3; break; case 'S': - data = FindPoint(data, out point2, relative, c); - data = FindPoint(data, out point3, relative, c); + svgPath = FindPoint(svgPath, out point2, relative, c); + svgPath = FindPoint(svgPath, out point3, relative, c); point1 = c; if (previousOp is 'C' or 'S') { @@ -239,14 +245,14 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) c = point3; break; case 'Q': // Quadratic Bezier Curve - data = FindPoint(data, out point1, relative, c); - data = FindPoint(data, out point2, relative, c); + svgPath = FindPoint(svgPath, out point1, relative, c); + svgPath = FindPoint(svgPath, out point2, relative, c); builder.QuadraticBezierTo(point1, point2); lastc = point1; c = point2; break; case 'T': - data = FindPoint(data, out point2, relative, c); + svgPath = FindPoint(svgPath, out point2, relative, c); point1 = c; if (previousOp is 'Q' or 'T') { @@ -259,18 +265,18 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) c = point2; break; case 'A': - data = FindScaler(data, out float radiiX); - data = TrimSeparator(data); - data = FindScaler(data, out float radiiY); - data = TrimSeparator(data); - data = FindScaler(data, out float angle); - data = TrimSeparator(data); - data = FindScaler(data, out float largeArc); - data = TrimSeparator(data); - data = FindScaler(data, out float sweep); - - data = FindPoint(data, out PointF point, relative, c); - if (data.Length > 0) + svgPath = FindScaler(svgPath, out float radiiX); + svgPath = TrimSeparator(svgPath); + svgPath = FindScaler(svgPath, out float radiiY); + svgPath = TrimSeparator(svgPath); + svgPath = FindScaler(svgPath, out float angle); + svgPath = TrimSeparator(svgPath); + svgPath = FindScaler(svgPath, out float largeArc); + svgPath = TrimSeparator(svgPath); + svgPath = FindScaler(svgPath, out float sweep); + + svgPath = FindPoint(svgPath, out PointF point, relative, c); + if (svgPath.Length > 0) { builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point); c = point; @@ -282,8 +288,8 @@ public static bool TryParseSvgPath(ReadOnlySpan data, out IPath value) c = first; break; case '~': - data = FindPoint(data, out point1, relative, c); - data = FindPoint(data, out point2, relative, c); + svgPath = FindPoint(svgPath, out point1, relative, c); + svgPath = FindPoint(svgPath, out point2, relative, c); builder.MoveTo(point1); builder.LineTo(point2); break;