SvgPathBuilder.cs 13.8 KB
Newer Older
davescriven's avatar
davescriven committed
1
2
using System;
using System.Collections.Generic;
3
using System.ComponentModel;
davescriven's avatar
davescriven committed
4
using System.Diagnostics;
5
using System.Drawing;
6
using System.Globalization;
Tebjan Halm's avatar
Tebjan Halm committed
7
using System.Linq;
Tebjan Halm's avatar
Tebjan Halm committed
8
using System.Text.RegularExpressions;
9
10
11
using System.Threading;

using Svg.Pathing;
davescriven's avatar
davescriven committed
12
13
14

namespace Svg
{
Tebjan Halm's avatar
Tebjan Halm committed
15
16
17
18
19
20
21
22
	public static class PointFExtensions
	{
		public static string ToSvgString(this PointF p)
		{
			return p.X.ToString() + " " + p.Y.ToString();
		}
	}
	
23
    public class SvgPathBuilder : TypeConverter
davescriven's avatar
davescriven committed
24
    {
25
26
27
28
        /// <summary>
        /// Parses the specified string into a collection of path segments.
        /// </summary>
        /// <param name="path">A <see cref="string"/> containing path data.</param>
davescriven's avatar
davescriven committed
29
30
31
        public static SvgPathSegmentList Parse(string path)
        {
            if (string.IsNullOrEmpty(path))
32
            {
davescriven's avatar
davescriven committed
33
                throw new ArgumentNullException("path");
34
            }
davescriven's avatar
davescriven committed
35

36
            var segments = new SvgPathSegmentList();
davescriven's avatar
davescriven committed
37
38
39

            try
            {
40
41
42
43
                List<float> coords;
                char command;
                bool isRelative;

44
                foreach (var commandSet in SplitCommands(path.TrimEnd(null)))
davescriven's avatar
davescriven committed
45
                {
46
                    coords = new List<float>(ParseCoordinates(commandSet.Trim()));
47
48
                    command = commandSet[0];
                    isRelative = char.IsLower(command);
davescriven's avatar
davescriven committed
49
50
                    // http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation

51
52
53
54
55
56
57
58
59
60
61
62
63
64
                    CreatePathSegment(command, segments, coords, isRelative);
                }
            }
            catch (Exception exc)
            {
                Trace.TraceError("Error parsing path \"{0}\": {1}", path, exc.Message);
            }

            return segments;
        }

        public static void CreatePathSegment(char command, SvgPathSegmentList segments, List<float> coords, bool isRelative)
        {
         
davescriven's avatar
davescriven committed
65
66
67
68
                    switch (command)
                    {
                        case 'm': // relative moveto
                        case 'M': // moveto
69
70
71
72
                            segments.Add(
                                new SvgMoveToSegment(ToAbsolute(coords[0], coords[1], segments, isRelative)));

                            for (var i = 2; i < coords.Count; i += 2)
davescriven's avatar
davescriven committed
73
                            {
74
75
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
76
77
78
79
                            }
                            break;
                        case 'a':
                        case 'A':
80
81
82
83
84
                            SvgArcSize size;
                            SvgArcSweep sweep;

                            for (var i = 0; i < coords.Count; i += 7)
                            {
85
86
                                size = (coords[i + 3] != 0.0f) ? SvgArcSize.Large : SvgArcSize.Small;
                                sweep = (coords[i + 4] != 0.0f) ? SvgArcSweep.Positive : SvgArcSweep.Negative;
87
88
89
90
91
92

                                // A|a rx ry x-axis-rotation large-arc-flag sweep-flag x y
                                segments.Add(new SvgArcSegment(segments.Last.End, coords[i], coords[i + 1], coords[i + 2],
                                    size, sweep, ToAbsolute(coords[i + 5], coords[i + 6], segments, isRelative)));
                            }
                            break;
davescriven's avatar
davescriven committed
93
94
                        case 'l': // relative lineto
                        case 'L': // lineto
95
                            for (var i = 0; i < coords.Count; i += 2)
davescriven's avatar
davescriven committed
96
                            {
97
98
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
99
100
101
102
                            }
                            break;
                        case 'H': // horizontal lineto
                        case 'h': // relative horizontal lineto
103
104
105
                            foreach (var value in coords)
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(value, segments.Last.End.Y, segments, isRelative, false)));
davescriven's avatar
davescriven committed
106
107
108
                            break;
                        case 'V': // vertical lineto
                        case 'v': // relative vertical lineto
109
110
111
                            foreach (var value in coords)
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(segments.Last.End.X, value, segments, false, isRelative)));
davescriven's avatar
davescriven committed
112
113
114
                            break;
                        case 'Q': // curveto
                        case 'q': // relative curveto
115
                            for (var i = 0; i < coords.Count; i += 4)
davescriven's avatar
davescriven committed
116
                            {
117
118
119
                                segments.Add(new SvgQuadraticCurveSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative),
                                    ToAbsolute(coords[i + 2], coords[i + 3], segments, isRelative)));
davescriven's avatar
davescriven committed
120
121
122
123
                            }
                            break;
                        case 'T': // shorthand/smooth curveto
                        case 't': // relative shorthand/smooth curveto
124
                            for (var i = 0; i < coords.Count; i += 2)
davescriven's avatar
davescriven committed
125
                            {
126
                                var lastQuadCurve = segments.Last as SvgQuadraticCurveSegment;
davescriven's avatar
davescriven committed
127

128
129
130
                                var controlPoint = lastQuadCurve != null
                                    ? Reflect(lastQuadCurve.ControlPoint, segments.Last.End)
                                    : segments.Last.End;
davescriven's avatar
davescriven committed
131

132
133
                                segments.Add(new SvgQuadraticCurveSegment(segments.Last.End, controlPoint,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
134
135
136
137
                            }
                            break;
                        case 'C': // curveto
                        case 'c': // relative curveto
138
                            for (var i = 0; i < coords.Count; i += 6)
davescriven's avatar
davescriven committed
139
                            {
140
141
142
143
                                segments.Add(new SvgCubicCurveSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative),
                                    ToAbsolute(coords[i + 2], coords[i + 3], segments, isRelative),
                                    ToAbsolute(coords[i + 4], coords[i + 5], segments, isRelative)));
davescriven's avatar
davescriven committed
144
145
146
147
148
                            }
                            break;
                        case 'S': // shorthand/smooth curveto
                        case 's': // relative shorthand/smooth curveto

149
                            for (var i = 0; i < coords.Count; i += 4)
davescriven's avatar
davescriven committed
150
                            {
151
152
153
154
155
156
157
158
159
                                var lastCubicCurve = segments.Last as SvgCubicCurveSegment;

                                var controlPoint = lastCubicCurve != null
                                    ? Reflect(lastCubicCurve.SecondControlPoint, segments.Last.End)
                                    : segments.Last.End;

                                segments.Add(new SvgCubicCurveSegment(segments.Last.End, controlPoint,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative),
                                    ToAbsolute(coords[i + 2], coords[i + 3], segments, isRelative)));
davescriven's avatar
davescriven committed
160
161
162
163
164
165
166
167
168
169
170
                            }
                            break;
                        case 'Z': // closepath
                        case 'z': // relative closepath
                            segments.Add(new SvgClosePathSegment());
                            break;
                    }
        }

        private static PointF Reflect(PointF point, PointF mirror)
        {
Matt Bowers's avatar
Matt Bowers committed
171
172
173
            float x, y, dx, dy;
            dx = Math.Abs(mirror.X - point.X);
            dy = Math.Abs(mirror.Y - point.Y);
davescriven's avatar
davescriven committed
174

Matt Bowers's avatar
Matt Bowers committed
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
            if (mirror.X >= point.X)
            {
                x = mirror.X + dx;
            }
            else
            {
                x = mirror.X - dx;
            }
            if (mirror.Y >= point.Y)
            {
                y = mirror.Y + dy;
            }
            else
            {
                y = mirror.Y - dy;
            }

            return new PointF(x, y);
davescriven's avatar
davescriven committed
193
194
        }

195
196
197
198
199
200
201
202
203
        /// <summary>
        /// Creates point with absolute coorindates.
        /// </summary>
        /// <param name="x">Raw X-coordinate value.</param>
        /// <param name="y">Raw Y-coordinate value.</param>
        /// <param name="segments">Current path segments.</param>
        /// <param name="isRelativeBoth"><b>true</b> if <paramref name="x"/> and <paramref name="y"/> contains relative coordinate values, otherwise <b>false</b>.</param>
        /// <returns><see cref="PointF"/> that contains absolute coordinates.</returns>
        private static PointF ToAbsolute(float x, float y, SvgPathSegmentList segments, bool isRelativeBoth)
davescriven's avatar
davescriven committed
204
        {
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
            return ToAbsolute(x, y, segments, isRelativeBoth, isRelativeBoth);
        }

        /// <summary>
        /// Creates point with absolute coorindates.
        /// </summary>
        /// <param name="x">Raw X-coordinate value.</param>
        /// <param name="y">Raw Y-coordinate value.</param>
        /// <param name="segments">Current path segments.</param>
        /// <param name="isRelativeX"><b>true</b> if <paramref name="x"/> contains relative coordinate value, otherwise <b>false</b>.</param>
        /// <param name="isRelativeY"><b>true</b> if <paramref name="y"/> contains relative coordinate value, otherwise <b>false</b>.</param>
        /// <returns><see cref="PointF"/> that contains absolute coordinates.</returns>
        private static PointF ToAbsolute(float x, float y, SvgPathSegmentList segments, bool isRelativeX, bool isRelativeY)
        {
            var point = new PointF(x, y);

            if ((isRelativeX || isRelativeY) && segments.Count > 0)
            {
                var lastSegment = segments.Last;

Eric Domke's avatar
Eric Domke committed
225
                // if the last element is a SvgClosePathSegment the position of the previous move to should be used because the position of SvgClosePathSegment is 0,0
226
                if (lastSegment is SvgClosePathSegment)
Eric Domke's avatar
Eric Domke committed
227
                    lastSegment = segments.OfType<SvgMoveToSegment>().Last();
228

229
                if (isRelativeX)
230
                {
231
                    point.X += lastSegment.End.X;
232
                }
233
234

                if (isRelativeY)
235
                {
236
                    point.Y += lastSegment.End.Y;
237
                }
238
239
240
            }

            return point;
davescriven's avatar
davescriven committed
241
242
243
244
        }

        private static IEnumerable<string> SplitCommands(string path)
        {
245
            var commandStart = 0;
davescriven's avatar
davescriven committed
246

247
            for (var i = 0; i < path.Length; i++)
davescriven's avatar
davescriven committed
248
            {
249
                string command;
250
				if (char.IsLetter(path[i]) && path[i] != 'e') //e is used in scientific notiation. but not svg path
davescriven's avatar
davescriven committed
251
252
253
254
255
                {
                    command = path.Substring(commandStart, i - commandStart).Trim();
                    commandStart = i;

                    if (!string.IsNullOrEmpty(command))
256
                    {
davescriven's avatar
davescriven committed
257
                        yield return command;
258
                    }
davescriven's avatar
davescriven committed
259
260

                    if (path.Length == i + 1)
261
                    {
davescriven's avatar
davescriven committed
262
                        yield return path[i].ToString();
263
                    }
davescriven's avatar
davescriven committed
264
265
266
267
268
269
                }
                else if (path.Length == i + 1)
                {
                    command = path.Substring(commandStart, i - commandStart + 1).Trim();

                    if (!string.IsNullOrEmpty(command))
270
                    {
davescriven's avatar
davescriven committed
271
                        yield return command;
272
                    }
davescriven's avatar
davescriven committed
273
274
275
276
                }
            }
        }

277
        private static IEnumerable<float> ParseCoordinates(string coords)
davescriven's avatar
davescriven committed
278
        {
279
            var parts = Regex.Split(coords.Remove(0, 1), @"[\s,]|(?=(?<!e)-)", RegexOptions.Compiled);
davescriven's avatar
davescriven committed
280

Tebjan Halm's avatar
Tebjan Halm committed
281
            for (int i = 0; i < parts.Length; i++)
282
            {
Tebjan Halm's avatar
Tebjan Halm committed
283
284
285
                if (!String.IsNullOrEmpty(parts[i]))
                    yield return float.Parse(parts[i].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);

286
            }
davescriven's avatar
davescriven committed
287
288
        }

289
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
davescriven's avatar
davescriven committed
290
291
        {
            if (value is string)
292
            {
Eric Domke's avatar
Eric Domke committed
293
                if (string.IsNullOrEmpty((string)value)) return new SvgPathSegmentList();
294
                return Parse((string)value);
295
            }
davescriven's avatar
davescriven committed
296
297

            return base.ConvertFrom(context, culture, value);
Tebjan Halm's avatar
Tebjan Halm committed
298
299
300
301
302
303
304
305
306
307
        }
        
		public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
		{
			if (destinationType == typeof(string))
            {
                var paths = value as SvgPathSegmentList;

                if (paths != null)
                {
308
309
310
311
312
                	var curretCulture = CultureInfo.CurrentCulture;
                	Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
                	var s = string.Join(" ", paths.Select(p => p.ToString()).ToArray());
                	Thread.CurrentThread.CurrentCulture = curretCulture;
                    return s;
Tebjan Halm's avatar
Tebjan Halm committed
313
314
315
316
317
318
319
320
321
322
323
324
325
326
                }
            }

			return base.ConvertTo(context, culture, value, destinationType);
		}
        
		public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                return true;
            }

            return base.CanConvertTo(context, destinationType);
davescriven's avatar
davescriven committed
327
328
329
        }
    }
}