|
#nullable disable
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Controls.Shapes
{
/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/PathFigureCollectionConverter.xml" path="Type[@FullName='Microsoft.Maui.Controls.Shapes.PathFigureCollectionConverter']/Docs/*" />
public class PathFigureCollectionConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string);
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
=> destinationType == typeof(string);
const bool AllowSign = true;
const bool AllowComma = true;
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strValue = value?.ToString();
PathFigureCollection pathFigureCollection = new PathFigureCollection();
ParseStringToPathFigureCollection(pathFigureCollection, strValue);
return pathFigureCollection;
}
/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/PathFigureCollectionConverter.xml" path="//Member[@MemberName='ParseStringToPathFigureCollection']/Docs/*" />
public static void ParseStringToPathFigureCollection(PathFigureCollection pathFigureCollection, string pathString)
{
bool figureStarted = default;
string currentPathString = default;
int pathLength = default;
int currentIndex = default;
Point lastStart = default;
Point lastPoint = default;
Point secondLastPoint = default;
char token = default;
if (pathString != null)
{
int curIndex = 0;
while ((curIndex < pathString.Length) && char.IsWhiteSpace(pathString, curIndex))
{
curIndex++;
}
if (curIndex < pathString.Length)
{
if (pathString[curIndex] == 'F')
{
curIndex++;
while ((curIndex < pathString.Length) && char.IsWhiteSpace(pathString, curIndex))
{
curIndex++;
}
// If we ran out of text, this is an error, because 'F' cannot be specified without 0 or 1
// Also, if the next token isn't 0 or 1, this too is illegal
if ((curIndex == pathString.Length) ||
((pathString[curIndex] != '0') &&
(pathString[curIndex] != '1')))
{
throw new FormatException("IllegalToken");
}
// Increment curIndex to point to the next char
curIndex++;
}
}
ParseToPathFigureCollection(pathFigureCollection, pathString, curIndex);
}
void ParseToPathFigureCollection(PathFigureCollection pathFigureCollection, string pathString, int startIndex)
{
PathFigure pathFigure = null;
currentPathString = pathString;
pathLength = pathString.Length;
currentIndex = startIndex;
secondLastPoint = new Point(0, 0);
lastPoint = new Point(0, 0);
lastStart = new Point(0, 0);
figureStarted = false;
bool first = true;
char last_cmd = ' ';
while (ReadToken()) // Empty path is allowed in XAML
{
char cmd = token;
if (first)
{
if ((cmd != 'M') && (cmd != 'm')) // Path starts with M|m
{
ThrowBadToken();
}
first = false;
}
switch (cmd)
{
case 'm':
case 'M':
// XAML allows multiple points after M/m
lastPoint = ReadPoint(cmd, !AllowComma);
pathFigure = new PathFigure
{
StartPoint = lastPoint
};
pathFigureCollection.Add(pathFigure);
figureStarted = true;
lastStart = lastPoint;
last_cmd = 'M';
while (IsNumber(AllowComma))
{
lastPoint = ReadPoint(cmd, !AllowComma);
LineSegment lineSegment = new LineSegment
{
Point = lastPoint
};
pathFigure.Segments.Add(lineSegment);
last_cmd = 'L';
}
break;
case 'l':
case 'L':
case 'h':
case 'H':
case 'v':
case 'V':
EnsureFigure();
do
{
switch (cmd)
{
case 'l':
lastPoint = ReadPoint(cmd, !AllowComma);
break;
case 'L':
lastPoint = ReadPoint(cmd, !AllowComma);
break;
case 'h':
lastPoint.X += ReadNumber(!AllowComma);
break;
case 'H':
lastPoint.X = ReadNumber(!AllowComma);
break;
case 'v':
lastPoint.Y += ReadNumber(!AllowComma);
break;
case 'V':
lastPoint.Y = ReadNumber(!AllowComma);
break;
}
pathFigure.Segments.Add(new LineSegment
{
Point = lastPoint
});
}
while (IsNumber(AllowComma));
last_cmd = 'L';
break;
case 'c':
case 'C': // Cubic Bezier
case 's':
case 'S': // Smooth cublic Bezier
EnsureFigure();
do
{
Point p;
if ((cmd == 's') || (cmd == 'S'))
{
if (last_cmd == 'C')
{
p = Reflect();
}
else
{
p = lastPoint;
}
secondLastPoint = ReadPoint(cmd, !AllowComma);
}
else
{
p = ReadPoint(cmd, !AllowComma);
secondLastPoint = ReadPoint(cmd, AllowComma);
}
lastPoint = ReadPoint(cmd, AllowComma);
BezierSegment bezierSegment = new BezierSegment
{
Point1 = p,
Point2 = secondLastPoint,
Point3 = lastPoint
};
pathFigure.Segments.Add(bezierSegment);
last_cmd = 'C';
}
while (IsNumber(AllowComma));
break;
case 'q':
case 'Q': // Quadratic Bezier
case 't':
case 'T': // Smooth quadratic Bezier
EnsureFigure();
do
{
if ((cmd == 't') || (cmd == 'T'))
{
if (last_cmd == 'Q')
{
secondLastPoint = Reflect();
}
else
{
secondLastPoint = lastPoint;
}
lastPoint = ReadPoint(cmd, !AllowComma);
}
else
{
secondLastPoint = ReadPoint(cmd, !AllowComma);
lastPoint = ReadPoint(cmd, AllowComma);
}
QuadraticBezierSegment quadraticBezierSegment = new QuadraticBezierSegment
{
Point1 = secondLastPoint,
Point2 = lastPoint
};
pathFigure.Segments.Add(quadraticBezierSegment);
last_cmd = 'Q';
}
while (IsNumber(AllowComma));
break;
case 'a':
case 'A':
EnsureFigure();
do
{
// A 3,4 5, 0, 0, 6,7
double w = ReadNumber(!AllowComma);
double h = ReadNumber(AllowComma);
double rotation = ReadNumber(AllowComma);
bool large = ReadBool();
bool sweep = ReadBool();
lastPoint = ReadPoint(cmd, AllowComma);
ArcSegment arcSegment = new ArcSegment
{
Size = new Size(w, h),
RotationAngle = rotation,
IsLargeArc = large,
SweepDirection = sweep ? SweepDirection.Clockwise : SweepDirection.CounterClockwise,
Point = lastPoint
};
pathFigure.Segments.Add(arcSegment);
}
while (IsNumber(AllowComma));
last_cmd = 'A';
break;
case 'z':
case 'Z':
EnsureFigure();
pathFigure.IsClosed = true;
figureStarted = false;
last_cmd = 'Z';
lastPoint = lastStart; // Set reference point to be first point of current figure
break;
default:
ThrowBadToken();
break;
}
}
}
void EnsureFigure()
{
if (!figureStarted)
figureStarted = true;
}
Point Reflect()
{
return new Point(
2 * lastPoint.X - secondLastPoint.X,
2 * lastPoint.Y - secondLastPoint.Y);
}
bool More()
{
return currentIndex < pathLength;
}
bool SkipWhiteSpace(bool allowComma)
{
bool commaMet = false;
while (More())
{
char ch = currentPathString[currentIndex];
switch (ch)
{
case ' ':
case '\n':
case '\r':
case '\t':
break;
case ',':
if (allowComma)
{
commaMet = true;
allowComma = false; // One comma only
}
else
{
ThrowBadToken();
}
break;
default:
// Avoid calling IsWhiteSpace for ch in (' ' .. 'z']
if (((ch > ' ') && (ch <= 'z')) || !char.IsWhiteSpace(ch))
{
return commaMet;
}
break;
}
currentIndex++;
}
return commaMet;
}
bool ReadBool()
{
SkipWhiteSpace(AllowComma);
if (More())
{
token = currentPathString[currentIndex++];
if (token == '0')
{
return false;
}
else if (token == '1')
{
return true;
}
}
ThrowBadToken();
return false;
}
bool ReadToken()
{
SkipWhiteSpace(!AllowComma);
// Check for end of string
if (More())
{
token = currentPathString[currentIndex++];
return true;
}
else
{
return false;
}
}
void ThrowBadToken()
{
throw new FormatException(string.Format("UnexpectedToken \"{0}\" into {1}", currentPathString, currentIndex - 1));
}
Point ReadPoint(char cmd, bool allowcomma)
{
double x = ReadNumber(allowcomma);
double y = ReadNumber(AllowComma);
if (cmd >= 'a') // 'A' < 'a'. lower case for relative
{
x += lastPoint.X;
y += lastPoint.Y;
}
return new Point(x, y);
}
bool IsNumber(bool allowComma)
{
bool commaMet = SkipWhiteSpace(allowComma);
if (More())
{
token = currentPathString[currentIndex];
// Valid start of a number
if ((token == '.') || (token == '-') || (token == '+') || ((token >= '0') && (token <= '9'))
|| (token == 'I') // Infinity
|| (token == 'N')) // NaN
{
return true;
}
}
if (commaMet) // Only allowed between numbers
{
ThrowBadToken();
}
return false;
}
double ReadNumber(bool allowComma)
{
if (!IsNumber(allowComma))
{
ThrowBadToken();
}
bool simple = true;
int start = currentIndex;
// Allow for a sign
//
// There are numbers that cannot be preceded with a sign, for instance, -NaN, but it's
// fine to ignore that at this point, since the CLR parser will catch this later.
if (More() && ((currentPathString[currentIndex] == '-') || currentPathString[currentIndex] == '+'))
{
currentIndex++;
}
// Check for Infinity (or -Infinity).
if (More() && (currentPathString[currentIndex] == 'I'))
{
// Don't bother reading the characters, as the CLR parser will
// do this for us later.
currentIndex = Math.Min(currentIndex + 8, pathLength); // "Infinity" has 8 characters
simple = false;
}
// Check for NaN
else if (More() && (currentPathString[currentIndex] == 'N'))
{
//
// Don't bother reading the characters, as the CLR parser will
// do this for us later.
//
currentIndex = Math.Min(currentIndex + 3, pathLength); // "NaN" has 3 characters
simple = false;
}
else
{
SkipDigits(!AllowSign);
// Optional period, followed by more digits
if (More() && (currentPathString[currentIndex] == '.'))
{
simple = false;
currentIndex++;
SkipDigits(!AllowSign);
}
// Exponent
if (More() && ((currentPathString[currentIndex] == 'E') || (currentPathString[currentIndex] == 'e')))
{
simple = false;
currentIndex++;
SkipDigits(AllowSign);
}
}
if (simple && (currentIndex <= (start + 8))) // 32-bit integer
{
int sign = 1;
if (currentPathString[start] == '+')
{
start++;
}
else if (currentPathString[start] == '-')
{
start++;
sign = -1;
}
int value = 0;
while (start < currentIndex)
{
value = value * 10 + (currentPathString[start] - '0');
start++;
}
return value * sign;
}
else
{
string subString = currentPathString.Substring(start, currentIndex - start);
try
{
return Convert.ToDouble(subString, CultureInfo.InvariantCulture);
}
catch (FormatException)
{
throw new FormatException(string.Format("UnexpectedToken \"{0}\" into {1}", start, currentPathString));
}
}
}
void SkipDigits(bool signAllowed)
{
// Allow for a sign
if (signAllowed && More() && ((currentPathString[currentIndex] == '-') || currentPathString[currentIndex] == '+'))
{
currentIndex++;
}
while (More() && (currentPathString[currentIndex] >= '0') && (currentPathString[currentIndex] <= '9'))
{
currentIndex++;
}
}
}
private static string ParsePathFigureCollectionToString(PathFigureCollection pathFigureCollection)
{
var sb = new StringBuilder();
foreach (var pathFigure in pathFigureCollection)
{
sb.Append('M')
.Append(pathFigure.StartPoint.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(pathFigure.StartPoint.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ');
foreach (var pathSegment in pathFigure.Segments)
{
if (pathSegment is LineSegment lineSegment)
{
sb.Append('L')
.Append(lineSegment.Point.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(lineSegment.Point.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ');
}
else if (pathSegment is BezierSegment bezierSegment)
{
sb.Append('C')
.Append(bezierSegment.Point1.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(bezierSegment.Point1.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ')
.Append(bezierSegment.Point2.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(bezierSegment.Point2.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ')
.Append(bezierSegment.Point3.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(bezierSegment.Point3.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ');
}
else if (pathSegment is QuadraticBezierSegment quadraticBezierSegment)
{
sb.Append('Q')
.Append(quadraticBezierSegment.Point1.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(quadraticBezierSegment.Point1.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ')
.Append(quadraticBezierSegment.Point2.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(quadraticBezierSegment.Point2.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ');
}
else if (pathSegment is ArcSegment arcSegment)
{
sb.Append('A')
.Append(arcSegment.Size.Width)
.Append(',')
.Append(arcSegment.Size.Height)
.Append(' ')
.Append(arcSegment.RotationAngle)
.Append(' ')
.Append(arcSegment.IsLargeArc ? "1" : "0")
.Append(',')
.Append(arcSegment.SweepDirection == SweepDirection.Clockwise ? "1" : "0")
.Append(' ')
.Append(arcSegment.Point.X.ToString(CultureInfo.InvariantCulture))
.Append(',')
.Append(arcSegment.Point.Y.ToString(CultureInfo.InvariantCulture))
.Append(' ');
}
}
if (pathFigure.IsClosed)
{
sb.Append('Z');
}
sb.Append(' ');
}
if (sb.Length > 0)
{
sb.Length--;
if (sb[sb.Length - 1] == ' ')
{
sb.Length--;
}
}
return sb.ToString();
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is PathFigureCollection pathFigureCollection)
{
return ParsePathFigureCollectionToString(pathFigureCollection);
}
throw new InvalidDataException($"Value is not of type {nameof(PathFigureCollection)}");
}
}
}
|