using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using Svg.Pathing; namespace Svg { public static class PointFExtensions { public static string ToSvgString(this PointF p) { return p.X.ToString() + " " + p.Y.ToString(); } } public class SvgPathBuilder : TypeConverter { /// /// Parses the specified string into a collection of path segments. /// /// A containing path data. public static SvgPathSegmentList Parse(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } var segments = new SvgPathSegmentList(); try { char command; bool isRelative; foreach (var commandSet in SplitCommands(path.TrimEnd(null))) { command = commandSet[0]; isRelative = char.IsLower(command); // http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation CreatePathSegment(command, segments, new CoordinateParser(commandSet.Trim()), isRelative); } } catch (Exception exc) { Trace.TraceError("Error parsing path \"{0}\": {1}", path, exc.Message); } return segments; } private static void CreatePathSegment(char command, SvgPathSegmentList segments, CoordinateParser parser, bool isRelative) { var coords = new float[6]; switch (command) { case 'm': // relative moveto case 'M': // moveto if (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1])) { segments.Add(new SvgMoveToSegment(ToAbsolute(coords[0], coords[1], segments, isRelative))); } while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1])) { segments.Add(new SvgLineSegment(segments.Last.End, ToAbsolute(coords[0], coords[1], segments, isRelative))); } break; case 'a': case 'A': bool size; bool sweep; while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1]) && parser.TryGetFloat(out coords[2]) && parser.TryGetBool(out size) && parser.TryGetBool(out sweep) && parser.TryGetFloat(out coords[3]) && parser.TryGetFloat(out coords[4])) { // A|a rx ry x-axis-rotation large-arc-flag sweep-flag x y segments.Add(new SvgArcSegment(segments.Last.End, coords[0], coords[1], coords[2], (size ? SvgArcSize.Large : SvgArcSize.Small), (sweep ? SvgArcSweep.Positive : SvgArcSweep.Negative), ToAbsolute(coords[3], coords[4], segments, isRelative))); } break; case 'l': // relative lineto case 'L': // lineto while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1])) { segments.Add(new SvgLineSegment(segments.Last.End, ToAbsolute(coords[0], coords[1], segments, isRelative))); } break; case 'H': // horizontal lineto case 'h': // relative horizontal lineto while (parser.TryGetFloat(out coords[0])) { segments.Add(new SvgLineSegment(segments.Last.End, ToAbsolute(coords[0], segments.Last.End.Y, segments, isRelative, false))); } break; case 'V': // vertical lineto case 'v': // relative vertical lineto while (parser.TryGetFloat(out coords[0])) { segments.Add(new SvgLineSegment(segments.Last.End, ToAbsolute(segments.Last.End.X, coords[0], segments, false, isRelative))); } break; case 'Q': // curveto case 'q': // relative curveto while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1]) && parser.TryGetFloat(out coords[2]) && parser.TryGetFloat(out coords[3])) { segments.Add(new SvgQuadraticCurveSegment(segments.Last.End, ToAbsolute(coords[0], coords[1], segments, isRelative), ToAbsolute(coords[2], coords[3], segments, isRelative))); } break; case 'T': // shorthand/smooth curveto case 't': // relative shorthand/smooth curveto while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1])) { var lastQuadCurve = segments.Last as SvgQuadraticCurveSegment; var controlPoint = lastQuadCurve != null ? Reflect(lastQuadCurve.ControlPoint, segments.Last.End) : segments.Last.End; segments.Add(new SvgQuadraticCurveSegment(segments.Last.End, controlPoint, ToAbsolute(coords[0], coords[1], segments, isRelative))); } break; case 'C': // curveto case 'c': // relative curveto while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1]) && parser.TryGetFloat(out coords[2]) && parser.TryGetFloat(out coords[3]) && parser.TryGetFloat(out coords[4]) && parser.TryGetFloat(out coords[5])) { segments.Add(new SvgCubicCurveSegment(segments.Last.End, ToAbsolute(coords[0], coords[1], segments, isRelative), ToAbsolute(coords[2], coords[3], segments, isRelative), ToAbsolute(coords[4], coords[5], segments, isRelative))); } break; case 'S': // shorthand/smooth curveto case 's': // relative shorthand/smooth curveto while (parser.TryGetFloat(out coords[0]) && parser.TryGetFloat(out coords[1]) && parser.TryGetFloat(out coords[2]) && parser.TryGetFloat(out coords[3])) { 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[0], coords[1], segments, isRelative), ToAbsolute(coords[2], coords[3], segments, isRelative))); } break; case 'Z': // closepath case 'z': // relative closepath segments.Add(new SvgClosePathSegment()); break; } } private static PointF Reflect(PointF point, PointF mirror) { float x, y, dx, dy; dx = Math.Abs(mirror.X - point.X); dy = Math.Abs(mirror.Y - point.Y); 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); } /// /// Creates point with absolute coorindates. /// /// Raw X-coordinate value. /// Raw Y-coordinate value. /// Current path segments. /// true if and contains relative coordinate values, otherwise false. /// that contains absolute coordinates. private static PointF ToAbsolute(float x, float y, SvgPathSegmentList segments, bool isRelativeBoth) { return ToAbsolute(x, y, segments, isRelativeBoth, isRelativeBoth); } /// /// Creates point with absolute coorindates. /// /// Raw X-coordinate value. /// Raw Y-coordinate value. /// Current path segments. /// true if contains relative coordinate value, otherwise false. /// true if contains relative coordinate value, otherwise false. /// that contains absolute coordinates. 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 the last element is a SvgClosePathSegment the position of the previous element should be used because the position of SvgClosePathSegment is 0,0 if (lastSegment is SvgClosePathSegment) lastSegment = segments.Reverse().OfType().First(); if (isRelativeX) { point.X += lastSegment.End.X; } if (isRelativeY) { point.Y += lastSegment.End.Y; } } return point; } private static IEnumerable SplitCommands(string path) { var commandStart = 0; for (var i = 0; i < path.Length; i++) { string command; if (char.IsLetter(path[i]) && path[i] != 'e') //e is used in scientific notiation. but not svg path { command = path.Substring(commandStart, i - commandStart).Trim(); commandStart = i; if (!string.IsNullOrEmpty(command)) { yield return command; } if (path.Length == i + 1) { yield return path[i].ToString(); } } else if (path.Length == i + 1) { command = path.Substring(commandStart, i - commandStart + 1).Trim(); if (!string.IsNullOrEmpty(command)) { yield return command; } } } } private enum NumState { invalid, separator, prefix, integer, decPlace, fraction, exponent, expPrefix, expValue } private class CoordinateParser { private string _coords; private int _pos = 0; private NumState _currState = NumState.separator; private NumState _newState = NumState.separator; private int i = 1; private bool _parseWorked = true; public CoordinateParser(string coords) { _coords = coords; } public bool HasMore { get { return _parseWorked; } } private bool MarkState(bool state) { _parseWorked = state; i++; return state; } public bool TryGetBool(out bool result) { while (i < _coords.Length && _parseWorked) { switch (_currState) { case NumState.separator: if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else if (_coords[i] == '0') { result = false; _newState = NumState.separator; _pos = i + 1; return MarkState(true); } else if (_coords[i] == '1') { result = true; _newState = NumState.separator; _pos = i + 1; return MarkState(true); } else { result = false; return MarkState(false); } break; default: result = false; return MarkState(false); } i++; } result = false; return MarkState(false); } public bool TryGetFloat(out float result) { while (i < _coords.Length && _parseWorked) { switch (_currState) { case NumState.separator: if (char.IsNumber(_coords[i])) { _newState = NumState.integer; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else { switch (_coords[i]) { case '.': _newState = NumState.decPlace; break; case '+': case '-': _newState = NumState.prefix; break; default: _newState = NumState.invalid; break; } } break; case NumState.prefix: if (char.IsNumber(_coords[i])) { _newState = NumState.integer; } else if (_coords[i] == '.') { _newState = NumState.decPlace; } else { _newState = NumState.invalid; } break; case NumState.integer: if (char.IsNumber(_coords[i])) { _newState = NumState.integer; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else { switch (_coords[i]) { case '.': _newState = NumState.decPlace; break; case 'e': _newState = NumState.exponent; break; case '+': case '-': _newState = NumState.prefix; break; default: _newState = NumState.invalid; break; } } break; case NumState.decPlace: if (char.IsNumber(_coords[i])) { _newState = NumState.fraction; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else { switch (_coords[i]) { case 'e': _newState = NumState.exponent; break; case '+': case '-': _newState = NumState.prefix; break; default: _newState = NumState.invalid; break; } } break; case NumState.fraction: if (char.IsNumber(_coords[i])) { _newState = NumState.fraction; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else { switch (_coords[i]) { case '.': _newState = NumState.decPlace; break; case 'e': _newState = NumState.exponent; break; case '+': case '-': _newState = NumState.prefix; break; default: _newState = NumState.invalid; break; } } break; case NumState.exponent: if (char.IsNumber(_coords[i])) { _newState = NumState.expValue; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.invalid; } else { switch (_coords[i]) { case '+': case '-': _newState = NumState.expPrefix; break; default: _newState = NumState.invalid; break; } } break; case NumState.expPrefix: if (char.IsNumber(_coords[i])) { _newState = NumState.expValue; } else { _newState = NumState.invalid; } break; case NumState.expValue: if (char.IsNumber(_coords[i])) { _newState = NumState.expValue; } else if (IsCoordSeparator(_coords[i])) { _newState = NumState.separator; } else { switch (_coords[i]) { case '.': _newState = NumState.decPlace; break; case '+': case '-': _newState = NumState.prefix; break; default: _newState = NumState.invalid; break; } } break; } if (_newState < _currState) { result = float.Parse(_coords.Substring(_pos, i - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); _pos = i; _currState = _newState; return MarkState(true); } else if (_newState != _currState && _currState == NumState.separator) { _pos = i; } if (_newState == NumState.invalid) { result = float.MinValue; return MarkState(false); } _currState = _newState; i++; } if (_currState == NumState.separator || !_parseWorked || _pos >= _coords.Length) { result = float.MinValue; return MarkState(false); } else { result = float.Parse(_coords.Substring(_pos, _coords.Length - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); _pos = _coords.Length; return MarkState(true); } } private static bool IsCoordSeparator(char value) { switch (value) { case ' ': case '\t': case '\n': case '\r': case ',': return true; } return false; } } //private static IEnumerable ParseCoordinates(string coords) //{ // if (string.IsNullOrEmpty(coords) || coords.Length < 2) yield break; // var pos = 0; // var currState = NumState.separator; // var newState = NumState.separator; // for (int i = 1; i < coords.Length; i++) // { // switch (currState) // { // case NumState.separator: // if (char.IsNumber(coords[i])) // { // newState = NumState.integer; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.separator; // } // else // { // switch (coords[i]) // { // case '.': // newState = NumState.decPlace; // break; // case '+': // case '-': // newState = NumState.prefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // case NumState.prefix: // if (char.IsNumber(coords[i])) // { // newState = NumState.integer; // } // else if (coords[i] == '.') // { // newState = NumState.decPlace; // } // else // { // newState = NumState.invalid; // } // break; // case NumState.integer: // if (char.IsNumber(coords[i])) // { // newState = NumState.integer; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.separator; // } // else // { // switch (coords[i]) // { // case '.': // newState = NumState.decPlace; // break; // case 'e': // newState = NumState.exponent; // break; // case '+': // case '-': // newState = NumState.prefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // case NumState.decPlace: // if (char.IsNumber(coords[i])) // { // newState = NumState.fraction; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.separator; // } // else // { // switch (coords[i]) // { // case 'e': // newState = NumState.exponent; // break; // case '+': // case '-': // newState = NumState.prefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // case NumState.fraction: // if (char.IsNumber(coords[i])) // { // newState = NumState.fraction; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.separator; // } // else // { // switch (coords[i]) // { // case '.': // newState = NumState.decPlace; // break; // case 'e': // newState = NumState.exponent; // break; // case '+': // case '-': // newState = NumState.prefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // case NumState.exponent: // if (char.IsNumber(coords[i])) // { // newState = NumState.expValue; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.invalid; // } // else // { // switch (coords[i]) // { // case '+': // case '-': // newState = NumState.expPrefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // case NumState.expPrefix: // if (char.IsNumber(coords[i])) // { // newState = NumState.expValue; // } // else // { // newState = NumState.invalid; // } // break; // case NumState.expValue: // if (char.IsNumber(coords[i])) // { // newState = NumState.expValue; // } // else if (IsCoordSeparator(coords[i])) // { // newState = NumState.separator; // } // else // { // switch (coords[i]) // { // case '.': // newState = NumState.decPlace; // break; // case '+': // case '-': // newState = NumState.prefix; // break; // default: // newState = NumState.invalid; // break; // } // } // break; // } // if (newState < currState) // { // yield return float.Parse(coords.Substring(pos, i - pos), NumberStyles.Float, CultureInfo.InvariantCulture); // pos = i; // } // else if (newState != currState && currState == NumState.separator) // { // pos = i; // } // if (newState == NumState.invalid) yield break; // currState = newState; // } // if (currState != NumState.separator) // { // yield return float.Parse(coords.Substring(pos, coords.Length - pos), NumberStyles.Float, CultureInfo.InvariantCulture); // } //} public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { return Parse((string)value); } return base.ConvertFrom(context, culture, value); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { var paths = value as SvgPathSegmentList; if (paths != null) { 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; } } 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); } } }