ImageSharp/SixLabors.Fonts/FontCollection.cs
2026-08-03 22:31:27 +02:00

304 lines
12 KiB
C#

// 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 {
/// <summary>
/// Represents a collection of font families.
/// </summary>
public sealed class FontCollection : IFontCollection, IFontMetricsCollection
{
private readonly HashSet<string> searchDirectories = [];
private readonly HashSet<FontMetrics> metricsCollection = [];
/// <summary>
/// Initializes a new instance of the <see cref="FontCollection"/> class.
/// </summary>
public FontCollection()
: this(Array.Empty<string>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FontCollection"/> class.
/// </summary>
/// <param name="searchDirectories">The collection of directories used to search for font families.</param>
/// <remarks>
/// 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
/// <see cref="FontFamilyNotFoundException"/> will include the searched directories.
/// </remarks>
internal FontCollection(IReadOnlyCollection<string> searchDirectories)
{
Guard.NotNull(searchDirectories, nameof(searchDirectories));
foreach (string? dir in searchDirectories)
{
this.searchDirectories.Add(dir);
}
}
/// <inheritdoc/>
public IEnumerable<FontFamily> Families => this.FamiliesByCultureImpl(CultureInfo.InvariantCulture);
/// <inheritdoc/>
public FontFamily Add(string path)
=> this.Add(path, out _);
/// <inheritdoc/>
public FontFamily Add(string path, out FontDescription description)
=> this.AddImpl(path, CultureInfo.InvariantCulture, out description);
/// <inheritdoc/>
public FontFamily Add(Stream stream)
=> this.Add(stream, out _);
/// <inheritdoc/>
public FontFamily Add(Stream stream, out FontDescription description)
=> this.AddImpl(stream, CultureInfo.InvariantCulture, out description);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(string path)
=> this.AddCollection(path, out _);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(string path, out ReadOnlyMemory<FontDescription> descriptions)
=> this.AddCollectionImpl(path, CultureInfo.InvariantCulture, out descriptions);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(Stream stream)
=> this.AddCollection(stream, out _);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(Stream stream, out ReadOnlyMemory<FontDescription> descriptions)
=> this.AddCollectionImpl(stream, CultureInfo.InvariantCulture, out descriptions);
/// <inheritdoc/>
public FontFamily Get(string name)
=> this.GetByCulture(name, CultureInfo.InvariantCulture);
/// <inheritdoc/>
public bool TryGet(string name, out FontFamily family)
=> this.TryGetByCulture(name, CultureInfo.InvariantCulture, out family);
/// <inheritdoc/>
public FontFamily AddWithCulture(string path, CultureInfo culture)
=> this.AddImpl(path, culture, out _);
/// <inheritdoc/>
public FontFamily AddWithCulture(string path, CultureInfo culture, out FontDescription description)
=> this.AddImpl(path, culture, out description);
/// <inheritdoc/>
public FontFamily AddWithCulture(Stream stream, CultureInfo culture)
=> this.AddImpl(stream, culture, out _);
/// <inheritdoc/>
public FontFamily AddWithCulture(Stream stream, CultureInfo culture, out FontDescription description)
=> this.AddImpl(stream, culture, out description);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(string path, CultureInfo culture)
=> this.AddCollection(path, culture, out _);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(
string path,
CultureInfo culture,
out ReadOnlyMemory<FontDescription> descriptions)
=> this.AddCollectionImpl(path, culture, out descriptions);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(Stream stream, CultureInfo culture)
=> this.AddCollection(stream, culture, out _);
/// <inheritdoc/>
public ReadOnlyMemory<FontFamily> AddCollection(
Stream stream,
CultureInfo culture,
out ReadOnlyMemory<FontDescription> descriptions)
=> this.AddCollectionImpl(stream, culture, out descriptions);
/// <inheritdoc/>
public IEnumerable<FontFamily> GetByCulture(CultureInfo culture)
=> this.FamiliesByCultureImpl(culture);
/// <inheritdoc/>
public FontFamily GetByCulture(string name, CultureInfo culture)
=> this.GetImpl(name, culture);
/// <inheritdoc/>
public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family)
=> this.TryGetImpl(name, culture, out family);
/// <inheritdoc/>
FontFamily IFontMetricsCollection.AddMetrics(FontMetrics metrics, CultureInfo culture)
{
((IFontMetricsCollection)this).AddMetrics(metrics);
return new FontFamily(metrics.Description.FontFamily(culture), this, culture);
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
IEnumerable<FontMetrics> 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();
}
/// <inheritdoc/>
ReadOnlyMemory<FontStyle> IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture)
=> ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture).Select(x => x.Description.Style).ToArray();
/// <inheritdoc/>
IEnumerator<FontMetrics> IReadOnlyFontMetricsCollection.GetEnumerator()
=> this.metricsCollection.GetEnumerator();
internal void AddSearchDirectories(IEnumerable<string> 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<FontFamily> AddCollectionImpl(
string path,
CultureInfo culture,
out ReadOnlyMemory<FontDescription> descriptions)
{
ReadOnlyMemory<FileFontMetrics> fontMetrics = FileFontMetrics.LoadFontCollection(path);
ReadOnlySpan<FileFontMetrics> 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<FontFamily>(families, 0, familyCount);
}
private ReadOnlyMemory<FontFamily> AddCollectionImpl(
Stream stream,
CultureInfo culture,
out ReadOnlyMemory<FontDescription> 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<FontFamily>(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);
}
}
}