SvgPathBuilder.cs 11.5 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;
davescriven's avatar
davescriven committed
7
8
9
10
11
12
13

using Svg.Pathing;

namespace Svg
{
    internal class SvgPathBuilder : TypeConverter
    {
14
15
16
17
        /// <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
18
19
20
        public static SvgPathSegmentList Parse(string path)
        {
            if (string.IsNullOrEmpty(path))
21
            {
davescriven's avatar
davescriven committed
22
                throw new ArgumentNullException("path");
23
            }
davescriven's avatar
davescriven committed
24

25
            var segments = new SvgPathSegmentList();
davescriven's avatar
davescriven committed
26
27
28

            try
            {
29
30
31
32
                List<float> coords;
                char command;
                bool isRelative;

33
                foreach (var commandSet in SplitCommands(path.TrimEnd(null)))
davescriven's avatar
davescriven committed
34
                {
35
                    coords = new List<float>(ParseCoordinates(commandSet.Trim()));
36
37
                    command = commandSet[0];
                    isRelative = char.IsLower(command);
davescriven's avatar
davescriven committed
38
39
40
41
42
43
                    // http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation

                    switch (command)
                    {
                        case 'm': // relative moveto
                        case 'M': // moveto
44
45
46
47
                            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
48
                            {
49
50
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
51
52
53
54
                            }
                            break;
                        case 'a':
                        case 'A':
55
56
57
58
59
                            SvgArcSize size;
                            SvgArcSweep sweep;

                            for (var i = 0; i < coords.Count; i += 7)
                            {
60
61
                                size = (coords[i + 3] != 0.0f) ? SvgArcSize.Large : SvgArcSize.Small;
                                sweep = (coords[i + 4] != 0.0f) ? SvgArcSweep.Positive : SvgArcSweep.Negative;
62
63
64
65
66
67

                                // 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
68
69
                        case 'l': // relative lineto
                        case 'L': // lineto
70
                            for (var i = 0; i < coords.Count; i += 2)
davescriven's avatar
davescriven committed
71
                            {
72
73
                                segments.Add(new SvgLineSegment(segments.Last.End,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
74
75
76
77
                            }
                            break;
                        case 'H': // horizontal lineto
                        case 'h': // relative horizontal lineto
78
79
80
                            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
81
82
83
                            break;
                        case 'V': // vertical lineto
                        case 'v': // relative vertical lineto
84
85
86
                            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
87
88
89
                            break;
                        case 'Q': // curveto
                        case 'q': // relative curveto
90
                            for (var i = 0; i < coords.Count; i += 4)
davescriven's avatar
davescriven committed
91
                            {
92
93
94
                                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
95
96
97
98
                            }
                            break;
                        case 'T': // shorthand/smooth curveto
                        case 't': // relative shorthand/smooth curveto
99
                            for (var i = 0; i < coords.Count; i += 2)
davescriven's avatar
davescriven committed
100
                            {
101
                                var lastQuadCurve = segments.Last as SvgQuadraticCurveSegment;
davescriven's avatar
davescriven committed
102

103
104
105
                                var controlPoint = lastQuadCurve != null
                                    ? Reflect(lastQuadCurve.ControlPoint, segments.Last.End)
                                    : segments.Last.End;
davescriven's avatar
davescriven committed
106

107
108
                                segments.Add(new SvgQuadraticCurveSegment(segments.Last.End, controlPoint,
                                    ToAbsolute(coords[i], coords[i + 1], segments, isRelative)));
davescriven's avatar
davescriven committed
109
110
111
112
                            }
                            break;
                        case 'C': // curveto
                        case 'c': // relative curveto
113
                            for (var i = 0; i < coords.Count; i += 6)
davescriven's avatar
davescriven committed
114
                            {
115
116
117
118
                                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
119
120
121
122
123
                            }
                            break;
                        case 'S': // shorthand/smooth curveto
                        case 's': // relative shorthand/smooth curveto

124
                            for (var i = 0; i < coords.Count; i += 4)
davescriven's avatar
davescriven committed
125
                            {
126
127
128
129
130
131
132
133
134
                                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
135
136
137
138
139
140
141
142
143
                            }
                            break;
                        case 'Z': // closepath
                        case 'z': // relative closepath
                            segments.Add(new SvgClosePathSegment());
                            break;
                    }
                }
            }
144
            catch (Exception exc)
davescriven's avatar
davescriven committed
145
            {
146
                Trace.TraceError("Error parsing path \"{0}\": {1}", path, exc.Message);
davescriven's avatar
davescriven committed
147
148
149
150
151
152
153
154
            }

            return segments;
        }

        private static PointF Reflect(PointF point, PointF mirror)
        {
            // TODO: Only works left to right???
155
156
            var x = mirror.X + (mirror.X - point.X);
            var y = mirror.Y + (mirror.Y - point.Y);
davescriven's avatar
davescriven committed
157
158
159
160

            return new PointF(Math.Abs(x), Math.Abs(y));
        }

161
162
163
164
165
166
167
168
169
        /// <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
170
        {
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
            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)
192
                {
193
                    point.X += lastSegment.End.X;
194
                }
195
196

                if (isRelativeY)
197
                {
198
                    point.Y += lastSegment.End.Y;
199
                }
200
201
202
            }

            return point;
davescriven's avatar
davescriven committed
203
204
205
206
        }

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

209
            for (var i = 0; i < path.Length; i++)
davescriven's avatar
davescriven committed
210
            {
211
                string command;
davescriven's avatar
davescriven committed
212
213
214
215
216
217
                if (char.IsLetter(path[i]))
                {
                    command = path.Substring(commandStart, i - commandStart).Trim();
                    commandStart = i;

                    if (!string.IsNullOrEmpty(command))
218
                    {
davescriven's avatar
davescriven committed
219
                        yield return command;
220
                    }
davescriven's avatar
davescriven committed
221
222

                    if (path.Length == i + 1)
223
                    {
davescriven's avatar
davescriven committed
224
                        yield return path[i].ToString();
225
                    }
davescriven's avatar
davescriven committed
226
227
228
229
230
231
                }
                else if (path.Length == i + 1)
                {
                    command = path.Substring(commandStart, i - commandStart + 1).Trim();

                    if (!string.IsNullOrEmpty(command))
232
                    {
davescriven's avatar
davescriven committed
233
                        yield return command;
234
                    }
davescriven's avatar
davescriven committed
235
236
237
238
                }
            }
        }

239
        private static IEnumerable<float> ParseCoordinates(string coords)
davescriven's avatar
davescriven committed
240
241
        {
            // TODO: Handle "1-1" (new PointF(1, -1);
242
            var parts = coords.Remove(0, 1).Replace("-", " -").Split(new[] { ',', ' ', '\r', '\n' },
243
                StringSplitOptions.RemoveEmptyEntries);
davescriven's avatar
davescriven committed
244

245
            for (var i = 0; i < parts.Length; i++)
246
247
248
            {
                yield return float.Parse(parts[i].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
            }
davescriven's avatar
davescriven committed
249
250
        }

251
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
davescriven's avatar
davescriven committed
252
253
        {
            if (value is string)
254
            {
255
                return Parse((string)value);
256
            }
davescriven's avatar
davescriven committed
257
258
259
260
261

            return base.ConvertFrom(context, culture, value);
        }
    }
}