File: Fonts\FontFile.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
#nullable enable
using System;
using System.Collections.Generic;
 
namespace Microsoft.Maui
{
	/// <summary>
	/// Represents a font file.
	/// </summary>
	public class FontFile
	{
		/// <summary>
		/// Supported extensions for <see cref="FontFile"/>.
		/// </summary>
		public static readonly string[] Extensions = { ".ttf", ".otf", };
 
		/// <summary>
		/// The filename for this font.
		/// </summary>
		/// <remarks>This should not include the extension. If you want the filename including the extension use <see cref="FileNameWithExtension()"/>.</remarks>
		public string? FileName { get; set; }
 
		/// <summary>
		/// The filename extension for this font.
		/// </summary>
		public string? Extension { get; set; }
 
		/// <summary>
		/// The font PostScript name as read from the font file.
		/// </summary>
		/// <remarks>Some platforms have issues with spaces in the PostScript name. In this property spaces might be stripped.
		/// To get the PostScript name with spaces (if it had any) use <see cref="GetPostScriptNameWithSpaces"/>.</remarks>
		public string? PostScriptName { get; set; }
 
		/// <summary>
		/// Gets the filename of this font file with the provided extension appended to the end.
		/// </summary>
		/// <param name="extension">The extension to append to the font filename.</param>
		/// <returns>The filename of this font file including the given extension.</returns>
		/// <remarks>The value for <paramref name="extension"/> should include a leading dot (.) character.</remarks>
		public string FileNameWithExtension(string? extension) => $"{FileName}{extension}";
 
		/// <summary>
		/// Gets the filename of this font file, including the extension.
		/// </summary>
		/// <returns>The filename of this font file, including the extension.</returns>
		/// <remarks>This returns the combination of <see cref="FileName"/> and <see cref="Extension"/>.</remarks>
		public string FileNameWithExtension() => FileNameWithExtension(Extension);
 
		/// <summary>
		/// Gets the font PostScript name as read from the font file, including any spaces if it had any.
		/// </summary>
		/// <returns>The font PostScript name including spaces.</returns>
		public string GetPostScriptNameWithSpaces() => string.Join(" ", GetFontName(PostScriptName!));
 
		/// <summary>
		/// Creates a new instance of a <see cref="FontFile"/> object based on the value in <paramref name="input"/>.
		/// </summary>
		/// <param name="input">Can either be a filename or font family name.</param>
		/// <returns>A new <see cref="FontFile"/> object with all the information that could be deduced through <paramref name="input"/>.</returns>
		/// <exception cref="ArgumentNullException">Thrown if <paramref name="input"/> is <see langword="null"/></exception>
		public static FontFile FromString(string input)
		{
			_ = input ?? throw new ArgumentNullException(nameof(input));
 
			var hashIndex = input.IndexOfChar('#');
			//UWP names require Spaces. Sometimes people may use those, "CuteFont-Regular#Cute Font" should be "CuteFont-Regular#CuteFont"
			var postScriptName = hashIndex > 0 ? input.Substring(hashIndex + 1)
#if NETSTANDARD2_0
				.Replace(" ", "")
#else
				.Replace(" ", "", StringComparison.Ordinal)
#endif
				: input;
			//Get the fontFamily name;
			var fontFamilyName = hashIndex > 0 ? input.Substring(0, hashIndex) : input;
 
			string? foundExtension = null;
			foreach (var extension in Extensions)
			{
				if (fontFamilyName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
				{
					foundExtension = extension;
					fontFamilyName = fontFamilyName.Substring(0, fontFamilyName.Length - foundExtension.Length);
					break;
				}
			}
 
			return new FontFile
			{
				FileName = fontFamilyName,
				Extension = foundExtension,
				PostScriptName = postScriptName,
			};
		}
 
		static IEnumerable<string> GetFontName(string fontFamily)
		{
			_ = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily));
 
			if (fontFamily.ContainsChar(' '))
			{
				yield return fontFamily;
				//We are done, they have spaces, they have it handled.
				yield break;
			}
 
			string currentString = "";
			char lastCharacter = ' ';
			var index = fontFamily.LastIndexOf('-');
			bool multipleCaps = false;
			var cleansedString = index > 0 ? fontFamily.Substring(0, index) : fontFamily;
			foreach (var c in cleansedString)
			{
				//Always break on these characters
				if (c == '_' || c == '-')
				{
					yield return currentString;
					//Reset everything,
					currentString = "";
					lastCharacter = ' ';
					multipleCaps = false;
				}
				else
				{
					if (char.IsUpper(c))
					{
						//If the last character is lowercase, we are in a new CamelCase font
						if (char.IsLower(lastCharacter))
						{
							yield return currentString;
							currentString = "";
							lastCharacter = ' ';
						}
						else if (char.IsUpper(lastCharacter))
						{
							multipleCaps = true;
						}
					}
 
					//Detect multiple UpperCase letters so we can separate things like PTSansNarrow into "PT Sans Narrow"
					else if (multipleCaps && currentString.Length > 1)
					{
						var last = currentString[currentString.Length - 1];
						yield return currentString.Substring(0, currentString.Length - 1);
						//Reset everything so it doesnt do a space
						multipleCaps = false;
						lastCharacter = ' ';
						currentString = last.ToString();
					}
 
					currentString += c;
					lastCharacter = c;
				}
			}
 
			//Send what is left!
			if (!string.IsNullOrWhiteSpace(currentString))
				yield return currentString.Trim();
		}
	}
}