From 64b32f4a7b07c839e201177f120a68e4e1b7abc7 Mon Sep 17 00:00:00 2001 From: Anirudh Agnihotry Date: Wed, 3 Jul 2019 15:59:39 -0700 Subject: [PATCH] Font converter implementation made similar to netfx (dotnet/corefx#39098) * Font converter implementation made similar to netfx * addressing feedback and fixing tests for spanish ci leg * name changes and checking the paramter name in argument Exception * fixing netfx failures and removing some redundant code Commit migrated from https://github.com/dotnet/corefx/commit/9635883cfc69eee3161693715a522ddecca05794 --- .../src/Resources/Strings.resx | 3 + .../src/System/Drawing/FontConverter.cs | 232 ++++++++++++++------- .../tests/System/Drawing/FontConverterTests.cs | 117 +++++++++++ 3 files changed, 277 insertions(+), 75 deletions(-) diff --git a/src/libraries/System.Windows.Extensions/src/Resources/Strings.resx b/src/libraries/System.Windows.Extensions/src/Resources/Strings.resx index 17a6ad2..0d363ab 100644 --- a/src/libraries/System.Windows.Extensions/src/Resources/Strings.resx +++ b/src/libraries/System.Windows.Extensions/src/Resources/Strings.resx @@ -128,6 +128,9 @@ (none) + + Value of '{0}' is not valid for font size unit. + System.Windows.Extensions types are not supported on this platform. diff --git a/src/libraries/System.Windows.Extensions/src/System/Drawing/FontConverter.cs b/src/libraries/System.Windows.Extensions/src/System/Drawing/FontConverter.cs index 3d3898f..1aea2b6 100644 --- a/src/libraries/System.Windows.Extensions/src/System/Drawing/FontConverter.cs +++ b/src/libraries/System.Windows.Extensions/src/System/Drawing/FontConverter.cs @@ -14,6 +14,8 @@ namespace System.Drawing { public class FontConverter : TypeConverter { + private const string StylePrefix = "style="; + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string) ? true : base.CanConvertFrom(context, sourceType); @@ -101,21 +103,15 @@ namespace System.Drawing public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - FontStyle f_style; - float f_size; - GraphicsUnit f_unit; - string font; - string units; - string[] fields; - - if (!(value is string)) + if (!(value is string font)) { return base.ConvertFrom(context, culture, value); } - font = (string)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; @@ -126,94 +122,172 @@ namespace System.Drawing culture = CultureInfo.CurrentCulture; } - // Format is FontFamily, size[[, style=1,2,3]] - // This is a bit tricky since the comma can be used for styles and fields - fields = font.Split(new char[] { culture.TextInfo.ListSeparator[0] }); - if (fields.Length < 1) + 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 = null; + 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) { - throw new ArgumentException("Failed to parse font format"); + return new Font(fontName, fontSize, fontStyle, units); } - font = fields[0]; - f_size = 8f; - units = "px"; - f_unit = GraphicsUnit.Pixel; - if (fields.Length > 1) - { // We have a size - for (int i = 0; i < fields[1].Length; i++) + // Some parameters are provided in addition to name. + fontName = font.Substring(0, 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.Substring(styleIndex, font.Length - styleIndex); + + // Get the mid-substring containing the size information. + sizeStr = font.Substring(nameIndex + 1, styleIndex - nameIndex - 1); + } + else { - if (char.IsLetter(fields[1][i])) + // no style. + sizeStr = font.Substring(nameIndex + 1); + } + + // Parse size. + (string size, string unit) unitTokens = ParseSizeTokens(sizeStr, separator); + + if (unitTokens.size != null) + { + try { - f_size = (float)TypeDescriptor.GetConverter(typeof(float)).ConvertFromString(context, culture, fields[1].Substring(0, i)); - units = fields[1].Substring(i); - break; + fontSize = (float)TypeDescriptor.GetConverter(typeof(float)).ConvertFromString(context, culture, unitTokens.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(sizeStr)); } } - switch (units) + + if (unitTokens.unit != null) + { + // ParseGraphicsUnits throws an ArgumentException if format is invalid. + units = ParseGraphicsUnits(unitTokens.unit); + } + + if (style != null) { - case "display": - f_unit = GraphicsUnit.Display; - break; + // Parse FontStyle + style = style.Substring(6); // style string always starts with style= + string[] styleTokens = style.Split(separator); - case "doc": - f_unit = GraphicsUnit.Document; - break; + for (int tokenCount = 0; tokenCount < styleTokens.Length; tokenCount++) + { + string styleText = styleTokens[tokenCount]; + styleText = styleText.Trim(); - case "pt": - f_unit = GraphicsUnit.Point; - break; + fontStyle |= (FontStyle)Enum.Parse(typeof(FontStyle), styleText, true); - case "in": - f_unit = GraphicsUnit.Inch; - break; + // 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)); + } + } + } + } - case "mm": - f_unit = GraphicsUnit.Millimeter; - break; + return new Font(fontName, fontSize, fontStyle, units); + } - case "px": - f_unit = GraphicsUnit.Pixel; - break; + private (string, string) ParseSizeTokens(string text, char separator) + { + string size = null; + string units = null; - case "world": - f_unit = GraphicsUnit.World; + 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; + } } - } - f_style = FontStyle.Regular; - if (fields.Length > 2) - { // We have style - string compare; + char[] trimChars = new char[] { separator, ' ' }; - for (int i = 2; i < fields.Length; i++) + if (splitPoint > 0) { - compare = fields[i]; + size = text.Substring(0, splitPoint); + // Trimming spaces between size and units. + size = size.Trim(trimChars); + } - if (compare.IndexOf("Regular") != -1) - { - f_style |= FontStyle.Regular; - } - if (compare.IndexOf("Bold") != -1) - { - f_style |= FontStyle.Bold; - } - if (compare.IndexOf("Italic") != -1) - { - f_style |= FontStyle.Italic; - } - if (compare.IndexOf("Strikeout") != -1) - { - f_style |= FontStyle.Strikeout; - } - if (compare.IndexOf("Underline") != -1) - { - f_style |= FontStyle.Underline; - } + if (splitPoint < length) + { + units = text.Substring(splitPoint); + units = units.TrimEnd(trimChars); } } - return new Font(font, f_size, f_style, f_unit); + return (size, units); + } + + private GraphicsUnit ParseGraphicsUnits(string units) + { + GraphicsUnit fontSizeUnit; + switch (units) + { + case "display": + fontSizeUnit = GraphicsUnit.Display; + break; + + case "doc": + fontSizeUnit = GraphicsUnit.Document; + break; + + case "pt": + fontSizeUnit = GraphicsUnit.Point; + break; + + case "in": + fontSizeUnit = GraphicsUnit.Inch; + break; + + case "mm": + fontSizeUnit = GraphicsUnit.Millimeter; + break; + + case "px": + fontSizeUnit = GraphicsUnit.Pixel; + break; + + case "world": + fontSizeUnit = GraphicsUnit.World; + break; + + default: + throw new ArgumentException(SR.Format(SR.InvalidArgumentValue, units), nameof(units)); + } + + return fontSizeUnit; } public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues) @@ -391,7 +465,15 @@ namespace System.Drawing public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) { - return base.GetStandardValues(context); + // display graphic unit is not supported. + if (Values == null) + { + base.GetStandardValues(context); // sets "values" + ArrayList filteredValues = new ArrayList(Values); + filteredValues.Remove(GraphicsUnit.Display); + Values = new StandardValuesCollection(filteredValues); + } + return Values; } } } diff --git a/src/libraries/System.Windows.Extensions/tests/System/Drawing/FontConverterTests.cs b/src/libraries/System.Windows.Extensions/tests/System/Drawing/FontConverterTests.cs index 9074fe5..e1d8abb 100644 --- a/src/libraries/System.Windows.Extensions/tests/System/Drawing/FontConverterTests.cs +++ b/src/libraries/System.Windows.Extensions/tests/System/Drawing/FontConverterTests.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Drawing; +using System.Globalization; using Xunit; +using static System.Drawing.FontConverter; namespace System.ComponentModel.TypeConverterTests { @@ -34,4 +36,119 @@ namespace System.ComponentModel.TypeConverterTests Assert.Throws(() => converter.ConvertFrom(1)); } } + + public class FontConverterTest + { + public static char s_Separator = CultureInfo.CurrentCulture.TextInfo.ListSeparator[0]; + + [ConditionalTheory(Helpers.IsDrawingSupported)] + [MemberData(nameof(TestConvertFormData))] + public void TestConvertFrom(string input, string expectedName, float expectedSize, GraphicsUnit expectedUnits, FontStyle expectedFontStyle) + { + FontConverter converter = new FontConverter(); + Font font = (Font)converter.ConvertFrom(input); + Assert.Equal(expectedName, font.Name); + Assert.Equal(expectedSize, font.Size); + Assert.Equal(expectedUnits, font.Unit); + Assert.Equal(expectedFontStyle, font.Style); + } + + [ConditionalTheory(Helpers.IsDrawingSupported)] + [MemberData(nameof(ArgumentExceptionFontConverterData))] + public void InvalidInputThrowsArgumentException(string input, string paramName, string netfxParamName) + { + FontConverter converter = new FontConverter(); + AssertExtensions.Throws(paramName, netfxParamName, () => converter.ConvertFrom(input)); + } + + [ConditionalTheory(Helpers.IsDrawingSupported)] + [MemberData(nameof(InvalidEnumArgumentExceptionFontConverterData))] + public void InvalidInputThrowsInvalidEnumArgumentException(string input, string paramName) + { + FontConverter converter = new FontConverter(); + Assert.Throws(paramName, () => converter.ConvertFrom(input)); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void EmptyStringInput() + { + FontConverter converter = new FontConverter(); + Font font = (Font)converter.ConvertFrom(string.Empty); + Assert.Null(font); + } + + public static TheoryData TestConvertFormData() + { + var data = new TheoryData() + { + { $"Courier New", "Courier New", 8.25f, GraphicsUnit.Point, FontStyle.Regular }, + { $"Courier New{s_Separator} 11", "Courier New", 11f, GraphicsUnit.Point, FontStyle.Regular }, + { $"Arial{s_Separator} 11px", "Arial", 11f, GraphicsUnit.Pixel, FontStyle.Regular }, + { $"Courier New{s_Separator} 11 px", "Courier New", 11f, GraphicsUnit.Pixel, FontStyle.Regular }, + { $"Courier New{s_Separator} 11 px{s_Separator} style=Regular", "Courier New", 11f, GraphicsUnit.Pixel, FontStyle.Regular }, + { $"Courier New{s_Separator} style=Bold", "Courier New", 8.25f, GraphicsUnit.Point, FontStyle.Bold }, + { $"Courier New{s_Separator} 11 px{s_Separator} style=Bold{s_Separator} Italic", "Courier New", 11f, GraphicsUnit.Pixel, FontStyle.Bold | FontStyle.Italic }, + { $"Courier New{s_Separator} 11 px{s_Separator} style=Regular, Italic", "Courier New", 11f, GraphicsUnit.Pixel, FontStyle.Regular | FontStyle.Italic }, + { $"Courier New{s_Separator} 11 px{s_Separator} style=Bold{s_Separator} Italic{s_Separator} Strikeout", "Courier New", 11f, GraphicsUnit.Pixel, FontStyle.Bold | FontStyle.Italic | FontStyle.Strikeout }, + { $"Arial{s_Separator} 11 px{s_Separator} style=Bold, Italic, Strikeout", "Arial", 11f, GraphicsUnit.Pixel, FontStyle.Bold | FontStyle.Italic | FontStyle.Strikeout }, + { $"11px", "Microsoft Sans Serif", 8.25f, GraphicsUnit.Point, FontStyle.Regular }, + { $"Style=Bold", "Microsoft Sans Serif", 8.25f, GraphicsUnit.Point, FontStyle.Regular }, + { $"arIAL{s_Separator} 10{s_Separator} style=bold", "Arial", 10f, GraphicsUnit.Point, FontStyle.Bold }, + { $"Arial{s_Separator} 10{s_Separator}", "Arial", 10f, GraphicsUnit.Point, FontStyle.Regular }, + { $"Arial{s_Separator}", "Arial", 8.25f, GraphicsUnit.Point, FontStyle.Regular }, + { $"Arial{s_Separator} 10{s_Separator} style=12", "Arial", 10f, GraphicsUnit.Point, FontStyle.Underline | FontStyle.Strikeout }, + }; + + if (!PlatformDetection.IsFullFramework) + { + // FullFramework style keyword is case sensitive. + data.Add($"Courier New{s_Separator} Style=Bold", "Courier New", 8.25f, GraphicsUnit.Point, FontStyle.Bold); + data.Add($"11px{s_Separator} Style=Bold", "Microsoft Sans Serif", 8.25f, GraphicsUnit.Point, FontStyle.Bold); + + // FullFramework disregards all arguments if the font name is an empty string. + if (PlatformDetection.IsWindows10Version1607OrGreater) + { + data.Add($"{s_Separator} 10{s_Separator} style=bold", "", 10f, GraphicsUnit.Point, FontStyle.Bold); // empty string is not a installed font on Windows 7 and windows 8. + } + } + + return data; + } + + public static TheoryData ArgumentExceptionFontConverterData() => new TheoryData() + { + { $"Courier New{s_Separator} 11 px{s_Separator} type=Bold{s_Separator} Italic", "units", null }, + { $"Courier New{s_Separator} {s_Separator} Style=Bold", "sizeStr", null }, + { $"Courier New{s_Separator} 11{s_Separator} Style=", "value", null }, + { $"Courier New{s_Separator} 11{s_Separator} Style=RandomEnum", null, null }, + { $"Arial{s_Separator} 10{s_Separator} style=bold{s_Separator}", "value", null }, + { $"Arial{s_Separator} 10{s_Separator} style=null", null, null }, + { $"Arial{s_Separator} 10{s_Separator} style=abc#", null, null }, + { $"Arial{s_Separator} 10{s_Separator} style=##", null, null }, + { $"Arial{s_Separator} 10display{s_Separator} style=bold", null, null }, + { $"Arial{s_Separator} 10style{s_Separator} style=bold", "units", null }, + }; + + public static TheoryData InvalidEnumArgumentExceptionFontConverterData() => new TheoryData() + { + { $"Arial{s_Separator} 10{s_Separator} style=56", "style" }, + { $"Arial{s_Separator} 10{s_Separator} style=-1", "style" }, + }; + } + + public class FontUnitConverterTest + { + [ConditionalFact(Helpers.IsDrawingSupported)] + public void GetStandardValuesTest() + { + FontUnitConverter converter = new FontUnitConverter(); + var values = converter.GetStandardValues(); + Assert.Equal(6, values.Count); // The six supported values of Graphics unit: World, Pixel, Point, Inch, Document, Millimeter. + + foreach (var item in values) + { + Assert.NotEqual(GraphicsUnit.Display, (GraphicsUnit)item); + } + } + } } -- 2.7.4