Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 10 additions & 1 deletion src/SixLabors.Fonts/Bounds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,16 @@ public static Bounds Load(IList<ControlPoint> controlPoints)
}

public static Bounds Transform(in Bounds bounds, Matrix3x2 matrix)
=> new(Vector2.Transform(bounds.Min, matrix), Vector2.Transform(bounds.Max, matrix));
{
Vector2 topLeft = Vector2.Transform(new Vector2(bounds.Min.X, bounds.Max.Y), matrix);
Vector2 topRight = Vector2.Transform(bounds.Max, matrix);
Vector2 bottomLeft = Vector2.Transform(bounds.Min, matrix);
Vector2 bottomRight = Vector2.Transform(new Vector2(bounds.Max.X, bounds.Min.Y), matrix);

Vector2 min = Vector2.Min(Vector2.Min(topLeft, topRight), Vector2.Min(bottomLeft, bottomRight));
Vector2 max = Vector2.Max(Vector2.Max(topLeft, topRight), Vector2.Max(bottomLeft, bottomRight));
return new Bounds(min, max);
}

public override bool Equals(object? obj) => obj is Bounds bounds && this.Equals(bounds);

Expand Down
53 changes: 47 additions & 6 deletions src/SixLabors.Fonts/FileFontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,39 @@ namespace SixLabors.Fonts;
internal sealed class FileFontMetrics : FontMetrics
{
private readonly Lazy<StreamFontMetrics> fontMetrics;
private readonly FontSource source;

/// <summary>
/// Initializes a new instance of the <see cref="FileFontMetrics"/> class.
/// </summary>
/// <param name="path">The filesystem path to the font.</param>
public FileFontMetrics(string path)
: this(path, 0)
{
}

public FileFontMetrics(string path, long offset)
/// <summary>
/// Initializes a new instance of the <see cref="FileFontMetrics"/> class.
/// </summary>
/// <param name="path">The filesystem path to the font.</param>
/// <param name="offset">The offset of the font within the file.</param>
private FileFontMetrics(string path, long offset)
: this(FontDescription.LoadDescription(path), path, offset)
{
}

internal FileFontMetrics(FontDescription description, string path, long offset)
/// <summary>
/// Initializes a new instance of the <see cref="FileFontMetrics"/> class.
/// </summary>
/// <param name="description">The font description.</param>
/// <param name="path">The filesystem path to the font.</param>
/// <param name="offset">The offset of the font within the file.</param>
private FileFontMetrics(FontDescription description, string path, long offset)
{
this.Description = description;
this.Path = path;
this.fontMetrics = new Lazy<StreamFontMetrics>(() => StreamFontMetrics.LoadFont(path, offset), true);
this.source = FontSource.Create(path, offset);
this.fontMetrics = new Lazy<StreamFontMetrics>(this.LoadFont, true);
}

/// <inheritdoc cref="FontMetrics.Description"/>
Expand All @@ -48,7 +65,7 @@ internal FileFontMetrics(FontDescription description, string path, long offset)
/// <summary>
/// Gets the underlying <see cref="StreamFontMetrics"/> that this file-backed instance delegates to.
/// </summary>
internal StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value;
public StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value;

/// <inheritdoc />
public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm;
Expand Down Expand Up @@ -101,6 +118,14 @@ internal FileFontMetrics(FontDescription description, string path, long offset)
/// <inheritdoc/>
public override float ItalicAngle => this.fontMetrics.Value.ItalicAngle;

/// <inheritdoc/>
public override bool TryGetTableData(Tag tag, out ReadOnlyMemory<byte> table)
=> this.source.TryGetTableData(tag, out table);

/// <inheritdoc/>
public override Stream OpenStream()
=> this.source.OpenStream();

/// <inheritdoc/>
internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId)
=> this.fontMetrics.Value.TryGetGlyphId(codePoint, out glyphId);
Expand Down Expand Up @@ -143,6 +168,16 @@ public override bool TryGetGlyphMetrics(
[NotNullWhen(true)] out FontGlyphMetrics? metrics)
=> this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics);

/// <inheritdoc />
public override bool TryGetGlyphMetrics(
ushort glyphId,
TextAttributes textAttributes,
TextDecorations textDecorations,
LayoutMode layoutMode,
ColorFontSupport support,
[NotNullWhen(true)] out FontGlyphMetrics? metrics)
=> this.fontMetrics.Value.TryGetGlyphMetrics(glyphId, textAttributes, textDecorations, layoutMode, support, out metrics);

/// <inheritdoc />
internal override FontGlyphMetrics GetGlyphMetrics(
CodePoint codePoint,
Expand Down Expand Up @@ -182,9 +217,9 @@ internal override ReadOnlySpan<float> GetNormalizedCoordinates()
=> this.fontMetrics.Value.GetNormalizedCoordinates();

/// <summary>
/// Reads a <see cref="StreamFontMetrics"/> from the specified stream.
/// Reads a font collection from the specified filesystem path.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="path">The filesystem path to the font collection.</param>
/// <returns>A read-only memory region containing the font metrics.</returns>
public static ReadOnlyMemory<FileFontMetrics> LoadFontCollection(string path)
{
Expand All @@ -203,4 +238,10 @@ public static ReadOnlyMemory<FileFontMetrics> LoadFontCollection(string path)

return fonts;
}

private StreamFontMetrics LoadFont()
{
using Stream stream = this.OpenStream();
return StreamFontMetrics.LoadFont(stream, this.source);
}
}
124 changes: 122 additions & 2 deletions src/SixLabors.Fonts/Font.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace SixLabors.Fonts;
public sealed class Font
{
private readonly FontVariation[] variations;
private readonly FontWeight? requestedWeight;
private readonly Lazy<FontMetrics?> metrics;
private readonly Lazy<string> fontName;

Expand Down Expand Up @@ -93,6 +94,24 @@ public Font(Font prototype, params FontVariation[] variations)
this.RequestedStyle = prototype.RequestedStyle;
this.Size = prototype.Size;
this.variations = variations;
this.requestedWeight = prototype.requestedWeight;
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 with a numeric weight that the
/// operating-system family resolves through its normal metrics lookup.
/// </summary>
/// <param name="prototype">The font supplying family, size, style, and variation settings.</param>
/// <param name="weight">The requested numeric system-font weight.</param>
private Font(Font prototype, FontWeight weight)
{
this.Family = prototype.Family;
this.RequestedStyle = prototype.RequestedStyle;
this.Size = prototype.Size;
this.variations = prototype.variations;
this.requestedWeight = weight;
this.metrics = new Lazy<FontMetrics?>(this.LoadInstanceInternal, true);
this.fontName = new Lazy<string>(this.LoadFontName, true);
}
Expand Down Expand Up @@ -138,6 +157,80 @@ public Font(Font prototype, params FontVariation[] variations)
/// </summary>
internal FontStyle RequestedStyle { get; }

/// <summary>
/// Creates a variable-font instance at the requested weight when the font exposes a
/// registered weight axis. Static fonts are returned unchanged.
/// </summary>
/// <param name="weight">The requested weight.</param>
/// <param name="applied"><see langword="true"/> when the weight axis was applied.</param>
/// <returns>The variable-font instance, or this font for a static face.</returns>
internal Font WithWeight(FontWeight weight, out bool applied)
{
applied = false;
if (this.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory<Tables.AdvancedTypographic.Variations.VariationAxis> axes))
{
// A variable face owns its complete weight range, so apply wght before asking the
// system collection for another static face. This also preserves every other axis.
bool hasWeightAxis = false;
foreach (Tables.AdvancedTypographic.Variations.VariationAxis axis in axes.Span)
{
if (axis.Tag == KnownVariationAxes.Weight)
{
hasWeightAxis = true;
break;
}
}

if (hasWeightAxis)
{
applied = true;
for (int i = 0; i < this.variations.Length; i++)
{
if (this.variations[i].Tag != KnownVariationAxes.Weight)
{
continue;
}

if (this.variations[i].Value == (int)weight)
{
return this;
}

// Font instances are immutable and can be shared by concurrent layouts.
// Recreate every variation in storage owned by the new Font so replacing wght
// cannot alter the source Font, while preserving every other configured axis.
FontVariation[] variations = new FontVariation[this.variations.Length];
for (int variationIndex = 0; variationIndex < this.variations.Length; variationIndex++)
{
FontVariation variation = this.variations[variationIndex];
variations[variationIndex] = new FontVariation(variation.Tag, variation.Value);
}

variations[i] = new FontVariation(KnownVariationAxes.Weight, (int)weight);
return new Font(this, variations);
}

// Preserve axes such as wdth or opsz while appending wght. A new array is required
// because the Font constructor retains the complete variation set for its life.
FontVariation[] weightedVariations = new FontVariation[this.variations.Length + 1];
this.variations.CopyTo(weightedVariations, 0);
weightedVariations[^1] = new FontVariation(KnownVariationAxes.Weight, (int)weight);

return new Font(this, weightedVariations);
}
}

// System collections preserve the platform's family grouping. Use an exact face exposed
// by that grouping before falling back to synthetic weight on the supplied static face.
if (this.Family.TryGetMetrics(this.RequestedStyle, weight, out FontMetrics? systemMetrics)
&& !ReferenceEquals(systemMetrics, this.FontMetrics))
{
return new Font(this, weight);
}

return this;
}

/// <summary>
/// Gets the filesystem path to the font family source.
/// </summary>
Expand Down Expand Up @@ -215,6 +308,20 @@ public bool TryGetGlyphs(
[NotNullWhen(true)] out Glyph? glyph)
=> this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyph);

/// <summary>
/// Gets the glyph identifier for the given code point.
/// </summary>
/// <param name="codePoint">The code point of the character.</param>
/// <param name="glyphId">
/// When this method returns, contains the glyph identifier for the given code point if the glyph
/// is found; otherwise <value>0</value>. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the face contains a glyph for the specified code point; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId)
=> this.FontMetrics.TryGetGlyphId(codePoint, out glyphId);

/// <summary>
/// Gets the glyph for the given codepoint.
/// </summary>
Expand Down Expand Up @@ -260,9 +367,12 @@ public bool TryGetGlyph(
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))
FontMetrics fontMetrics = this.FontMetrics;
if (fontMetrics.TryGetGlyphId(codePoint, out ushort glyphId))
{
TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations };
FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support);

glyph = new(metrics.CloneForRendering(textRun), this.Size);
return true;
}
Expand Down Expand Up @@ -317,6 +427,7 @@ private string LoadFontName()
{
StreamFontMetrics s => s,
FileFontMetrics f => f.StreamFontMetrics,
MemoryFontMetrics m => m.StreamFontMetrics,
_ => null
};

Expand All @@ -331,6 +442,15 @@ private string LoadFontName()

private FontMetrics? ResolveBaseMetrics()
{
// Numeric lookup belongs to FontFamily because it owns the mapping from a family request
// to a concrete face. Only system-backed families implement this overload; custom
// collections continue through the existing FontStyle lookup below.
if (this.requestedWeight.HasValue
&& this.Family.TryGetMetrics(this.RequestedStyle, this.requestedWeight.Value, out FontMetrics? weightedMetrics))
{
return weightedMetrics;
}

if (this.Family.TryGetMetrics(this.RequestedStyle, out FontMetrics? metrics))
{
return metrics;
Expand Down
Loading
Loading