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

376 lines
16 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts {
/// <summary>
/// Defines a particular format for text, including font face, size, and style attributes.
/// This class cannot be inherited.
/// </summary>
public sealed class Font
{
private readonly FontVariation[] variations;
private readonly Lazy<FontMetrics?> metrics;
private readonly Lazy<string> fontName;
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class.
/// </summary>
/// <param name="family">The font family.</param>
/// <param name="size">The size of the font in PT units.</param>
public Font(FontFamily family, float size)
: this(family, size, FontStyle.Regular)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class.
/// </summary>
/// <param name="family">The font family.</param>
/// <param name="size">The size of the font in PT units.</param>
/// <param name="style">The font style.</param>
public Font(FontFamily family, float size, FontStyle style)
{
if (family == default)
{
throw new ArgumentException("Cannot use the default value type instance to create a font.", nameof(family));
}
this.Family = family;
this.RequestedStyle = style;
this.Size = size;
this.variations = [];
this.metrics = new Lazy<FontMetrics?>(this.LoadInstanceInternal, true);
this.fontName = new Lazy<string>(this.LoadFontName, true);
}
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class.
/// </summary>
/// <param name="prototype">The prototype.</param>
/// <param name="style">The font style.</param>
public Font(Font prototype, FontStyle style)
: this(prototype?.Family ?? throw new ArgumentNullException(nameof(prototype)), prototype.Size, style)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class.
/// </summary>
/// <param name="prototype">The prototype.</param>
/// <param name="size">The size of the font in PT units.</param>
/// <param name="style">The font style.</param>
public Font(Font prototype, float size, FontStyle style)
: this(prototype?.Family ?? throw new ArgumentNullException(nameof(prototype)), size, style)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class.
/// </summary>
/// <param name="prototype">The prototype.</param>
/// <param name="size">The size of the font in PT units.</param>
public Font(Font prototype, float size)
: this(prototype.Family, size, prototype.RequestedStyle)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class with the specified variation axis settings.
/// </summary>
/// <param name="prototype">The prototype font providing family, size, and style.</param>
/// <param name="variations">The variation axis settings to apply.</param>
public Font(Font prototype, params FontVariation[] variations)
{
Guard.NotNull(prototype, nameof(prototype));
Guard.NotNull(variations, nameof(variations));
this.Family = prototype.Family;
this.RequestedStyle = prototype.RequestedStyle;
this.Size = prototype.Size;
this.variations = variations;
this.metrics = new Lazy<FontMetrics?>(this.LoadInstanceInternal, true);
this.fontName = new Lazy<string>(this.LoadFontName, true);
}
/// <summary>
/// Gets the family.
/// </summary>
public FontFamily Family { get; }
/// <summary>
/// Gets the name.
/// </summary>
public string Name => this.fontName.Value;
/// <summary>
/// Gets the size of the font in PT units.
/// </summary>
public float Size { get; }
/// <summary>
/// Gets the font metrics.
/// </summary>
/// <exception cref="FontException">Font instance not found.</exception>
public FontMetrics FontMetrics => this.metrics.Value ?? throw new FontException("Font instance not found.");
/// <summary>
/// Gets a value indicating whether this <see cref="Font"/> is bold.
/// </summary>
public bool IsBold => (this.FontMetrics.Description.Style & FontStyle.Bold) == FontStyle.Bold;
/// <summary>
/// Gets a value indicating whether this <see cref="Font"/> is italic.
/// </summary>
public bool IsItalic => (this.FontMetrics.Description.Style & FontStyle.Italic) == FontStyle.Italic;
/// <summary>
/// Gets the variation axis settings applied to this font.
/// </summary>
public ReadOnlySpan<FontVariation> Variations => this.variations;
/// <summary>
/// Gets the requested style.
/// </summary>
internal FontStyle RequestedStyle { get; }
/// <summary>
/// Gets the filesystem path to the font family source.
/// </summary>
/// <param name="path">
/// When this method returns, contains the filesystem path to the font family source,
/// if the path exists; otherwise, the default value for the type of the path parameter.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if the <see cref="Font" /> was created via a filesystem path; otherwise, <see langword="false" />.
/// </returns>
public bool TryGetPath([NotNullWhen(true)] out string? path)
{
if (this == default)
{
FontsThrowHelper.ThrowDefaultInstance();
}
if (this.FontMetrics is FileFontMetrics fileMetrics)
{
path = fileMetrics.Path;
return true;
}
path = null;
return false;
}
/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="glyph">
/// When this method returns, contains the glyph for the given codepoint if the glyph
/// is found; otherwise the default value. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains glyphs for the specified codepoint; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyphs(CodePoint codePoint, [NotNullWhen(true)] out Glyph? glyph)
=> this.TryGetGlyphs(codePoint, TextAttributes.None, ColorFontSupport.None, out glyph);
/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="support">Options for enabling color font support during layout and rendering.</param>
/// <param name="glyph">
/// When this method returns, contains the glyphs for the given codepoint and color support if the glyph
/// is found; otherwise the default value. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains glyphs for the specified codepoint; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyphs(CodePoint codePoint, ColorFontSupport support, [NotNullWhen(true)] out Glyph? glyph)
=> this.TryGetGlyphs(codePoint, TextAttributes.None, support, out glyph);
/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="textAttributes">The text attributes to apply to the glyphs.</param>
/// <param name="support">Options for enabling color font support during layout and rendering.</param>
/// <param name="glyph">
/// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph
/// is found; otherwise the default value. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains glyphs for the specified codepoint; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyphs(
CodePoint codePoint,
TextAttributes textAttributes,
ColorFontSupport support,
[NotNullWhen(true)] out Glyph? glyph)
=> this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyph);
/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="textAttributes">The text attributes to apply to the glyphs.</param>
/// <param name="layoutMode">The layout mode to apply to the glyphs.</param>
/// <param name="support">Options for enabling color font support during layout and rendering.</param>
/// <param name="glyph">
/// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph
/// is found; otherwise the default value. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains glyphs for the specified codepoint; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyph(
CodePoint codePoint,
TextAttributes textAttributes,
LayoutMode layoutMode,
ColorFontSupport support,
[NotNullWhen(true)] out Glyph? glyph)
=> this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, layoutMode, support, out glyph);
/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="textAttributes">The text attributes to apply to the glyphs.</param>
/// <param name="textDecorations">The text decorations to apply to the glyphs.</param>
/// <param name="layoutMode">The layout mode to apply to the glyphs.</param>
/// <param name="support">Options for enabling color font support during layout and rendering.</param>
/// <param name="glyph">
/// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph
/// is found; otherwise the default value. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains glyphs for the specified codepoint; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyph(
CodePoint codePoint,
TextAttributes textAttributes,
TextDecorations textDecorations,
LayoutMode layoutMode,
ColorFontSupport support,
[NotNullWhen(true)] out Glyph? glyph)
{
TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations };
if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out FontGlyphMetrics? metrics))
{
glyph = new(metrics.CloneForRendering(textRun), this.Size);
return true;
}
glyph = null;
return false;
}
/// <summary>
/// Gets the amount, in px units, the <paramref name="current"/> glyph should be offset if it is followed by
/// the <paramref name="next"/> glyph.
/// </summary>
/// <param name="current">The current glyph.</param>
/// <param name="next">The next glyph.</param>
/// <param name="dpi">The DPI (Dots Per Inch) to render/measure the kerning offset at.</param>
/// <param name="vector">
/// When this method returns, contains the offset, in font units, that should be applied to the
/// <paramref name="current"/> glyph, if the offset is found; otherwise the default vector value.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains and offset for the glyph combination; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetKerningOffset(Glyph current, Glyph next, float dpi, out Vector2 vector)
{
if (this.FontMetrics.TryGetKerningOffset(current.GlyphMetrics.GlyphId, next.GlyphMetrics.GlyphId, out vector))
{
// Scale the result
Vector2 scale = new Vector2(this.Size * dpi) / next.GlyphMetrics.ScaleFactor;
vector *= scale;
return true;
}
return false;
}
private string LoadFontName()
=> this.metrics.Value?.Description.FontName(this.Family.Culture) ?? string.Empty;
private FontMetrics? LoadInstanceInternal()
{
FontMetrics? metrics = this.ResolveBaseMetrics();
if (metrics is null)
{
return null;
}
// If variations are specified and the base metrics supports them, create a variation instance.
if (this.variations.Length > 0)
{
StreamFontMetrics? streamMetrics = metrics switch
{
StreamFontMetrics s => s,
FileFontMetrics f => f.StreamFontMetrics,
_ => null
};
if (streamMetrics is not null)
{
return streamMetrics.CreateVariationInstance(this.variations);
}
}
return metrics;
}
private FontMetrics? ResolveBaseMetrics()
{
if (this.Family.TryGetMetrics(this.RequestedStyle, out FontMetrics? metrics))
{
return metrics;
}
if ((this.RequestedStyle & FontStyle.Italic) == FontStyle.Italic)
{
// Can't find style requested and they want one that's at least partial italic.
// Try the regular italic.
if (this.Family.TryGetMetrics(FontStyle.Italic, out metrics))
{
return metrics;
}
}
if ((this.RequestedStyle & FontStyle.Bold) == FontStyle.Bold)
{
// Can't find style requested and they want one that's at least partial bold.
// Try the regular bold.
if (this.Family.TryGetMetrics(FontStyle.Bold, out metrics))
{
return metrics;
}
}
// Can't find style requested so let's just try returning the default.
ReadOnlySpan<FontStyle> styles = this.Family.GetAvailableStyles().Span;
FontStyle defaultStyle = styles[0];
foreach (FontStyle style in styles)
{
if (style == FontStyle.Regular)
{
defaultStyle = FontStyle.Regular;
break;
}
}
this.Family.TryGetMetrics(defaultStyle, out metrics);
return metrics;
}
}
}