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.
357 lines
11 KiB
357 lines
11 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.Text;
|
|
|
|
namespace Avalonia.Media
|
|
{
|
|
/// <summary>
|
|
/// Parses a path markup string.
|
|
/// </summary>
|
|
public class PathMarkupParser
|
|
{
|
|
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 },
|
|
{ 'C', Command.CubicBezierCurve },
|
|
{ 'A', Command.Arc },
|
|
{ 'Z', Command.Close },
|
|
};
|
|
|
|
private static readonly Dictionary<char, FillRule> FillRules = new Dictionary<char, FillRule>
|
|
{
|
|
{'0', FillRule.EvenOdd },
|
|
{'1', FillRule.NonZero }
|
|
};
|
|
|
|
private readonly StreamGeometryContext _context;
|
|
|
|
/// <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)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the command currently being processed.
|
|
/// </summary>
|
|
private enum Command
|
|
{
|
|
None,
|
|
FillRule,
|
|
Move,
|
|
Line,
|
|
HorizontalLine,
|
|
VerticalLine,
|
|
CubicBezierCurve,
|
|
Arc,
|
|
Close,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the specified markup string.
|
|
/// </summary>
|
|
/// <param name="s">The markup string.</param>
|
|
public void Parse(string s)
|
|
{
|
|
bool openFigure = false;
|
|
|
|
using (StringReader reader = new StringReader(s))
|
|
{
|
|
Command command = Command.None;
|
|
Point point = new Point();
|
|
bool relative = false;
|
|
|
|
while (ReadCommand(reader, ref command, ref relative))
|
|
{
|
|
switch (command)
|
|
{
|
|
case Command.FillRule:
|
|
_context.SetFillRule(ReadFillRule(reader));
|
|
break;
|
|
|
|
case Command.Move:
|
|
if (openFigure)
|
|
{
|
|
_context.EndFigure(false);
|
|
}
|
|
|
|
point = ReadPoint(reader, point, relative);
|
|
_context.BeginFigure(point, true);
|
|
openFigure = true;
|
|
break;
|
|
|
|
case Command.Line:
|
|
point = ReadPoint(reader, point, relative);
|
|
_context.LineTo(point);
|
|
break;
|
|
|
|
case Command.HorizontalLine:
|
|
if (!relative)
|
|
{
|
|
point = point.WithX(ReadDouble(reader));
|
|
}
|
|
else
|
|
{
|
|
point = new Point(point.X + ReadDouble(reader), point.Y);
|
|
}
|
|
|
|
_context.LineTo(point);
|
|
break;
|
|
|
|
case Command.VerticalLine:
|
|
if (!relative)
|
|
{
|
|
point = point.WithY(ReadDouble(reader));
|
|
}
|
|
else
|
|
{
|
|
point = new Point(point.X, point.Y + ReadDouble(reader));
|
|
}
|
|
|
|
_context.LineTo(point);
|
|
break;
|
|
|
|
case Command.CubicBezierCurve:
|
|
{
|
|
Point point1 = ReadPoint(reader, point, relative);
|
|
Point point2 = ReadPoint(reader, point, relative);
|
|
point = ReadPoint(reader, point, relative);
|
|
_context.CubicBezierTo(point1, point2, point);
|
|
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;
|
|
point = ReadPoint(reader, point, relative);
|
|
|
|
_context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
|
|
break;
|
|
}
|
|
|
|
case Command.Close:
|
|
_context.EndFigure(true);
|
|
openFigure = false;
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException("Unsupported command");
|
|
}
|
|
}
|
|
|
|
if (openFigure)
|
|
{
|
|
_context.EndFigure(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool ReadCommand(
|
|
StringReader reader,
|
|
ref Command command,
|
|
ref bool relative)
|
|
{
|
|
ReadWhitespace(reader);
|
|
|
|
int i = reader.Peek();
|
|
|
|
if (i == -1)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
char c = (char)i;
|
|
Command next = Command.None;
|
|
|
|
if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next))
|
|
{
|
|
if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') &&
|
|
(command != Command.None))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'.");
|
|
}
|
|
}
|
|
|
|
command = next;
|
|
relative = char.IsLower(c);
|
|
reader.Read();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static FillRule ReadFillRule(StringReader reader)
|
|
{
|
|
int i = reader.Read();
|
|
if (i == -1)
|
|
{
|
|
throw new InvalidDataException("Invalid fill rule");
|
|
}
|
|
char c = (char)i;
|
|
FillRule rule;
|
|
|
|
if (!FillRules.TryGetValue(c, out rule))
|
|
{
|
|
throw new InvalidDataException("Invalid fill rule");
|
|
}
|
|
|
|
return rule;
|
|
}
|
|
|
|
private static double ReadDouble(StringReader reader)
|
|
{
|
|
ReadWhitespace(reader);
|
|
|
|
// TODO: Handle Infinity, NaN and scientific notation.
|
|
StringBuilder b = new StringBuilder();
|
|
bool readSign = false;
|
|
bool readPoint = false;
|
|
bool readExponent = false;
|
|
int i;
|
|
|
|
while ((i = reader.Peek()) != -1)
|
|
{
|
|
char c = char.ToUpperInvariant((char)i);
|
|
|
|
if (((c == '+' || c == '-') && !readSign) ||
|
|
(c == '.' && !readPoint) ||
|
|
(c == 'E' && !readExponent) ||
|
|
char.IsDigit(c))
|
|
{
|
|
b.Append(c);
|
|
reader.Read();
|
|
|
|
if (!readSign)
|
|
{
|
|
readSign = c == '+' || c == '-';
|
|
}
|
|
|
|
if (!readPoint)
|
|
{
|
|
readPoint = c == '.';
|
|
}
|
|
|
|
if (c == 'E')
|
|
{
|
|
readSign = false;
|
|
readExponent = c == 'E';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return double.Parse(b.ToString(), CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static Point ReadPoint(StringReader reader, Point current, bool relative)
|
|
{
|
|
if (!relative)
|
|
{
|
|
current = new Point();
|
|
}
|
|
|
|
ReadWhitespace(reader);
|
|
double x = current.X + ReadDouble(reader);
|
|
ReadSeparator(reader);
|
|
double y = current.Y + ReadDouble(reader);
|
|
return new Point(x, y);
|
|
}
|
|
|
|
private static Size ReadSize(StringReader reader)
|
|
{
|
|
ReadWhitespace(reader);
|
|
double x = ReadDouble(reader);
|
|
ReadSeparator(reader);
|
|
double y = ReadDouble(reader);
|
|
return new Size(x, y);
|
|
}
|
|
|
|
private static bool ReadBool(StringReader reader)
|
|
{
|
|
return ReadDouble(reader) != 0;
|
|
}
|
|
|
|
private static Point ReadRelativePoint(StringReader reader, Point lastPoint)
|
|
{
|
|
ReadWhitespace(reader);
|
|
double x = ReadDouble(reader);
|
|
ReadSeparator(reader);
|
|
double y = ReadDouble(reader);
|
|
return new Point(lastPoint.X + x, lastPoint.Y + y);
|
|
}
|
|
|
|
private static void ReadSeparator(StringReader reader)
|
|
{
|
|
int i;
|
|
bool readComma = false;
|
|
|
|
while ((i = reader.Peek()) != -1)
|
|
{
|
|
char c = (char)i;
|
|
|
|
if (char.IsWhiteSpace(c))
|
|
{
|
|
reader.Read();
|
|
}
|
|
else if (c == ',')
|
|
{
|
|
if (readComma)
|
|
{
|
|
throw new InvalidDataException("Unexpected ','.");
|
|
}
|
|
|
|
readComma = true;
|
|
reader.Read();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ReadWhitespace(StringReader reader)
|
|
{
|
|
int i;
|
|
|
|
while ((i = reader.Peek()) != -1)
|
|
{
|
|
char c = (char)i;
|
|
|
|
if (char.IsWhiteSpace(c))
|
|
{
|
|
reader.Read();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|