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