A cross-platform UI framework for .NET
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.
 
 
 

606 lines
18 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Platform;
namespace Avalonia.Media
{
/// <summary>
/// Parses a path markup string.
/// </summary>
public class PathMarkupParser : IDisposable
{
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? _beginFigurePoint;
private Point? _previousControlPoint;
private bool _isOpen;
private bool _isDisposed;
/// <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
}
void IDisposable.Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
if (disposing)
{
_geometryContext = null;
}
_isDisposed = true;
}
private static Point MirrorControlPoint(Point controlPoint, Point center)
{
var dir = controlPoint - center;
return center + -dir;
}
/// <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)
{
ThrowIfDisposed();
var span = pathData.AsSpan();
_currentPoint = new Point();
while(!span.IsEmpty)
{
if(!ReadCommand(ref span, out var command, out var relative))
{
break;
}
bool initialCommand = true;
do
{
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)
{
_geometryContext.EndFigure(false);
}
}
private void CreateFigure()
{
ThrowIfDisposed();
if (_isOpen)
{
_geometryContext.EndFigure(false);
}
_geometryContext.BeginFigure(_currentPoint);
_beginFigurePoint = _currentPoint;
_isOpen = true;
}
private void SetFillRule(ref ReadOnlySpan<char> span)
{
ThrowIfDisposed();
if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1)
{
throw new InvalidDataException("Invalid fill rule.");
}
FillRule rule;
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()
{
ThrowIfDisposed();
if (_isOpen)
{
_geometryContext.EndFigure(true);
if (_beginFigurePoint != null)
{
_currentPoint = _beginFigurePoint.Value;
_beginFigurePoint = null;
}
}
_previousControlPoint = null;
_isOpen = false;
}
private void AddMove(ref ReadOnlySpan<char> span, bool relative)
{
var currentPoint = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_currentPoint = currentPoint;
CreateFigure();
while (PeekArgument(span))
{
span = ReadSeparator(span);
AddLine(ref span, relative);
}
}
private void AddLine(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
_currentPoint = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.LineTo(_currentPoint);
}
private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
_currentPoint = relative
? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
: _currentPoint.WithX(ReadDouble(ref span));
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.LineTo(_currentPoint);
}
private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
_currentPoint = relative
? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
: _currentPoint.WithY(ReadDouble(ref span));
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.LineTo(_currentPoint);
}
private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
var point1 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
span = ReadSeparator(span);
var point2 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_previousControlPoint = point2;
span = ReadSeparator(span);
var point3 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.CubicBezierTo(point1, point2, point3);
_currentPoint = point3;
}
private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
var start = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_previousControlPoint = start;
span = ReadSeparator(span);
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.QuadraticBezierTo(start, end);
_currentPoint = end;
}
private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
var point2 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
span = ReadSeparator(span);
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
_previousControlPoint = point2;
_currentPoint = end;
}
private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
_currentPoint = end;
}
private void AddArc(ref ReadOnlySpan<char> span, bool relative)
{
ThrowIfDisposed();
var size = ReadSize(ref span);
span = ReadSeparator(span);
var rotationAngle = ReadDouble(ref span);
span = ReadSeparator(span);
var isLargeArc = ReadBool(ref span);
span = ReadSeparator(span);
var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
span = ReadSeparator(span);
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
_geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
_currentPoint = end;
_previousControlPoint = null;
}
private static bool PeekArgument(ReadOnlySpan<char> span)
{
span = SkipWhitespace(span);
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
}
private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)
{
argument = ReadOnlySpan<char>.Empty;
return false;
}
var valid = false;
int i = 0;
if (remaining[i] == '-')
{
i++;
}
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
if (i < remaining.Length && remaining[i] == '.')
{
valid = false;
i++;
}
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
if (i < remaining.Length)
{
// scientific notation
if (remaining[i] == 'E' || remaining[i] == 'e')
{
valid = false;
i++;
if (remaining[i] == '-' || remaining[i] == '+')
{
i++;
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
}
}
}
if (!valid)
{
argument = ReadOnlySpan<char>.Empty;
return false;
}
argument = remaining.Slice(0, i);
remaining = remaining.Slice(i);
return true;
}
private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span)
{
span = SkipWhitespace(span);
if (!span.IsEmpty && span[0] == ',')
{
span = span.Slice(1);
}
return span;
}
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 bool ReadBool(ref ReadOnlySpan<char> span)
{
span = SkipWhitespace(span);
if (span.IsEmpty)
{
throw new InvalidDataException("Invalid bool rule.");
}
var c = span[0];
span = span.Slice(1);
switch (c)
{
case '0':
return false;
case '1':
return true;
default:
throw new InvalidDataException("Invalid bool rule");
}
}
private double ReadDouble(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var doubleValue))
{
throw new InvalidDataException("Invalid double value");
}
return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
}
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);
}
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);
}
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);
}
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;
}
var c = span[0];
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
{
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
relative = char.IsLower(c);
span = span.Slice(1);
return true;
}
[MemberNotNull(nameof(_geometryContext))]
private void ThrowIfDisposed()
{
if (_isDisposed || _geometryContext is null)
throw new ObjectDisposedException(nameof(PathMarkupParser));
}
}
}