csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
684 lines
20 KiB
684 lines
20 KiB
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Parses a path markup string.
|
|
/// </summary>
|
|
public class PathMarkupParser : IDisposable
|
|
{
|
|
private static readonly string s_separatorPattern;
|
|
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 },
|
|
};
|
|
|
|
private IGeometryContext _geometryContext;
|
|
private Point _currentPoint;
|
|
private Point? _previousControlPoint;
|
|
private bool? _isOpen;
|
|
private bool _isDisposed;
|
|
|
|
static PathMarkupParser()
|
|
{
|
|
s_separatorPattern = CreatesSeparatorPattern();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
|
|
/// </summary>
|
|
/// <param name="geometryContext">The geometry context.</param>
|
|
/// <exception cref="ArgumentNullException">geometryContext</exception>
|
|
public PathMarkupParser(IGeometryContext geometryContext)
|
|
{
|
|
if (geometryContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(geometryContext));
|
|
}
|
|
|
|
_geometryContext = geometryContext;
|
|
}
|
|
|
|
private enum Command
|
|
{
|
|
None,
|
|
FillRule,
|
|
Move,
|
|
Line,
|
|
HorizontalLine,
|
|
VerticalLine,
|
|
CubicBezierCurve,
|
|
QuadraticBezierCurve,
|
|
SmoothCubicBezierCurve,
|
|
SmoothQuadraticBezierCurve,
|
|
Arc,
|
|
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);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (disposing)
|
|
{
|
|
_geometryContext = null;
|
|
}
|
|
|
|
_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;
|
|
|
|
return center + -dir;
|
|
}
|
|
|
|
private void CreateGeometry(IEnumerable<CommandToken> commandTokens)
|
|
{
|
|
_currentPoint = new Point();
|
|
|
|
foreach (var commandToken in commandTokens)
|
|
{
|
|
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)
|
|
{
|
|
break;
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_isOpen != null)
|
|
{
|
|
_geometryContext.EndFigure(false);
|
|
}
|
|
}
|
|
|
|
private void CreateFigure()
|
|
{
|
|
if (_isOpen != null)
|
|
{
|
|
_geometryContext.EndFigure(false);
|
|
}
|
|
|
|
_geometryContext.BeginFigure(_currentPoint);
|
|
|
|
_isOpen = true;
|
|
}
|
|
|
|
private void SetFillRule(CommandToken commandToken)
|
|
{
|
|
var fillRule = commandToken.ReadFillRule();
|
|
|
|
_geometryContext.SetFillRule(fillRule);
|
|
}
|
|
|
|
private void CloseFigure()
|
|
{
|
|
if (_isOpen == true)
|
|
{
|
|
_geometryContext.EndFigure(true);
|
|
}
|
|
|
|
_previousControlPoint = null;
|
|
|
|
_isOpen = null;
|
|
}
|
|
|
|
private void AddMove(CommandToken commandToken)
|
|
{
|
|
var currentPoint = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
_currentPoint = currentPoint;
|
|
|
|
CreateFigure();
|
|
|
|
if (!commandToken.HasImplicitCommands)
|
|
{
|
|
return;
|
|
}
|
|
|
|
while (commandToken.HasImplicitCommands)
|
|
{
|
|
AddLine(commandToken);
|
|
|
|
if (commandToken.IsRelative)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_currentPoint = currentPoint;
|
|
|
|
CreateFigure();
|
|
}
|
|
}
|
|
|
|
private void AddLine(CommandToken commandToken)
|
|
{
|
|
_currentPoint = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddHorizontalLine(CommandToken commandToken)
|
|
{
|
|
_currentPoint = commandToken.IsRelative
|
|
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
|
|
: _currentPoint.WithX(commandToken.ReadDouble());
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddVerticalLine(CommandToken commandToken)
|
|
{
|
|
_currentPoint = commandToken.IsRelative
|
|
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
|
|
: _currentPoint.WithY(commandToken.ReadDouble());
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddCubicBezierCurve(CommandToken commandToken)
|
|
{
|
|
var point1 = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
var point2 = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
_previousControlPoint = point2;
|
|
|
|
var point3 = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.CubicBezierTo(point1, point2, point3);
|
|
|
|
_currentPoint = point3;
|
|
}
|
|
|
|
private void AddQuadraticBezierCurve(CommandToken commandToken)
|
|
{
|
|
var start = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
_previousControlPoint = start;
|
|
|
|
var end = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.QuadraticBezierTo(start, end);
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddSmoothCubicBezierCurve(CommandToken commandToken)
|
|
{
|
|
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);
|
|
}
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
|
|
|
|
_previousControlPoint = point2;
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
|
|
{
|
|
var end = commandToken.IsRelative
|
|
? commandToken.ReadRelativePoint(_currentPoint)
|
|
: commandToken.ReadPoint();
|
|
|
|
if (_previousControlPoint != null)
|
|
{
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
|
|
}
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddArc(CommandToken commandToken)
|
|
{
|
|
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();
|
|
|
|
if (_isOpen == null)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
|
|
|
|
_currentPoint = end;
|
|
|
|
_previousControlPoint = null;
|
|
}
|
|
|
|
private class CommandToken
|
|
{
|
|
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; }
|
|
|
|
public bool IsRelative { get; }
|
|
|
|
public bool HasImplicitCommands
|
|
{
|
|
get
|
|
{
|
|
if (CurrentPosition == 0 && Arguments.Count > 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return CurrentPosition < Arguments.Count - 1;
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
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");
|
|
}
|
|
}
|
|
|
|
public bool ReadBool()
|
|
{
|
|
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");
|
|
}
|
|
}
|
|
|
|
public double ReadDouble()
|
|
{
|
|
if (CurrentPosition == Arguments.Count)
|
|
{
|
|
throw new InvalidDataException("Invalid double value");
|
|
}
|
|
|
|
var value = Arguments[CurrentPosition];
|
|
|
|
CurrentPosition++;
|
|
|
|
return double.Parse(value, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
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)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var c = (char)i;
|
|
|
|
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|