// 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);
}
}
}