|
|
|
@ -5,9 +5,6 @@ using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Globalization; |
|
|
|
using System.IO; |
|
|
|
using System.Linq; |
|
|
|
using System.Text; |
|
|
|
using System.Text.RegularExpressions; |
|
|
|
using Avalonia.Platform; |
|
|
|
|
|
|
|
namespace Avalonia.Media |
|
|
|
@ -17,7 +14,6 @@ namespace Avalonia.Media |
|
|
|
/// </summary>
|
|
|
|
public class PathMarkupParser : IDisposable |
|
|
|
{ |
|
|
|
private static readonly string s_separatorPattern; |
|
|
|
private static readonly Dictionary<char, Command> s_commands = |
|
|
|
new Dictionary<char, Command> |
|
|
|
{ |
|
|
|
@ -37,14 +33,9 @@ namespace Avalonia.Media |
|
|
|
private IGeometryContext _geometryContext; |
|
|
|
private Point _currentPoint; |
|
|
|
private Point? _previousControlPoint; |
|
|
|
private bool? _isOpen; |
|
|
|
private bool _isOpen; |
|
|
|
private bool _isDisposed; |
|
|
|
|
|
|
|
static PathMarkupParser() |
|
|
|
{ |
|
|
|
s_separatorPattern = CreatesSeparatorPattern(); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
|
|
|
|
/// </summary>
|
|
|
|
@ -76,18 +67,6 @@ namespace Avalonia.Media |
|
|
|
Close |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Parses the specified path data and writes the result to the geometryContext of this instance.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="pathData">The path data.</param>
|
|
|
|
public void Parse(string pathData) |
|
|
|
{ |
|
|
|
var normalizedPathData = NormalizeWhiteSpaces(pathData); |
|
|
|
var tokens = ParseTokens(normalizedPathData); |
|
|
|
|
|
|
|
CreateGeometry(tokens); |
|
|
|
} |
|
|
|
|
|
|
|
void IDisposable.Dispose() |
|
|
|
{ |
|
|
|
Dispose(true); |
|
|
|
@ -108,66 +87,6 @@ namespace Avalonia.Media |
|
|
|
_isDisposed = true; |
|
|
|
} |
|
|
|
|
|
|
|
private static string NormalizeWhiteSpaces(string s) |
|
|
|
{ |
|
|
|
int length = s.Length, |
|
|
|
index = 0, |
|
|
|
i = 0; |
|
|
|
var source = s.ToCharArray(); |
|
|
|
var skip = false; |
|
|
|
|
|
|
|
for (; i < length; i++) |
|
|
|
{ |
|
|
|
var c = source[i]; |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(c)) |
|
|
|
{ |
|
|
|
if (skip) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
source[index++] = c; |
|
|
|
|
|
|
|
skip = true; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
skip = false; |
|
|
|
|
|
|
|
source[index++] = c; |
|
|
|
} |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(source[index - 1])) |
|
|
|
{ |
|
|
|
index--; |
|
|
|
} |
|
|
|
|
|
|
|
return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index); |
|
|
|
} |
|
|
|
|
|
|
|
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) |
|
|
|
{ |
|
|
|
var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)); |
|
|
|
|
|
|
|
return expressions.Select(CommandToken.Parse); |
|
|
|
} |
|
|
|
|
|
|
|
private static Point MirrorControlPoint(Point controlPoint, Point center) |
|
|
|
{ |
|
|
|
var dir = controlPoint - center; |
|
|
|
@ -175,76 +94,78 @@ namespace Avalonia.Media |
|
|
|
return center + -dir; |
|
|
|
} |
|
|
|
|
|
|
|
private void CreateGeometry(IEnumerable<CommandToken> commandTokens) |
|
|
|
/// <summary>
|
|
|
|
/// Parses the specified path data and writes the result to the geometryContext of this instance.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="pathData">The path data.</param>
|
|
|
|
public void Parse(string pathData) |
|
|
|
{ |
|
|
|
var span = pathData.AsSpan(); |
|
|
|
_currentPoint = new Point(); |
|
|
|
|
|
|
|
foreach (var commandToken in commandTokens) |
|
|
|
while(!span.IsEmpty) |
|
|
|
{ |
|
|
|
try |
|
|
|
{ |
|
|
|
while (true) |
|
|
|
{ |
|
|
|
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.SmoothCubicBezierCurve: |
|
|
|
AddSmoothCubicBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
case Command.SmoothQuadraticBezierCurve: |
|
|
|
AddSmoothQuadraticBezierCurve(commandToken); |
|
|
|
break; |
|
|
|
case Command.Arc: |
|
|
|
AddArc(commandToken); |
|
|
|
break; |
|
|
|
case Command.Close: |
|
|
|
CloseFigure(); |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new NotSupportedException("Unsupported command"); |
|
|
|
} |
|
|
|
|
|
|
|
if (commandToken.HasImplicitCommands) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
catch (InvalidDataException) |
|
|
|
if(!ReadCommand(ref span, out var command, out var relative)) |
|
|
|
{ |
|
|
|
break; |
|
|
|
return; |
|
|
|
} |
|
|
|
catch (NotSupportedException) |
|
|
|
|
|
|
|
bool initialCommand = true; |
|
|
|
|
|
|
|
do |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
if (!initialCommand) |
|
|
|
{ |
|
|
|
span = ReadSeparator(span); |
|
|
|
} |
|
|
|
|
|
|
|
switch (command) |
|
|
|
{ |
|
|
|
case Command.None: |
|
|
|
break; |
|
|
|
case Command.FillRule: |
|
|
|
SetFillRule(ref span); |
|
|
|
break; |
|
|
|
case Command.Move: |
|
|
|
AddMove(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.Line: |
|
|
|
AddLine(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.HorizontalLine: |
|
|
|
AddHorizontalLine(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.VerticalLine: |
|
|
|
AddVerticalLine(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.CubicBezierCurve: |
|
|
|
AddCubicBezierCurve(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.QuadraticBezierCurve: |
|
|
|
AddQuadraticBezierCurve(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.SmoothCubicBezierCurve: |
|
|
|
AddSmoothCubicBezierCurve(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.SmoothQuadraticBezierCurve: |
|
|
|
AddSmoothQuadraticBezierCurve(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.Arc: |
|
|
|
AddArc(ref span, relative); |
|
|
|
break; |
|
|
|
case Command.Close: |
|
|
|
CloseFigure(); |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new NotSupportedException("Unsupported command"); |
|
|
|
} |
|
|
|
|
|
|
|
initialCommand = false; |
|
|
|
} while (PeekArgument(span)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (_isOpen != null) |
|
|
|
if (_isOpen) |
|
|
|
{ |
|
|
|
_geometryContext.EndFigure(false); |
|
|
|
} |
|
|
|
@ -252,7 +173,7 @@ namespace Avalonia.Media |
|
|
|
|
|
|
|
private void CreateFigure() |
|
|
|
{ |
|
|
|
if (_isOpen != null) |
|
|
|
if (_isOpen) |
|
|
|
{ |
|
|
|
_geometryContext.EndFigure(false); |
|
|
|
} |
|
|
|
@ -262,62 +183,72 @@ namespace Avalonia.Media |
|
|
|
_isOpen = true; |
|
|
|
} |
|
|
|
|
|
|
|
private void SetFillRule(CommandToken commandToken) |
|
|
|
private void SetFillRule(ref ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
var fillRule = commandToken.ReadFillRule(); |
|
|
|
if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid fill rule."); |
|
|
|
} |
|
|
|
|
|
|
|
FillRule rule; |
|
|
|
|
|
|
|
_geometryContext.SetFillRule(fillRule); |
|
|
|
switch (fillRule[0]) |
|
|
|
{ |
|
|
|
case '0': |
|
|
|
rule = FillRule.EvenOdd; |
|
|
|
break; |
|
|
|
case '1': |
|
|
|
rule = FillRule.NonZero; |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new InvalidDataException("Invalid fill rule"); |
|
|
|
} |
|
|
|
|
|
|
|
_geometryContext.SetFillRule(rule); |
|
|
|
} |
|
|
|
|
|
|
|
private void CloseFigure() |
|
|
|
{ |
|
|
|
if (_isOpen == true) |
|
|
|
if (_isOpen) |
|
|
|
{ |
|
|
|
_geometryContext.EndFigure(true); |
|
|
|
} |
|
|
|
|
|
|
|
_previousControlPoint = null; |
|
|
|
|
|
|
|
_isOpen = null; |
|
|
|
_isOpen = false; |
|
|
|
} |
|
|
|
|
|
|
|
private void AddMove(CommandToken commandToken) |
|
|
|
private void AddMove(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var currentPoint = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var currentPoint = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
_currentPoint = currentPoint; |
|
|
|
|
|
|
|
CreateFigure(); |
|
|
|
|
|
|
|
if (!commandToken.HasImplicitCommands) |
|
|
|
while (PeekArgument(span)) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
span = ReadSeparator(span); |
|
|
|
AddLine(ref span, relative); |
|
|
|
|
|
|
|
while (commandToken.HasImplicitCommands) |
|
|
|
{ |
|
|
|
AddLine(commandToken); |
|
|
|
|
|
|
|
if (commandToken.IsRelative) |
|
|
|
if (!relative) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
_currentPoint = currentPoint; |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
|
|
|
|
_currentPoint = currentPoint; |
|
|
|
|
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void AddLine(CommandToken commandToken) |
|
|
|
private void AddLine(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
_currentPoint = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -325,13 +256,13 @@ namespace Avalonia.Media |
|
|
|
_geometryContext.LineTo(_currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddHorizontalLine(CommandToken commandToken) |
|
|
|
private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) |
|
|
|
: _currentPoint.WithX(commandToken.ReadDouble()); |
|
|
|
_currentPoint = relative |
|
|
|
? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y) |
|
|
|
: _currentPoint.WithX(ReadDouble(ref span)); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -339,13 +270,13 @@ namespace Avalonia.Media |
|
|
|
_geometryContext.LineTo(_currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddVerticalLine(CommandToken commandToken) |
|
|
|
private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
_currentPoint = commandToken.IsRelative |
|
|
|
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) |
|
|
|
: _currentPoint.WithY(commandToken.ReadDouble()); |
|
|
|
_currentPoint = relative |
|
|
|
? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span)) |
|
|
|
: _currentPoint.WithY(ReadDouble(ref span)); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -353,23 +284,27 @@ namespace Avalonia.Media |
|
|
|
_geometryContext.LineTo(_currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
private void AddCubicBezierCurve(CommandToken commandToken) |
|
|
|
private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var point1 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var point1 = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
var point2 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var point2 = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
_previousControlPoint = point2; |
|
|
|
|
|
|
|
var point3 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var point3 = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -379,19 +314,21 @@ namespace Avalonia.Media |
|
|
|
_currentPoint = point3; |
|
|
|
} |
|
|
|
|
|
|
|
private void AddQuadraticBezierCurve(CommandToken commandToken) |
|
|
|
private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var start = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var start = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
_previousControlPoint = start; |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var end = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -401,22 +338,24 @@ namespace Avalonia.Media |
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private void AddSmoothCubicBezierCurve(CommandToken commandToken) |
|
|
|
private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var point2 = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var point2 = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var end = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_previousControlPoint != null) |
|
|
|
{ |
|
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -428,18 +367,18 @@ namespace Avalonia.Media |
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) |
|
|
|
private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var end = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_previousControlPoint != null) |
|
|
|
{ |
|
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); |
|
|
|
} |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -449,21 +388,27 @@ namespace Avalonia.Media |
|
|
|
_currentPoint = end; |
|
|
|
} |
|
|
|
|
|
|
|
private void AddArc(CommandToken commandToken) |
|
|
|
private void AddArc(ref ReadOnlySpan<char> span, bool relative) |
|
|
|
{ |
|
|
|
var size = commandToken.ReadSize(); |
|
|
|
var size = ReadSize(ref span); |
|
|
|
|
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var rotationAngle = commandToken.ReadDouble(); |
|
|
|
var rotationAngle = ReadDouble(ref span); |
|
|
|
span = ReadSeparator(span); |
|
|
|
var isLargeArc = ReadBool(ref span); |
|
|
|
|
|
|
|
var isLargeArc = commandToken.ReadBool(); |
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; |
|
|
|
var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; |
|
|
|
|
|
|
|
span = ReadSeparator(span); |
|
|
|
|
|
|
|
var end = commandToken.IsRelative |
|
|
|
? commandToken.ReadRelativePoint(_currentPoint) |
|
|
|
: commandToken.ReadPoint(); |
|
|
|
var end = relative |
|
|
|
? ReadRelativePoint(ref span, _currentPoint) |
|
|
|
: ReadPoint(ref span); |
|
|
|
|
|
|
|
if (_isOpen == null) |
|
|
|
if (!_isOpen) |
|
|
|
{ |
|
|
|
CreateFigure(); |
|
|
|
} |
|
|
|
@ -475,210 +420,134 @@ namespace Avalonia.Media |
|
|
|
_previousControlPoint = null; |
|
|
|
} |
|
|
|
|
|
|
|
private class CommandToken |
|
|
|
private static bool PeekArgument(ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; |
|
|
|
|
|
|
|
private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments) |
|
|
|
{ |
|
|
|
Command = command; |
|
|
|
|
|
|
|
IsRelative = isRelative; |
|
|
|
|
|
|
|
Arguments = new List<string>(arguments); |
|
|
|
} |
|
|
|
|
|
|
|
public Command Command { get; } |
|
|
|
span = SkipWhitespace(span); |
|
|
|
|
|
|
|
public bool IsRelative { get; } |
|
|
|
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0])); |
|
|
|
} |
|
|
|
|
|
|
|
public bool HasImplicitCommands |
|
|
|
private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument) |
|
|
|
{ |
|
|
|
remaining = SkipWhitespace(remaining); |
|
|
|
if (remaining.IsEmpty) |
|
|
|
{ |
|
|
|
get |
|
|
|
{ |
|
|
|
if (CurrentPosition == 0 && Arguments.Count > 0) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return CurrentPosition < Arguments.Count - 1; |
|
|
|
} |
|
|
|
argument = ReadOnlySpan<char>.Empty; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private int CurrentPosition { get; set; } |
|
|
|
|
|
|
|
private List<string> Arguments { get; } |
|
|
|
|
|
|
|
public static CommandToken Parse(string s) |
|
|
|
{ |
|
|
|
using (var reader = new StringReader(s)) |
|
|
|
{ |
|
|
|
var command = Command.None; |
|
|
|
|
|
|
|
var isRelative = false; |
|
|
|
|
|
|
|
if (!ReadCommand(reader, ref command, ref isRelative)) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("No path command declared."); |
|
|
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public FillRule ReadFillRule() |
|
|
|
var valid = false; |
|
|
|
int i = 0; |
|
|
|
if (remaining[i] == '-') |
|
|
|
{ |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
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"); |
|
|
|
} |
|
|
|
i++; |
|
|
|
} |
|
|
|
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; |
|
|
|
|
|
|
|
public bool ReadBool() |
|
|
|
if (i < remaining.Length && remaining[i] == '.') |
|
|
|
{ |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid boolean value"); |
|
|
|
} |
|
|
|
|
|
|
|
var value = Arguments[CurrentPosition]; |
|
|
|
|
|
|
|
CurrentPosition++; |
|
|
|
|
|
|
|
switch (value) |
|
|
|
{ |
|
|
|
case "1": |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
case "0": |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
default: |
|
|
|
throw new InvalidDataException("Invalid boolean value"); |
|
|
|
} |
|
|
|
valid = false; |
|
|
|
i++; |
|
|
|
} |
|
|
|
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; |
|
|
|
|
|
|
|
public double ReadDouble() |
|
|
|
if (!valid) |
|
|
|
{ |
|
|
|
if (CurrentPosition == Arguments.Count) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid double value"); |
|
|
|
} |
|
|
|
|
|
|
|
var value = Arguments[CurrentPosition]; |
|
|
|
|
|
|
|
CurrentPosition++; |
|
|
|
|
|
|
|
return double.Parse(value, CultureInfo.InvariantCulture); |
|
|
|
argument = ReadOnlySpan<char>.Empty; |
|
|
|
return false; |
|
|
|
} |
|
|
|
argument = remaining.Slice(0, i); |
|
|
|
remaining = remaining.Slice(i); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
public Size ReadSize() |
|
|
|
{ |
|
|
|
var width = ReadDouble(); |
|
|
|
|
|
|
|
var height = ReadDouble(); |
|
|
|
|
|
|
|
return new Size(width, height); |
|
|
|
} |
|
|
|
|
|
|
|
public Point ReadPoint() |
|
|
|
private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
span = SkipWhitespace(span); |
|
|
|
if (!span.IsEmpty && span[0] == ',') |
|
|
|
{ |
|
|
|
var x = ReadDouble(); |
|
|
|
|
|
|
|
var y = ReadDouble(); |
|
|
|
|
|
|
|
return new Point(x, y); |
|
|
|
span = span.Slice(1); |
|
|
|
} |
|
|
|
return span; |
|
|
|
} |
|
|
|
|
|
|
|
public Point ReadRelativePoint(Point origin) |
|
|
|
{ |
|
|
|
var x = ReadDouble(); |
|
|
|
|
|
|
|
var y = ReadDouble(); |
|
|
|
|
|
|
|
return new Point(origin.X + x, origin.Y + y); |
|
|
|
} |
|
|
|
private static ReadOnlySpan<char> SkipWhitespace(ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
int i = 0; |
|
|
|
for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; |
|
|
|
return span.Slice(i); |
|
|
|
} |
|
|
|
|
|
|
|
private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) |
|
|
|
private bool ReadBool(ref ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) |
|
|
|
{ |
|
|
|
ReadWhitespace(reader); |
|
|
|
|
|
|
|
var i = reader.Peek(); |
|
|
|
|
|
|
|
if (i == -1) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid bool rule."); |
|
|
|
} |
|
|
|
|
|
|
|
switch (boolValue[0]) |
|
|
|
{ |
|
|
|
case '0': |
|
|
|
return false; |
|
|
|
} |
|
|
|
case '1': |
|
|
|
return true; |
|
|
|
default: |
|
|
|
throw new InvalidDataException("Invalid bool rule"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var c = (char)i; |
|
|
|
private double ReadDouble(ref ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
if (!ReadArgument(ref span, out var doubleValue)) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Invalid double value"); |
|
|
|
} |
|
|
|
|
|
|
|
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) |
|
|
|
{ |
|
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'."); |
|
|
|
} |
|
|
|
return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture); |
|
|
|
} |
|
|
|
|
|
|
|
command = next; |
|
|
|
private Size ReadSize(ref ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
var width = ReadDouble(ref span); |
|
|
|
span = ReadSeparator(span); |
|
|
|
var height = ReadDouble(ref span); |
|
|
|
return new Size(width, height); |
|
|
|
} |
|
|
|
|
|
|
|
relative = char.IsLower(c); |
|
|
|
private Point ReadPoint(ref ReadOnlySpan<char> span) |
|
|
|
{ |
|
|
|
var x = ReadDouble(ref span); |
|
|
|
span = ReadSeparator(span); |
|
|
|
var y = ReadDouble(ref span); |
|
|
|
return new Point(x, y); |
|
|
|
} |
|
|
|
|
|
|
|
reader.Read(); |
|
|
|
private Point ReadRelativePoint(ref ReadOnlySpan<char> span, Point origin) |
|
|
|
{ |
|
|
|
var x = ReadDouble(ref span); |
|
|
|
span = ReadSeparator(span); |
|
|
|
var y = ReadDouble(ref span); |
|
|
|
return new Point(origin.X + x, origin.Y + y); |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
private bool ReadCommand(ref ReadOnlySpan<char> span, out Command command, out bool relative) |
|
|
|
{ |
|
|
|
span = SkipWhitespace(span); |
|
|
|
if (span.IsEmpty) |
|
|
|
{ |
|
|
|
command = default; |
|
|
|
relative = false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private static void ReadWhitespace(TextReader reader) |
|
|
|
var c = span[0]; |
|
|
|
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) |
|
|
|
{ |
|
|
|
int i; |
|
|
|
|
|
|
|
while ((i = reader.Peek()) != -1) |
|
|
|
{ |
|
|
|
var c = (char)i; |
|
|
|
|
|
|
|
if (char.IsWhiteSpace(c)) |
|
|
|
{ |
|
|
|
reader.Read(); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'."); |
|
|
|
} |
|
|
|
relative = char.IsLower(c); |
|
|
|
span = span.Slice(1); |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |