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 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; try { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; var s = string.Join(" ", paths.Select(p => p.ToString()).ToArray()); return s; } finally { // Make sure to set back the old culture even an error occurred. Thread.CurrentThread.CurrentCulture = curretCulture; } } } 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); } } }