// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using SixLabors.Fonts.Tables; namespace SixLabors.Fonts { /// /// Represents a collection of font families. /// public sealed class FontCollection : IFontCollection, IFontMetricsCollection { private readonly HashSet searchDirectories = []; private readonly HashSet metricsCollection = []; /// /// Initializes a new instance of the class. /// public FontCollection() : this(Array.Empty()) { } /// /// Initializes a new instance of the class. /// /// The collection of directories used to search for font families. /// /// Use this constructor instead of the parameterless constructor if the fonts added to that collection /// are actually added after searching inside physical file system directories. The message of the /// will include the searched directories. /// internal FontCollection(IReadOnlyCollection searchDirectories) { Guard.NotNull(searchDirectories, nameof(searchDirectories)); foreach (string? dir in searchDirectories) { this.searchDirectories.Add(dir); } } /// public IEnumerable Families => this.FamiliesByCultureImpl(CultureInfo.InvariantCulture); /// public FontFamily Add(string path) => this.Add(path, out _); /// public FontFamily Add(string path, out FontDescription description) => this.AddImpl(path, CultureInfo.InvariantCulture, out description); /// public FontFamily Add(Stream stream) => this.Add(stream, out _); /// public FontFamily Add(Stream stream, out FontDescription description) => this.AddImpl(stream, CultureInfo.InvariantCulture, out description); /// public ReadOnlyMemory AddCollection(string path) => this.AddCollection(path, out _); /// public ReadOnlyMemory AddCollection(string path, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(path, CultureInfo.InvariantCulture, out descriptions); /// public ReadOnlyMemory AddCollection(Stream stream) => this.AddCollection(stream, out _); /// public ReadOnlyMemory AddCollection(Stream stream, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(stream, CultureInfo.InvariantCulture, out descriptions); /// public FontFamily Get(string name) => this.GetByCulture(name, CultureInfo.InvariantCulture); /// public bool TryGet(string name, out FontFamily family) => this.TryGetByCulture(name, CultureInfo.InvariantCulture, out family); /// public FontFamily AddWithCulture(string path, CultureInfo culture) => this.AddImpl(path, culture, out _); /// public FontFamily AddWithCulture(string path, CultureInfo culture, out FontDescription description) => this.AddImpl(path, culture, out description); /// public FontFamily AddWithCulture(Stream stream, CultureInfo culture) => this.AddImpl(stream, culture, out _); /// public FontFamily AddWithCulture(Stream stream, CultureInfo culture, out FontDescription description) => this.AddImpl(stream, culture, out description); /// public ReadOnlyMemory AddCollection(string path, CultureInfo culture) => this.AddCollection(path, culture, out _); /// public ReadOnlyMemory AddCollection( string path, CultureInfo culture, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(path, culture, out descriptions); /// public ReadOnlyMemory AddCollection(Stream stream, CultureInfo culture) => this.AddCollection(stream, culture, out _); /// public ReadOnlyMemory AddCollection( Stream stream, CultureInfo culture, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(stream, culture, out descriptions); /// public IEnumerable GetByCulture(CultureInfo culture) => this.FamiliesByCultureImpl(culture); /// public FontFamily GetByCulture(string name, CultureInfo culture) => this.GetImpl(name, culture); /// public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) => this.TryGetImpl(name, culture, out family); /// FontFamily IFontMetricsCollection.AddMetrics(FontMetrics metrics, CultureInfo culture) { ((IFontMetricsCollection)this).AddMetrics(metrics); return new FontFamily(metrics.Description.FontFamily(culture), this, culture); } /// void IFontMetricsCollection.AddMetrics(FontMetrics metrics) { Guard.NotNull(metrics, nameof(metrics)); if (metrics.Description is null) { throw new ArgumentException($"{nameof(FontMetrics)} must have a Description.", nameof(metrics)); } lock (this.metricsCollection) { this.metricsCollection.Add(metrics); } } /// bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) { metrics = ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture) .FirstOrDefault(x => x.Description.Style == style); return metrics != null; } /// IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string name, CultureInfo culture) { Guard.NotNull(name, nameof(name)); StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); return this.metricsCollection .Where(x => comparer.Equals(x.Description.FontFamily(culture), name)) .ToArray(); } /// ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) => ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture).Select(x => x.Description.Style).ToArray(); /// IEnumerator IReadOnlyFontMetricsCollection.GetEnumerator() => this.metricsCollection.GetEnumerator(); internal void AddSearchDirectories(IEnumerable directories) { foreach (string? directory in directories) { this.searchDirectories.Add(directory); } } private FontFamily AddImpl(string path, CultureInfo culture, out FontDescription description) { FileFontMetrics instance = new(path); description = instance.Description; return ((IFontMetricsCollection)this).AddMetrics(instance, culture); } private FontFamily AddImpl(Stream stream, CultureInfo culture, out FontDescription description) { StreamFontMetrics metrics = StreamFontMetrics.LoadFont(stream); description = metrics.Description; return ((IFontMetricsCollection)this).AddMetrics(metrics, culture); } private ReadOnlyMemory AddCollectionImpl( string path, CultureInfo culture, out ReadOnlyMemory descriptions) { ReadOnlyMemory fontMetrics = FileFontMetrics.LoadFontCollection(path); ReadOnlySpan fonts = fontMetrics.Span; FontDescription[] description = new FontDescription[fonts.Length]; FontFamily[] families = new FontFamily[fonts.Length]; int familyCount = 0; for (int i = 0; i < fonts.Length; i++) { description[i] = fonts[i].Description; FontFamily family = ((IFontMetricsCollection)this).AddMetrics(fonts[i], culture); if (!families.AsSpan(0, familyCount).Contains(family)) { families[familyCount++] = family; } } descriptions = description; return new ReadOnlyMemory(families, 0, familyCount); } private ReadOnlyMemory AddCollectionImpl( Stream stream, CultureInfo culture, out ReadOnlyMemory descriptions) { long startPos = stream.Position; using BigEndianBinaryReader reader = new(stream, true); TtcHeader ttcHeader = TtcHeader.Read(reader); FontDescription[] result = new FontDescription[(int)ttcHeader.NumFonts]; FontFamily[] installedFamilies = new FontFamily[(int)ttcHeader.NumFonts]; int familyCount = 0; for (int i = 0; i < ttcHeader.NumFonts; ++i) { stream.Position = startPos + ttcHeader.OffsetTable[i]; StreamFontMetrics instance = StreamFontMetrics.LoadFont(stream); FontFamily family = ((IFontMetricsCollection)this).AddMetrics(instance, culture); result[i] = instance.Description; if (!installedFamilies.AsSpan(0, familyCount).Contains(family)) { installedFamilies[familyCount++] = family; } } descriptions = result; return new ReadOnlyMemory(installedFamilies, 0, familyCount); } private FontFamily[] FamiliesByCultureImpl(CultureInfo culture) => [.. this.metricsCollection .Select(x => x.Description.FontFamily(culture)) .Distinct() .Select(x => new FontFamily(x, this, culture))]; private bool TryGetImpl(string name, CultureInfo culture, out FontFamily family) { Guard.NotNull(name, nameof(name)); StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); string? match = this.metricsCollection .Select(x => x.Description.FontFamily(culture)) .FirstOrDefault(x => comparer.Equals(name, x)); if (match != null) { family = new FontFamily(match, this, culture); return true; } family = default; return false; } private FontFamily GetImpl(string name, CultureInfo culture) { if (this.TryGetImpl(name, culture, out FontFamily family)) { return family; } throw new FontFamilyNotFoundException(name, this.searchDirectories); } } }