|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Drawing.Text;
using System.Globalization;
using System.Text;
namespace System.Drawing;
public class FontConverter : TypeConverter
{
private const string StylePrefix = "style=";
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
return (destinationType == typeof(string)) || (destinationType == typeof(InstanceDescriptor));
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is Font font)
{
if (destinationType == typeof(string))
{
culture ??= CultureInfo.CurrentCulture;
ValueStringBuilder sb = default;
sb.Append(font.Name);
sb.Append(culture.TextInfo.ListSeparator[0]);
sb.Append(' ');
sb.Append(font.Size.ToString(culture.NumberFormat));
switch (font.Unit)
{
// MS throws ArgumentException, if unit is set
// to GraphicsUnit.Display
// Don't know what to append for GraphicsUnit.Display
case GraphicsUnit.Display:
sb.Append("display");
break;
case GraphicsUnit.Document:
sb.Append("doc");
break;
case GraphicsUnit.Point:
sb.Append("pt");
break;
case GraphicsUnit.Inch:
sb.Append("in");
break;
case GraphicsUnit.Millimeter:
sb.Append("mm");
break;
case GraphicsUnit.Pixel:
sb.Append("px");
break;
case GraphicsUnit.World:
sb.Append("world");
break;
}
if (font.Style != FontStyle.Regular)
{
sb.Append(culture.TextInfo.ListSeparator[0]);
sb.Append(" style=");
sb.Append(font.Style.ToString());
}
return sb.ToString();
}
if (destinationType == typeof(InstanceDescriptor))
{
// Generate the smallest constructor possible.
int argCount = 2;
if (font.GdiVerticalFont)
{
argCount = 6;
}
else if (font.GdiCharSet != (byte)FONT_CHARSET.DEFAULT_CHARSET)
{
argCount = 5;
}
else if (font.Unit != GraphicsUnit.Point)
{
argCount = 4;
}
else if (font.Style != FontStyle.Regular)
{
argCount++;
}
object[] args = new object[argCount];
Type[] types = new Type[argCount];
args[0] = font.Name;
types[0] = typeof(string);
args[1] = font.Size;
types[1] = typeof(float);
if (argCount > 2)
{
args[2] = font.Style;
types[2] = typeof(FontStyle);
}
if (argCount > 3)
{
args[3] = font.Unit;
types[3] = typeof(GraphicsUnit);
}
if (argCount > 4)
{
args[4] = font.GdiCharSet;
types[4] = typeof(byte);
}
if (argCount > 5)
{
args[5] = font.GdiVerticalFont;
types[5] = typeof(bool);
}
if (typeof(Font).GetConstructor(types) is { } constructor)
{
return new InstanceDescriptor(constructor, args);
}
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is not string font)
{
return base.ConvertFrom(context, culture, value);
}
font = font.Trim();
// Expected string format: "name[, size[, units[, style=style1[, style2[...]]]]]"
// Example using 'vi-VN' culture: "Microsoft Sans Serif, 8,25pt, style=Italic, Bold"
if (font.Length == 0)
{
return null;
}
culture ??= CultureInfo.CurrentCulture;
char separator = culture.TextInfo.ListSeparator[0]; // For vi-VN: ','
string fontName = font; // start with the assumption that only the font name was provided.
string? style = null;
string? sizeStr;
float fontSize = 8.25f;
FontStyle fontStyle = FontStyle.Regular;
GraphicsUnit units = GraphicsUnit.Point;
// Get the index of the first separator (would indicate the end of the name in the string).
int nameIndex = font.IndexOf(separator);
if (nameIndex < 0)
{
return new Font(fontName, fontSize, fontStyle, units);
}
// Some parameters are provided in addition to name.
fontName = font[..nameIndex];
if (nameIndex < font.Length - 1)
{
// Get the style index (if any). The size is a bit problematic because it can be formatted differently
// depending on the culture, we'll parse it last.
int styleIndex = culture.CompareInfo.IndexOf(font, StylePrefix, CompareOptions.IgnoreCase);
if (styleIndex != -1)
{
// style found.
style = font[styleIndex..];
// Get the mid-substring containing the size information.
sizeStr = font.Substring(nameIndex + 1, styleIndex - nameIndex - 1);
}
else
{
// no style.
sizeStr = font[(nameIndex + 1)..];
}
// Parse size.
(string? size, string? unit) = ParseSizeTokens(sizeStr, separator);
if (size is not null)
{
try
{
fontSize = (float)GetFloatConverter().ConvertFromString(context, culture, size)!;
}
catch
{
// Exception from converter is too generic.
throw new ArgumentException(SR.Format(SR.TextParseFailedFormat, font, $"name{separator} size[units[{separator} style=style1[{separator} style2{separator} ...]]]"), nameof(value));
}
}
if (unit is not null)
{
// ParseGraphicsUnits throws an ArgumentException if format is invalid.
units = ParseGraphicsUnits(unit);
}
if (style is not null)
{
// Parse FontStyle
style = style[6..]; // style string always starts with style=
string[] styleTokens = style.Split(separator);
for (int tokenCount = 0; tokenCount < styleTokens.Length; tokenCount++)
{
string styleText = styleTokens[tokenCount];
styleText = styleText.Trim();
fontStyle |= Enum.Parse<FontStyle>(styleText, true);
// Enum.IsDefined doesn't do what we want on flags enums...
FontStyle validBits = FontStyle.Regular | FontStyle.Bold | FontStyle.Italic | FontStyle.Underline | FontStyle.Strikeout;
if ((fontStyle | validBits) != validBits)
{
throw new InvalidEnumArgumentException(nameof(style), (int)fontStyle, typeof(FontStyle));
}
}
}
}
return new Font(fontName, fontSize, fontStyle, units);
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "TypeDescriptor.GetConverter is safe for primitive types.")]
static TypeConverter GetFloatConverter() => TypeDescriptor.GetConverter(typeof(float));
}
private static (string?, string?) ParseSizeTokens(string text, char separator)
{
string? size = null;
string? units = null;
text = text.Trim();
int length = text.Length;
int splitPoint;
if (length > 0)
{
// text is expected to have a format like " 8,25pt, ". Leading and trailing spaces (trimmed above),
// last comma, unit and decimal value may not appear. We need to make it ####.##CC
for (splitPoint = 0; splitPoint < length; splitPoint++)
{
if (char.IsLetter(text[splitPoint]))
{
break;
}
}
char[] trimChars = [separator, ' '];
if (splitPoint > 0)
{
size = text[..splitPoint];
// Trimming spaces between size and units.
size = size.Trim(trimChars);
}
if (splitPoint < length)
{
units = text[splitPoint..];
units = units.TrimEnd(trimChars);
}
}
return (size, units);
}
private static GraphicsUnit ParseGraphicsUnits(string units) =>
units switch
{
"display" => GraphicsUnit.Display,
"doc" => GraphicsUnit.Document,
"pt" => GraphicsUnit.Point,
"in" => GraphicsUnit.Inch,
"mm" => GraphicsUnit.Millimeter,
"px" => GraphicsUnit.Pixel,
"world" => GraphicsUnit.World,
_ => throw new ArgumentException(SR.Format(SR.InvalidArgumentValueFontConverter, units), nameof(units)),
};
public override object CreateInstance(ITypeDescriptorContext? context, IDictionary propertyValues)
{
ArgumentNullException.ThrowIfNull(propertyValues);
object? value;
byte charSet = 1;
float size = 8;
string? name = null;
bool vertical = false;
FontStyle style = FontStyle.Regular;
FontFamily? fontFamily = null;
GraphicsUnit unit = GraphicsUnit.Point;
if ((value = propertyValues["GdiCharSet"]) is not null)
charSet = (byte)value;
if ((value = propertyValues["Size"]) is not null)
size = (float)value;
if ((value = propertyValues["Unit"]) is not null)
unit = (GraphicsUnit)value;
if ((value = propertyValues["Name"]) is not null)
name = (string)value;
if ((value = propertyValues["GdiVerticalFont"]) is not null)
vertical = (bool)value;
if ((value = propertyValues["Bold"]) is not null)
{
if ((bool)value)
style |= FontStyle.Bold;
}
if ((value = propertyValues["Italic"]) is not null)
{
if ((bool)value)
style |= FontStyle.Italic;
}
if ((value = propertyValues["Strikeout"]) is not null)
{
if ((bool)value)
style |= FontStyle.Strikeout;
}
if ((value = propertyValues["Underline"]) is not null)
{
if ((bool)value)
style |= FontStyle.Underline;
}
if (name is null)
{
fontFamily = new FontFamily("Tahoma");
}
else
{
FontCollection collection = InstalledFontCollection.Instance;
FontFamily[] installedFontList = collection.Families;
foreach (FontFamily font in installedFontList)
{
if (name.Equals(font.Name, StringComparison.OrdinalIgnoreCase))
{
fontFamily = font;
break;
}
}
fontFamily ??= FontFamily.GenericSansSerif;
}
return new Font(fontFamily, size, style, unit, charSet, vertical);
}
public override bool GetCreateInstanceSupported(ITypeDescriptorContext? context) => true;
[RequiresUnreferencedCode("The Type of value cannot be statically discovered. The public parameterless constructor or the 'Default' static field may be trimmed from the Attribute's Type.")]
public override PropertyDescriptorCollection? GetProperties(
ITypeDescriptorContext? context,
object value,
Attribute[]? attributes)
{
if (value is not Font)
return base.GetProperties(context, value, attributes);
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(value, attributes);
return props.Sort([nameof(Font.Name), nameof(Font.Size), nameof(Font.Unit)]);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
public sealed class FontNameConverter : TypeConverter, IDisposable
{
private readonly FontFamily[] _fonts;
public FontNameConverter()
{
_fonts = FontFamily.Families;
}
void IDisposable.Dispose()
{
}
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) =>
value is string strValue ? MatchFontName(strValue, context) : base.ConvertFrom(context, culture, value);
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
{
string[] values = new string[_fonts.Length];
for (int i = 0; i < _fonts.Length; i++)
{
values[i] = _fonts[i].Name;
}
Array.Sort(values, Comparer.Default);
return new StandardValuesCollection(values);
}
// We allow other values other than those in the font list.
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => false;
// Yes, we support picking an element from the list.
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
private string MatchFontName(string name, ITypeDescriptorContext? context)
{
// Try a partial match
string? bestMatch = null;
// setting fontName as nullable since IEnumerable.Current returned nullable in 3.0
foreach (string? fontName in GetStandardValues(context))
{
Debug.Assert(fontName is not null);
if (fontName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
{
// For an exact match, return immediately
return fontName;
}
if (fontName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
if (bestMatch is null || fontName.Length <= bestMatch.Length)
{
bestMatch = fontName;
}
}
}
// No match... fall back on whatever was provided
return bestMatch ?? name;
}
}
public class FontUnitConverter : EnumConverter
{
public FontUnitConverter() : base(typeof(GraphicsUnit)) { }
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
{
// display graphic unit is not supported.
if (Values is null)
{
base.GetStandardValues(context); // sets "values"
Debug.Assert(Values is not null);
ArrayList filteredValues = new(Values);
filteredValues.Remove(GraphicsUnit.Display);
Values = new StandardValuesCollection(filteredValues);
}
return Values;
}
}
}
|