|
|
|
@ -5,50 +5,46 @@ using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Globalization; |
|
|
|
using System.IO; |
|
|
|
using System.Linq; |
|
|
|
using System.Text; |
|
|
|
using System.Text.RegularExpressions; |
|
|
|
|
|
|
|
namespace Avalonia.Media |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Parses a path markup string.
|
|
|
|
/// </summary>
|
|
|
|
public class PathMarkupParser |
|
|
|
public class PathMarkupParser : IDisposable |
|
|
|
{ |
|
|
|
private static readonly Dictionary<char, Command> Commands = new Dictionary<char, Command> |
|
|
|
{ |
|
|
|
{ 'F', Command.FillRule }, |
|
|
|
{ 'M', Command.Move }, |
|
|
|
{ 'L', Command.Line }, |
|
|
|
{ 'H', Command.HorizontalLine }, |
|
|
|
{ 'V', Command.VerticalLine }, |
|
|
|
{ 'Q', Command.QuadraticBezierCurve }, |
|
|
|
{ 'T', Command.SmoothQuadraticBezierCurve }, |
|
|
|
{ 'C', Command.CubicBezierCurve }, |
|
|
|
{ 'S', Command.SmoothCubicBezierCurve }, |
|
|
|
{ 'A', Command.Arc }, |
|
|
|
{ 'Z', Command.Close }, |
|
|
|
}; |
|
|
|
|
|
|
|
private static readonly Dictionary<char, FillRule> FillRules = new Dictionary<char, FillRule> |
|
|
|
{ |
|
|
|
{'0', FillRule.EvenOdd }, |
|
|
|
{'1', FillRule.NonZero } |
|
|
|
}; |
|
|
|
private static readonly string s_separatorPattern; |
|
|
|
|
|
|
|
private readonly StreamGeometryContext _context; |
|
|
|
private Point _currentPoint; |
|
|
|
private Point? _previousControlPoint; |
|
|
|
private PathGeometry _currentGeometry; |
|
|
|
private PathFigure _currentFigure; |
|
|
|
private bool _isDisposed; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="context">The context for the geometry.</param>
|
|
|
|
public PathMarkupParser(StreamGeometryContext context) |
|
|
|
private static readonly Dictionary<char, Command> s_commands = |
|
|
|
new Dictionary<char, Command> |
|
|
|
{ |
|
|
|
{ 'F', Command.FillRule }, |
|
|
|
{ 'M', Command.Move }, |
|
|
|
{ 'L', Command.Line }, |
|
|
|
{ 'H', Command.HorizontalLine }, |
|
|
|
{ 'V', Command.VerticalLine }, |
|
|
|
{ 'Q', Command.QuadraticBezierCurve }, |
|
|
|
{ 'T', Command.SmoothQuadraticBezierCurve }, |
|
|
|
{ 'C', Command.CubicBezierCurve }, |
|
|
|
{ 'S', Command.SmoothCubicBezierCurve }, |
|
|
|
{ 'A', Command.Arc }, |
|
|
|
{ 'Z', Command.Close }, |
|
|
|
}; |
|
|
|
|
|
|
|
static PathMarkupParser() |
|
|
|
{ |
|
|
|
_context = context; |
|
|
|
s_separatorPattern = CreatesSeparatorPattern(); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the command currently being processed.
|
|
|
|
/// </summary>
|
|
|
|
private enum Command |
|
|
|
{ |
|
|
|
None, |
|
|
|
@ -62,358 +58,581 @@ namespace Avalonia.Media |
|
|
|
SmoothCubicBezierCurve, |
|
|
|
SmoothQuadraticBezierCurve, |
|
|
|
Arc, |
|
|
|
Close, |
|
|
|
Close |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Parses the specified markup string.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="s">The markup string.</param>
|
|
|
|
public void Parse(string s) |
|
|
|
public PathGeometry Parse(string s) |
|
|
|
{ |
|
|
|
bool openFigure = false; |
|
|
|
_currentGeometry = new PathGeometry(); |
|
|
|
|
|
|
|
var tokens = ParseTokens(s); |
|
|
|
|
|
|
|
using (StringReader reader = new StringReader(s)) |
|
|
|
return CreateGeometry(tokens); |
|
|
|
} |
|
|
|
|
|
|
|
void IDisposable.Dispose() |
|
|
|
{ |
|
|
|
Dispose(true); |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing) |
|
|
|
{ |
|
|
|
if (_isDisposed) |
|
|
|
{ |
|
|
|
Command command = Command.None; |
|
|
|
Point point = new Point(); |
|
|
|
bool relative = false; |
|
|
|
Point? previousControlPoint = null; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (disposing) |
|
|
|
{ |
|
|
|
_currentFigure = null; |
|
|
|
|
|
|
|
_currentGeometry = null; |
|
|
|
} |
|
|
|
|
|
|
|
_isDisposed = true; |
|
|
|
} |
|
|
|
|
|
|
|
private static string CreatesSeparatorPattern() |
|
|
|
{ |
|
|
|
var stringBuilder = new StringBuilder(); |
|
|
|
|
|
|
|
foreach (var command in s_commands.Keys) |
|
|
|
{ |
|
|
|
stringBuilder.Append(command); |
|
|
|
|
|
|
|
stringBuilder.Append(char.ToLower(command)); |
|
|
|
} |
|
|
|
|
|
|
|
return @"(?=[" + stringBuilder + "])"; |
|
|
|
} |
|
|
|
|
|
|
|
private static IEnumerable<CommandToken> ParseTokens(string s) |
|
|
|
{ |
|
|
|
return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); |
|
|
|
} |
|
|
|
|
|
|
|
private static Point MirrorControlPoint(Point controlPoint, Point center) |
|
|
|
{ |
|
|
|
var dir = controlPoint - center; |
|
|
|
|
|
|
|
return center + -dir; |
|
|
|
} |
|
|
|
|
|
|
|
while (ReadCommand(reader, ref command, ref relative)) |
|
|
|
private PathGeometry CreateGeometry(IEnumerable<CommandToken> commandTokens) |
|
|
|
{ |
|
|
|
_currentGeometry = new PathGeometry(); |
|
|
|
|
|
|
|
_currentPoint = new Point(); |
|
|
|
|
|
|
|
foreach (var commandToken in commandTokens) |
|
|
|
{ |
|
|
|
try |
|
|
|
{ |
|
|
|
switch (command) |
|
|
|
while (true) |
|
|
|
{ |
|
|
|
case Command.FillRule: |
|
|
|
_context.SetFillRule(ReadFillRule(reader)); |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
|
|
|
|
case Command.Move: |
|
|
|
if (openFigure) |
|
|
|
{ |
|
|
|
_context.EndFigure(false); |
|
|
|
} |
|
|
|
|
|
|
|
point = ReadPoint(reader, point, relative); |
|
|
|
_context.BeginFigure(point, true); |
|
|
|
openFigure = true; |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
|
|
|
|
case Command.Line: |
|
|
|
point = ReadPoint(reader, point, relative); |
|
|
|
_context.LineTo(point); |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
|
|
|
|
case Command.HorizontalLine: |
|
|
|
if (!relative) |
|
|
|
{ |
|
|
|
point = point.WithX(ReadDouble(reader)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
point = new Point(point.X + ReadDouble(reader), point.Y); |
|
|
|
} |
|
|
|
|
|
|
|
_context.LineTo(point); |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
|
|
|
|
case Command.VerticalLine: |
|
|
|
if (!relative) |
|
|
|
{ |
|
|
|
point = point.WithY(ReadDouble(reader)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
point = new Point(point.X, point.Y + ReadDouble(reader)); |
|
|
|
} |
|
|
|
|
|
|
|
_context.LineTo(point); |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
|
|
|
|
case Command.QuadraticBezierCurve: |
|
|
|
{ |
|
|
|
Point handle = ReadPoint(reader, point, relative); |
|
|
|
previousControlPoint = handle; |
|
|
|
ReadSeparator(reader); |
|
|
|
point = ReadPoint(reader, point, relative); |
|
|
|
_context.QuadraticBezierTo(handle, point); |
|
|
|
switch (commandToken.Command) |
|
|
|
{ |
|
|
|
case Command.None: |
|
|
|
break; |
|
|
|
case Command.FillRule: |
|
|
|
SetFillRule(commandToken); |
|
|
|
break; |
|
|
|
case Command.Move: |
|
|
|
AddMove(commandToken); |
|
|
|
break; |
|
|
|
case Command.Line: |
|
|
|
AddLine(commandToken); |
|
|
|
break; |
|
|
|
case Command.HorizontalLine: |
|
|
|
AddHorizontalLine(commandToken); |
|
|
|
break; |
|
|
|
case Command.VerticalLine: |
|
|
|
AddVerticalLine(commandToken); |
|
|
|
break; |
|
|
|
case Command.CubicBezierCurve: |
|
|
|
AddCubicBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
case Command.QuadraticBezierCurve: |
|
|
|
AddQuadraticBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case Command.SmoothQuadraticBezierCurve: |
|
|
|
{ |
|
|
|
Point end = ReadPoint(reader, point, relative); |
|
|
|
|
|
|
|
if(previousControlPoint != null) |
|
|
|
previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); |
|
|
|
|
|
|
|
_context.QuadraticBezierTo(previousControlPoint ?? point, end); |
|
|
|
point = end; |
|
|
|
case Command.SmoothCubicBezierCurve: |
|
|
|
AddSmoothCubicBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case Command.CubicBezierCurve: |
|
|
|
{ |
|
|
|
Point point1 = ReadPoint(reader, point, relative); |
|
|
|
ReadSeparator(reader); |
|
|
|
Point point2 = ReadPoint(reader, point, relative); |
|
|
|
previousControlPoint = point2; |
|
|
|
ReadSeparator(reader); |
|
|
|
point = ReadPoint(reader, point, relative); |
|
|
|
_context.CubicBezierTo(point1, point2, point); |
|
|
|
case Command.SmoothQuadraticBezierCurve: |
|
|
|
AddSmoothQuadraticBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case Command.SmoothCubicBezierCurve: |
|
|
|
{ |
|
|
|
Point point2 = ReadPoint(reader, point, relative); |
|
|
|
ReadSeparator(reader); |
|
|
|
Point end = ReadPoint(reader, point, relative); |
|
|
|
|
|
|
|
if(previousControlPoint != null) |
|
|
|
previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); |
|
|
|
|
|
|
|
_context.CubicBezierTo(previousControlPoint ?? point, point2, end); |
|
|
|
previousControlPoint = point2; |
|
|
|
point = end; |
|
|
|
case Command.Arc: |
|
|
|
AddArc(commandToken); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case Command.Arc: |
|
|
|
{ |
|
|
|
Size size = ReadSize(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
double rotationAngle = ReadDouble(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
bool isLargeArc = ReadBool(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; |
|
|
|
ReadSeparator(reader); |
|
|
|
point = ReadPoint(reader, point, relative); |
|
|
|
|
|
|
|
_context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); |
|
|
|
previousControlPoint = null; |
|
|
|
case Command.Close: |
|
|
|
CloseFigure(); |
|
|
|
break; |
|
|
|
} |
|
|
|
default: |
|
|
|
throw new NotSupportedException("Unsupported command"); |
|
|
|
} |
|
|
|
|
|
|
|
case Command.Close: |
|
|
|
_context.EndFigure(true); |
|
|
|
openFigure = false; |
|
|
|
previousControlPoint = null; |
|
|
|
break; |
|
|
|
if (commandToken.HasImplicitCommands) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
default: |
|
|
|
throw new NotSupportedException("Unsupported command"); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (openFigure) |
|
|
|
catch (InvalidDataException) |
|
|
|
{ |
|
|
|
_context.EndFigure(false); |
|
|
|
break; |
|
|
|
} |
|
|
|
catch (NotSupportedException) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return _currentGeometry; |
|
|
|
} |
|
|
|
|
|
|
|
private Point MirrorControlPoint(Point controlPoint, Point center) |
|
|
|
private void SetFillRule(CommandToken commandToken) |
|
|
|
{ |
|
|
|
Point dir = (controlPoint - center); |
|
|
|
return center + -dir; |
|
|
|
_currentGeometry.FillRule = commandToken.ReadFillRule(); |
|
|
|
} |
|
|
|
|
|
|
|
private static bool ReadCommand( |
|
|
|
StringReader reader, |
|
|
|
ref Command command, |
|
|
|
ref bool relative) |
|
|
|
private void CloseFigure() |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
if (_currentFigure != null && !_currentFigure.IsClosed) |
|
|
|
{ |
|
|
|
_currentFigure.IsClosed = true; |
|
|
|
} |
|
|
|
|
|
|
|
int i = reader.Peek(); |
|
|
|
_previousControlPoint = null; |
|
|
|
|
|
|
|
if (i == -1) |
|
|
|
_currentFigure = null; |
|
|
|
} |
|
|
|
|
|
|
|
private void CreateFigure() |
|
|
|
{ |
|
|
|
_currentFigure = new PathFigure |
|
|
|
{ |
|
|
|
return false; |
|
|
|
StartPoint = _currentPoint, |
|
|
|
IsClosed = false |
|
|
|
}; |
|
|
|
|
|
|
|
_currentGeometry.Figures.Add(_currentFigure); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddSegment(PathSegment segment) |
|
|
|
{ |
|
|
|
if (_currentFigure == null) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
_currentFigure.Segments.Add(segment); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddMove(CommandToken commandToken) |
|
|
|
{ |
|
|
|
var currentPoint = commandToken.ReadPoint(); |
|
|
|
|
|
|
|
_currentPoint = currentPoint; |
|
|
|
|
|
|
|
CreateFigure(); |
|
|
|
|
|
|
|
if (!commandToken.HasImplicitCommands) |
|
|
|
{ |
|
|
|
char c = (char)i; |
|
|
|
Command next = Command.None; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) |
|
|
|
while (commandToken.HasImplicitCommands) |
|
|
|
{ |
|
|
|
AddLine(commandToken); |
|
|
|
|
|
|
|
if (commandToken.IsRelative) |
|
|
|
{ |
|
|
|
if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && |
|
|
|
(command != Command.None)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'."); |
|
|
|
} |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
command = next; |
|
|
|
relative = char.IsLower(c); |
|
|
|
reader.Read(); |
|
|
|
return true; |
|
|
|
_currentPoint = currentPoint; |
|
|
|
|
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static FillRule ReadFillRule(StringReader reader) |
|
|
|
private void AddLine(CommandToken commandToken) |
|
|
|
{ |
|
|
|
int i = reader.Read(); |
|
|
|
if (i == -1) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid fill rule"); |
|
|
|
} |
|
|
|
char c = (char)i; |
|
|
|
FillRule rule; |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
if (!FillRules.TryGetValue(c, out rule)) |
|
|
|
var lineSegment = new LineSegment |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid fill rule"); |
|
|
|
} |
|
|
|
Point = _currentPoint |
|
|
|
}; |
|
|
|
|
|
|
|
return rule; |
|
|
|
AddSegment(lineSegment); |
|
|
|
} |
|
|
|
|
|
|
|
private static double ReadDouble(StringReader reader) |
|
|
|
private void AddHorizontalLine(CommandToken commandToken) |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) |
|
|
|
: _currentPoint.WithX(commandToken.ReadDouble()); |
|
|
|
|
|
|
|
// TODO: Handle Infinity, NaN and scientific notation.
|
|
|
|
StringBuilder b = new StringBuilder(); |
|
|
|
bool readSign = false; |
|
|
|
bool readPoint = false; |
|
|
|
bool readExponent = false; |
|
|
|
int i; |
|
|
|
var lineSegment = new LineSegment |
|
|
|
{ |
|
|
|
Point = _currentPoint |
|
|
|
}; |
|
|
|
|
|
|
|
while ((i = reader.Peek()) != -1) |
|
|
|
AddSegment(lineSegment); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddVerticalLine(CommandToken commandToken) |
|
|
|
{ |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) |
|
|
|
: _currentPoint.WithY(commandToken.ReadDouble()); |
|
|
|
|
|
|
|
var lineSegment = new LineSegment |
|
|
|
{ |
|
|
|
char c = char.ToUpperInvariant((char)i); |
|
|
|
Point = _currentPoint |
|
|
|
}; |
|
|
|
|
|
|
|
if (((c == '+' || c == '-') && !readSign) || |
|
|
|
(c == '.' && !readPoint) || |
|
|
|
(c == 'E' && !readExponent) || |
|
|
|
char.IsDigit(c)) |
|
|
|
{ |
|
|
|
if (b.Length != 0 && !readExponent && c == '-') |
|
|
|
break; |
|
|
|
|
|
|
|
b.Append(c); |
|
|
|
reader.Read(); |
|
|
|
AddSegment(lineSegment); |
|
|
|
} |
|
|
|
|
|
|
|
if (!readSign) |
|
|
|
{ |
|
|
|
readSign = c == '+' || c == '-'; |
|
|
|
} |
|
|
|
private void AddCubicBezierCurve(CommandToken commandToken) |
|
|
|
{ |
|
|
|
var point1 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
if (!readPoint) |
|
|
|
{ |
|
|
|
readPoint = c == '.'; |
|
|
|
} |
|
|
|
var point2 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
if (c == 'E') |
|
|
|
{ |
|
|
|
readSign = false; |
|
|
|
readExponent = true; |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
_previousControlPoint = point2; |
|
|
|
|
|
|
|
var point3 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
var bezierSegment = new BezierSegment |
|
|
|
{ |
|
|
|
Point1 = point1, |
|
|
|
Point2 = point2, |
|
|
|
Point3 = point3 |
|
|
|
}; |
|
|
|
|
|
|
|
AddSegment(bezierSegment); |
|
|
|
|
|
|
|
return double.Parse(b.ToString(), CultureInfo.InvariantCulture); |
|
|
|
_currentPoint = point3; |
|
|
|
} |
|
|
|
|
|
|
|
private static Point ReadPoint(StringReader reader, Point current, bool relative) |
|
|
|
private void AddQuadraticBezierCurve(CommandToken commandToken) |
|
|
|
{ |
|
|
|
if (!relative) |
|
|
|
var start = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
_previousControlPoint = start; |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
var quadraticBezierSegment = new QuadraticBezierSegment |
|
|
|
{ |
|
|
|
current = new Point(); |
|
|
|
} |
|
|
|
Point1 = start, |
|
|
|
Point2 = end |
|
|
|
}; |
|
|
|
|
|
|
|
AddSegment(quadraticBezierSegment); |
|
|
|
|
|
|
|
ReadWhitespace(reader); |
|
|
|
double x = current.X + ReadDouble(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
double y = current.Y + ReadDouble(reader); |
|
|
|
return new Point(x, y); |
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private static Size ReadSize(StringReader reader) |
|
|
|
private void AddSmoothCubicBezierCurve(CommandToken commandToken) |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
double x = ReadDouble(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
double y = ReadDouble(reader); |
|
|
|
return new Size(x, y); |
|
|
|
var point2 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
if (_previousControlPoint != null) |
|
|
|
{ |
|
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
var bezierSegment = |
|
|
|
new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; |
|
|
|
|
|
|
|
AddSegment(bezierSegment); |
|
|
|
|
|
|
|
_previousControlPoint = point2; |
|
|
|
|
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private static bool ReadBool(StringReader reader) |
|
|
|
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) |
|
|
|
{ |
|
|
|
return ReadDouble(reader) != 0; |
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
if (_previousControlPoint != null) |
|
|
|
{ |
|
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
var quadraticBezierSegment = new QuadraticBezierSegment |
|
|
|
{ |
|
|
|
Point1 = _previousControlPoint ?? _currentPoint, |
|
|
|
Point2 = end |
|
|
|
}; |
|
|
|
|
|
|
|
AddSegment(quadraticBezierSegment); |
|
|
|
|
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private static Point ReadRelativePoint(StringReader reader, Point lastPoint) |
|
|
|
private void AddArc(CommandToken commandToken) |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
double x = ReadDouble(reader); |
|
|
|
ReadSeparator(reader); |
|
|
|
double y = ReadDouble(reader); |
|
|
|
return new Point(lastPoint.X + x, lastPoint.Y + y); |
|
|
|
var size = commandToken.ReadSize(); |
|
|
|
|
|
|
|
var rotationAngle = commandToken.ReadDouble(); |
|
|
|
|
|
|
|
var isLargeArc = commandToken.ReadBool(); |
|
|
|
|
|
|
|
var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
|
|
|
|
var arcSegment = new ArcSegment |
|
|
|
{ |
|
|
|
Size = size, |
|
|
|
RotationAngle = rotationAngle, |
|
|
|
IsLargeArc = isLargeArc, |
|
|
|
SweepDirection = sweepDirection, |
|
|
|
Point = end |
|
|
|
}; |
|
|
|
|
|
|
|
AddSegment(arcSegment); |
|
|
|
|
|
|
|
_currentPoint = end; |
|
|
|
|
|
|
|
_previousControlPoint = null; |
|
|
|
} |
|
|
|
|
|
|
|
private static void ReadSeparator(StringReader reader) |
|
|
|
private class CommandToken |
|
|
|
{ |
|
|
|
int i; |
|
|
|
bool readComma = false; |
|
|
|
private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; |
|
|
|
|
|
|
|
while ((i = reader.Peek()) != -1) |
|
|
|
private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments) |
|
|
|
{ |
|
|
|
char c = (char)i; |
|
|
|
Command = command; |
|
|
|
|
|
|
|
IsRelative = isRelative; |
|
|
|
|
|
|
|
Arguments = new List<string>(arguments); |
|
|
|
} |
|
|
|
|
|
|
|
public Command Command { get; } |
|
|
|
|
|
|
|
public bool IsRelative { get; } |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(c)) |
|
|
|
public bool HasImplicitCommands |
|
|
|
{ |
|
|
|
get |
|
|
|
{ |
|
|
|
reader.Read(); |
|
|
|
if (CurrentPosition == 0 && Arguments.Count > 0) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return CurrentPosition < Arguments.Count - 1; |
|
|
|
} |
|
|
|
else if (c == ',') |
|
|
|
} |
|
|
|
|
|
|
|
private int CurrentPosition { get; set; } |
|
|
|
|
|
|
|
private List<string> Arguments { get; } |
|
|
|
|
|
|
|
public static CommandToken Parse(string s) |
|
|
|
{ |
|
|
|
using (var reader = new StringReader(s)) |
|
|
|
{ |
|
|
|
if (readComma) |
|
|
|
var command = Command.None; |
|
|
|
|
|
|
|
var isRelative = false; |
|
|
|
|
|
|
|
if (!ReadCommand(reader, ref command, ref isRelative)) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Unexpected ','."); |
|
|
|
throw new InvalidDataException("No path command declared."); |
|
|
|
} |
|
|
|
|
|
|
|
readComma = true; |
|
|
|
reader.Read(); |
|
|
|
var commandArguments = reader.ReadToEnd(); |
|
|
|
|
|
|
|
var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); |
|
|
|
|
|
|
|
var arguments = new List<string>(); |
|
|
|
|
|
|
|
foreach (Match match in argumentMatches) |
|
|
|
{ |
|
|
|
arguments.Add(match.Value); |
|
|
|
} |
|
|
|
|
|
|
|
return new CommandToken(command, isRelative, arguments); |
|
|
|
} |
|
|
|
else |
|
|
|
} |
|
|
|
|
|
|
|
public FillRule ReadFillRule() |
|
|
|
{ |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
break; |
|
|
|
throw new InvalidDataException("Invalid fill rule"); |
|
|
|
} |
|
|
|
|
|
|
|
var value = Arguments[CurrentPosition]; |
|
|
|
|
|
|
|
CurrentPosition++; |
|
|
|
|
|
|
|
switch (value) |
|
|
|
{ |
|
|
|
case "0": |
|
|
|
{ |
|
|
|
return FillRule.EvenOdd; |
|
|
|
} |
|
|
|
|
|
|
|
case "1": |
|
|
|
{ |
|
|
|
return FillRule.NonZero; |
|
|
|
} |
|
|
|
|
|
|
|
default: |
|
|
|
throw new InvalidDataException("Invalid fill rule"); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void ReadWhitespace(StringReader reader) |
|
|
|
{ |
|
|
|
int i; |
|
|
|
public bool ReadBool() |
|
|
|
{ |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid boolean value"); |
|
|
|
} |
|
|
|
|
|
|
|
var value = Arguments[CurrentPosition]; |
|
|
|
|
|
|
|
while ((i = reader.Peek()) != -1) |
|
|
|
CurrentPosition++; |
|
|
|
|
|
|
|
switch (value) |
|
|
|
{ |
|
|
|
case "1": |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
case "0": |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
default: |
|
|
|
throw new InvalidDataException("Invalid boolean value"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public double ReadDouble() |
|
|
|
{ |
|
|
|
char c = (char)i; |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid double value"); |
|
|
|
} |
|
|
|
|
|
|
|
var value = Arguments[CurrentPosition]; |
|
|
|
|
|
|
|
CurrentPosition++; |
|
|
|
|
|
|
|
return double.Parse(value, CultureInfo.InvariantCulture); |
|
|
|
} |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(c)) |
|
|
|
public Size ReadSize() |
|
|
|
{ |
|
|
|
var width = ReadDouble(); |
|
|
|
|
|
|
|
var height = ReadDouble(); |
|
|
|
|
|
|
|
return new Size(width, height); |
|
|
|
} |
|
|
|
|
|
|
|
public Point ReadPoint() |
|
|
|
{ |
|
|
|
var x = ReadDouble(); |
|
|
|
|
|
|
|
var y = ReadDouble(); |
|
|
|
|
|
|
|
return new Point(x, y); |
|
|
|
} |
|
|
|
|
|
|
|
public Point ReadRelativePoint(Point origin) |
|
|
|
{ |
|
|
|
var x = ReadDouble(); |
|
|
|
|
|
|
|
var y = ReadDouble(); |
|
|
|
|
|
|
|
return new Point(origin.X + x, origin.Y + y); |
|
|
|
} |
|
|
|
|
|
|
|
private static bool ReadCommand( |
|
|
|
TextReader reader, |
|
|
|
ref Command command, |
|
|
|
ref bool relative) |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
|
|
|
|
var i = reader.Peek(); |
|
|
|
|
|
|
|
if (i == -1) |
|
|
|
{ |
|
|
|
reader.Read(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
var c = (char)i; |
|
|
|
|
|
|
|
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) |
|
|
|
{ |
|
|
|
break; |
|
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'."); |
|
|
|
} |
|
|
|
|
|
|
|
command = next; |
|
|
|
|
|
|
|
relative = char.IsLower(c); |
|
|
|
|
|
|
|
reader.Read(); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private static void ReadWhitespace(TextReader reader) |
|
|
|
{ |
|
|
|
int i; |
|
|
|
|
|
|
|
while ((i = reader.Peek()) != -1) |
|
|
|
{ |
|
|
|
var c = (char)i; |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(c)) |
|
|
|
{ |
|
|
|
reader.Read(); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|