diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs
index 0f0d44b6..870e62b3 100644
--- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs
@@ -11,6 +11,8 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos;
[DebuggerDisplay("X: {XCoordinate}, Y: {YCoordinate}")]
internal abstract class AnchorTable
{
+ private static readonly AnchorTable Empty = new EmptyAnchor();
+
///
/// Initializes a new instance of the class.
///
@@ -50,7 +52,10 @@ public static AnchorTable Load(BigEndianBinaryReader reader, long offset)
1 => AnchorFormat1.Load(reader),
2 => AnchorFormat2.Load(reader),
3 => AnchorFormat3.Load(reader),
- _ => throw new InvalidFontFileException($"anchorFormat identifier {anchorFormat} is invalid. Should be '1', '2' or '3'.")
+
+ // Harfbuzz (Anchor.hh) and FontKit appear to treat this as a default anchor and do not throw.
+ // NotoSans Regular can trigger this. See https://github.com/SixLabors/Fonts/issues/417
+ _ => Empty,
};
}
@@ -119,7 +124,6 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat
{
foreach (GlyphMetrics metric in metrics)
{
- // TODO: What does HarfBuzz do here?
if (metric is not TrueTypeGlyphMetrics ttmetric)
{
break;
@@ -185,4 +189,18 @@ public static AnchorFormat3 Load(BigEndianBinaryReader reader)
public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection)
=> new(this.XCoordinate, this.YCoordinate);
}
+
+ internal sealed class EmptyAnchor : AnchorTable
+ {
+ public EmptyAnchor()
+ : base(0, 0)
+ {
+ }
+
+ public override AnchorXY GetAnchor(
+ FontMetrics fontMetrics,
+ GlyphShapingData data,
+ GlyphPositioningCollection collection)
+ => new(0, 0);
+ }
}
diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoSans-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoSans-Regular.ttf
new file mode 100644
index 00000000..fa4cff50
Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoSans-Regular.ttf differ
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs
new file mode 100644
index 00000000..a43f57d0
--- /dev/null
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tests.Issues;
+
+public class Issues_417
+{
+ [Fact]
+ public void DoesNotThrow_InvalidAnchor()
+ {
+ FontFamily family = new FontCollection().Add(TestFonts.NotoSansRegular);
+ family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics);
+
+ Font font = family.CreateFont(metrics?.UnitsPerEm ?? 1000);
+
+ TextOptions options = new(font);
+
+ // References values generated using.
+ // https://www.corvelsoftware.co.uk/crowbar/
+ TextMeasurer.TryMeasureCharacterAdvances("Text", options, out ReadOnlySpan advances);
+
+ Assert.Equal(4, advances.Length);
+ Assert.Equal(486, advances[0].Bounds.Width);
+ Assert.Equal(544, advances[1].Bounds.Width);
+ Assert.Equal(529, advances[2].Bounds.Width);
+ Assert.Equal(361, advances[3].Bounds.Width);
+
+ GlyphRenderer renderer = new();
+ TextRenderer.RenderTextTo(renderer, "Text", new TextOptions(font));
+
+ int[] expectedGlyphIndices = { 55, 72, 91, 87 };
+ Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count);
+ for (int i = 0; i < expectedGlyphIndices.Length; i++)
+ {
+ Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphId);
+ }
+ }
+}
diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs
index 51bbfe6f..931e09fc 100644
--- a/tests/SixLabors.Fonts.Tests/TestFonts.cs
+++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs
@@ -257,6 +257,8 @@ public static class TestFonts
public static string KellySlabFile => GetFullPath("KellySlab-Regular.ttf");
+ public static string NotoSansRegular => GetFullPath("NotoSans-Regular.ttf");
+
public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile);
public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile);