using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using Avalonia.Platform; namespace Avalonia.Media { /// /// Parses a path markup string. /// public class PathMarkupParser : IDisposable { private static readonly Dictionary s_commands = new Dictionary { { '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; /// /// Initializes a new instance of the class. /// /// The geometry context. /// geometryContext 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; } /// /// Parses the specified path data and writes the result to the geometryContext of this instance. /// /// The path data. 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( #if NET7SDK scoped #endif ref ReadOnlySpan 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 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 span, bool relative) { ThrowIfDisposed(); _currentPoint = relative ? ReadRelativePoint(ref span, _currentPoint) : ReadPoint(ref span); if (!_isOpen) { CreateFigure(); } _geometryContext.LineTo(_currentPoint); } private void AddHorizontalLine(ref ReadOnlySpan 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 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 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 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 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 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 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 span) { span = SkipWhitespace(span); return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0])); } private static bool ReadArgument( #if NET7SDK scoped #endif ref ReadOnlySpan remaining, out ReadOnlySpan argument) { remaining = SkipWhitespace(remaining); if (remaining.IsEmpty) { argument = ReadOnlySpan.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.Empty; return false; } argument = remaining.Slice(0, i); remaining = remaining.Slice(i); return true; } private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) { span = SkipWhitespace(span); if (!span.IsEmpty && span[0] == ',') { span = span.Slice(1); } return span; } private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) { int i = 0; for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; return span.Slice(i); } private bool ReadBool(ref ReadOnlySpan 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 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 span) { var width = ReadDouble(ref span); span = ReadSeparator(span); var height = ReadDouble(ref span); return new Size(width, height); } private Point ReadPoint(ref ReadOnlySpan span) { var x = ReadDouble(ref span); span = ReadSeparator(span); var y = ReadDouble(ref span); return new Point(x, y); } private Point ReadRelativePoint(ref ReadOnlySpan 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 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)); } } }