SvgPathBuilder.cs 13.3 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
225
            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;

                if (isRelativeX)
226
                {
227
                    point.X += lastSegment.End.X;
228
                }
229
230

                if (isRelativeY)
231
                {
232
                    point.Y += lastSegment.End.Y;
233
                }
234
235
236
            }

            return point;
davescriven's avatar
davescriven committed
237
238
239
240
        }

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

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

                    if (!string.IsNullOrEmpty(command))
252
                    {
davescriven's avatar
davescriven committed
253
                        yield return command;
254
                    }
davescriven's avatar
davescriven committed
255
256

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

                    if (!string.IsNullOrEmpty(command))
266
                    {
davescriven's avatar
davescriven committed
267
                        yield return command;
268
                    }
davescriven's avatar
davescriven committed
269
270
271
272
                }
            }
        }

273
        private static IEnumerable<float> ParseCoordinates(string coords)
davescriven's avatar
davescriven committed
274
        {
Tebjan Halm's avatar
Tebjan Halm committed
275
            var parts = Regex.Split(coords.Remove(0, 1), @"[\s,]|(?=(?<!e)-)");
davescriven's avatar
davescriven committed
276

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

282
            }
davescriven's avatar
davescriven committed
283
284
        }

285
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
davescriven's avatar
davescriven committed
286
287
        {
            if (value is string)
288
            {
289
                return Parse((string)value);
290
            }
davescriven's avatar
davescriven committed
291
292

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

                if (paths != null)
                {
303
304
305
306
307
                	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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
                }
            }

			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
322
323
324
        }
    }
}