Skip to content

Commit 43874c3

Browse files
committed
Complete port of svg path parser
1 parent b329cc8 commit 43874c3

3 files changed

Lines changed: 123 additions & 144 deletions

File tree

src/ImageSharp.Drawing/Shapes/Path.cs

Lines changed: 90 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Linq;
88
using System.Numerics;
9+
using System.Runtime.CompilerServices;
910

1011
namespace SixLabors.ImageSharp.Drawing
1112
{
@@ -118,18 +119,33 @@ SegmentInfo IPathInternals.PointAlongPath(float distance)
118119
/// <inheritdoc/>
119120
IReadOnlyList<InternalPath> IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath };
120121

122+
/// <summary>
123+
/// Converts an svg path into a Path
124+
/// </summary>
125+
/// <param name="data">data</param>
126+
/// <param name="value">path</param>
127+
/// <returns>true if successful</returns>
128+
public static bool TryParseSvgPath(string data, out IPath value)
129+
=> TryParseSvgPath(data.AsSpan(), out value);
130+
131+
/// <summary>
132+
/// Converts an svg path into a Path
133+
/// </summary>
134+
/// <param name="data">data</param>
135+
/// <param name="value">path</param>
136+
/// <returns>true if successful</returns>
121137
public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
122138
{
123139
value = null;
124140

125141
var builder = new PathBuilder();
126142

127-
//parse svg
128143
PointF first = PointF.Empty;
129144
PointF c = PointF.Empty;
130145
PointF lastc = PointF.Empty;
131-
// stackalloc ???
132-
var points = new PointF[3].AsSpan();
146+
PointF point1;
147+
PointF point2;
148+
PointF point3;
133149

134150
char op = '\0';
135151
char previousOp = '\0';
@@ -167,22 +183,22 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
167183

168184
data = TrimSeperator(data.Slice(1));
169185
}
186+
170187
switch (op)
171188
{
172189
case 'M':
173-
data = FindPoints(data, points, 1, relative, c);
174-
builder.MoveTo(points[0]);
190+
data = FindPoint(data, out point1, relative, c);
191+
builder.MoveTo(point1);
175192
previousOp = '\0';
176193
op = 'L';
177-
c = points[0];
194+
c = point1;
178195
break;
179196
case 'L':
180-
data = FindPoints(data, points, 1, relative, c);
181-
builder.LineTo(points[0]);
182-
c = points[0];
197+
data = FindPoint(data, out point1, relative, c);
198+
builder.LineTo(point1);
199+
c = point1;
183200
break;
184201
case 'H':
185-
{
186202
data = FindScaler(data, out float x);
187203
if (relative)
188204
{
@@ -191,11 +207,8 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
191207

192208
builder.LineTo(x, c.Y);
193209
c.X = x;
194-
}
195-
196-
break;
210+
break;
197211
case 'V':
198-
{
199212
data = FindScaler(data, out float y);
200213
if (relative)
201214
{
@@ -204,47 +217,50 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
204217

205218
builder.LineTo(c.X, y);
206219
c.Y = y;
207-
}
208-
break;
220+
break;
209221
case 'C':
210-
data = FindPoints(data, points, 3, relative, c);
211-
builder.CubicBezierTo(points[0], points[1], points[2]);
212-
lastc = points[1];
213-
c = points[2];
222+
data = FindPoint(data, out point1, relative, c);
223+
data = FindPoint(data, out point2, relative, c);
224+
data = FindPoint(data, out point3, relative, c);
225+
builder.CubicBezierTo(point1, point2, point3);
226+
lastc = point2;
227+
c = point3;
214228
break;
215229
case 'S':
216-
data = FindPoints(data, points, 2, relative, c);
217-
points[0] = c;
218-
if (previousOp == 'C' || previousOp == 'S')
230+
data = FindPoint(data, out point2, relative, c);
231+
data = FindPoint(data, out point3, relative, c);
232+
point1 = c;
233+
if (previousOp is 'C' or 'S')
219234
{
220-
points[0].X -= lastc.X - c.X;
221-
points[0].Y -= lastc.Y - c.Y;
235+
point1.X -= lastc.X - c.X;
236+
point1.Y -= lastc.Y - c.Y;
222237
}
223-
builder.CubicBezierTo(points[0], points[1], points[2]);
224-
lastc = points[1];
225-
c = points[2];
238+
239+
builder.CubicBezierTo(point1, point2, point3);
240+
lastc = point2;
241+
c = point3;
226242
break;
227243
case 'Q': // Quadratic Bezier Curve
228-
data = FindPoints(data, points, 2, relative, c);
229-
builder.QuadraticBezierTo(points[0], points[1]);
230-
lastc = points[0];
231-
c = points[1];
244+
data = FindPoint(data, out point1, relative, c);
245+
data = FindPoint(data, out point2, relative, c);
246+
builder.QuadraticBezierTo(point1, point2);
247+
lastc = point2;
248+
c = point2;
232249
break;
233250
case 'T':
234-
data = FindPoints(data, points.Slice(1), 1, relative, c);
235-
points[0] = c;
251+
data = FindPoint(data, out point2, relative, c);
252+
point1 = c;
236253
if (previousOp is 'Q' or 'T')
237254
{
238-
points[0].X -= lastc.X - c.X;
239-
points[0].Y -= lastc.Y - c.Y;
255+
point1.X -= lastc.X - c.X;
256+
point1.Y -= lastc.Y - c.Y;
240257
}
241258

242-
builder.QuadraticBezierTo(points[0], points[1]);
243-
lastc = points[0];
244-
c = points[1];
259+
builder.QuadraticBezierTo(point1, point2);
260+
lastc = point1;
261+
c = point2;
245262
break;
246263
case 'A':
247-
{
248264
data = FindScaler(data, out float radiiX);
249265
data = TrimSeperator(data);
250266
data = FindScaler(data, out float radiiY);
@@ -261,30 +277,31 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
261277
builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point);
262278
c = point;
263279
}
264-
}
265-
break;
280+
281+
break;
266282
case 'Z':
267283
builder.CloseFigure();
268284
c = first;
269285
break;
270286
case '~':
271-
{
272-
SkPoint args[2];
273-
data = find_points(data, args, 2, false, nullptr);
274-
path.moveTo(args[0].fX, args[0].fY);
275-
path.lineTo(args[1].fX, args[1].fY);
276-
}
277-
break;
287+
data = FindPoint(data, out point1, relative, c);
288+
data = FindPoint(data, out point2, relative, c);
289+
builder.MoveTo(point1);
290+
builder.LineTo(point2);
291+
break;
278292
default:
279293
return false;
280294
}
295+
281296
if (previousOp == 0)
282297
{
283298
first = c;
284299
}
300+
285301
previousOp = op;
286302
}
287303

304+
value = builder.Build();
288305
return true;
289306

290307
static bool IsSeperator(char ch)
@@ -309,7 +326,6 @@ static ReadOnlySpan<char> TrimSeperator(ReadOnlySpan<char> data)
309326
return data.Slice(idx);
310327
}
311328

312-
313329
static ReadOnlySpan<char> FindPoint(ReadOnlySpan<char> str, out PointF value, bool isRelative, in PointF relative)
314330
{
315331
str = FindScaler(str, out float x);
@@ -324,33 +340,42 @@ static ReadOnlySpan<char> FindPoint(ReadOnlySpan<char> str, out PointF value, bo
324340
return str;
325341
}
326342

327-
static ReadOnlySpan<char> FindPoints(ReadOnlySpan<char> str, Span<PointF> value, int count, bool isRelative, in PointF relative)
328-
{
329-
for (int i = 0; i < value.Length && i < count; i++)
330-
{
331-
str = FindPoint(str, out value[i], isRelative, relative);
332-
}
333-
334-
return str;
335-
}
336-
337343
static ReadOnlySpan<char> FindScaler(ReadOnlySpan<char> str, out float scaler)
338344
{
339-
str = str.TrimStart();
345+
str = TrimSeperator(str);
340346
scaler = 0;
341347

342348
for (var i = 0; i < str.Length; i++)
343349
{
344-
if (IsSeperator(str[i]))
350+
if (IsSeperator(str[i]) || i == str.Length)
345351
{
346-
scaler = float.Parse(str.Slice(0, i));
352+
scaler = ParseFloat(str.Slice(0, i));
347353
str = str.Slice(i);
354+
return str;
348355
}
349356
}
350357

351-
// we concumed eveything
352-
return ReadOnlySpan<char>.Empty;
358+
if (str.Length > 0)
359+
{
360+
scaler = ParseFloat(str);
361+
}
362+
363+
str = ReadOnlySpan<char>.Empty;
364+
return str;
365+
}
366+
367+
#if !NETCOREAPP2_1_OR_GREATER
368+
static unsafe float ParseFloat(ReadOnlySpan<char> str)
369+
{
370+
fixed (char* p = str)
371+
{
372+
return float.Parse(new string(p, 0, str.Length));
373+
}
353374
}
375+
#else
376+
static float ParseFloat(ReadOnlySpan<char> str)
377+
=> float.Parse(str);
378+
#endif
354379
}
355380
}
356381
}

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -223,85 +223,6 @@ public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, Po
223223
public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint)
224224
=> this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint));
225225

226-
public PathBuilder ArcTo(float rx/*radiusX*/, float ry/*radiusY*/, float phi/*rotation*/, bool fa /*largeArc*/, bool fs /*sweep*/, PointF point)
227-
{
228-
var x1 = currentPoint.X;
229-
var y1 = currentPoint.Y;
230-
var x2 = point.X;
231-
var y2 = point.Y;
232-
233-
static float pow(float n) => MathF.Pow(n, 2);
234-
static float vectorAngle(float ux, float uy, float vx, float vy)
235-
{
236-
var sign = ux * vy - uy * vx < 0 ? -1 : 1;
237-
var ua = MathF.Sqrt(ux * ux + uy * uy);
238-
var va = MathF.Sqrt(vx * vx + vy * vy);
239-
var dot = ux * vx + uy * vy;
240-
241-
return sign * MathF.Acos(dot / (ua * va));
242-
}
243-
244-
static float deg(float rad) => rad * 180 / MathF.PI;
245-
246-
var sinphi = MathF.Sin(phi);
247-
var cosphi = MathF.Cos(phi);
248-
249-
// Step 1: simplify through translation/rotation
250-
var x = cosphi * (x1 - x2) / 2 + sinphi * (y1 - y2) / 2;
251-
var y = -sinphi * (x1 - x2) / 2 + cosphi * (y1 - y2) / 2;
252-
253-
var px = pow(x);
254-
var py = pow(y);
255-
var prx = pow(rx);
256-
var pry = pow(ry);
257-
258-
// correct of out-of-range radii
259-
var L = px / prx + py / pry;
260-
261-
if (L > 1)
262-
{
263-
rx = MathF.Sqrt(L) * MathF.Abs(rx);
264-
ry = MathF.Sqrt(L) * MathF.Abs(ry);
265-
}
266-
else
267-
{
268-
rx = MathF.Abs(rx);
269-
ry = MathF.Abs(ry);
270-
}
271-
272-
// Step 2 + 3: compute center
273-
var sign = fa == fs ? -1 : 1;
274-
var M = MathF.Sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign;
275-
276-
var _cx = M * (rx * y) / ry;
277-
var _cy = M * (-ry * x) / rx;
278-
279-
var cx = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2;
280-
var cy = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2;
281-
282-
// Step 4: compute θ and dθ
283-
var theta = vectorAngle(1, 0, (x - _cx) / rx, (y - _cy) / ry);
284-
285-
var _dTheta = deg(vectorAngle(
286-
(x - _cx) / rx,
287-
(y - _cy) / ry,
288-
(-x - _cx) / rx,
289-
(-y - _cy) / ry
290-
)) % 360;
291-
292-
if (!fs && _dTheta > 0)
293-
{
294-
_dTheta -= 360;
295-
}
296-
297-
if (fs && _dTheta < 0)
298-
{
299-
_dTheta += 360;
300-
}
301-
302-
return this.AddEllipticalArc(new PointF(cx, cy), rx, ry, 0, phi, _dTheta);
303-
}
304-
305226
/// <summary>
306227
/// <para>
307228
/// Adds an elliptical arc to the current figure. The arc curves from the last point to <paramref name="point"/>,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Numerics;
8+
using SixLabors.ImageSharp.Drawing.Processing;
9+
using SixLabors.ImageSharp.PixelFormats;
10+
using Xunit;
11+
12+
namespace SixLabors.ImageSharp.Drawing.Tests.Shapes
13+
{
14+
public class SvgPath
15+
{
16+
[Theory]
17+
[WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")]
18+
[WithBlankImage(200, 100, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")]
19+
[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")]
20+
[WithBlankImage(500, 400, PixelTypes.Rgba32, @"M300,200 h-150 a150,150 0 1,0 150,-150 z", "pie_small")]
21+
[WithBlankImage(500, 400, PixelTypes.Rgba32, @"M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")]
22+
public void RenderSvgPath<TPixel>(TestImageProvider<TPixel> provider, string svgPath, string exampleImageKey)
23+
where TPixel : unmanaged, IPixel<TPixel>
24+
{
25+
var parsed = Path.TryParseSvgPath(svgPath, out var path);
26+
Assert.True(parsed);
27+
28+
provider.RunValidatingProcessorTest(
29+
c => c.Fill(Color.White).Draw(Color.Red, 5, path),
30+
new { type = exampleImageKey });
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)