diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs index b0839cb1..7beb5832 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using static SixLabors.Fonts.Tables.AdvancedTypographic.CoverageFormat2Table; + namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// @@ -23,13 +25,16 @@ public static CoverageTable Load(BigEndianBinaryReader reader, long offset) { 1 => CoverageFormat1Table.Load(reader), 2 => CoverageFormat2Table.Load(reader), - _ => throw new InvalidFontFileException($"Invalid value for 'coverageFormat' {coverageFormat}. Should be '1' or '2'.") + + // Harfbuzz (Coverage.hh) treats this as an empty table and does not throw. + // SofiaSans Condensed can trigger this. See https://github.com/SixLabors/Fonts/issues/470 + _ => EmptyCoverageTable.Instance }; } public static CoverageTable[] LoadArray(BigEndianBinaryReader reader, long offset, ReadOnlySpan coverageOffsets) { - var tables = new CoverageTable[coverageOffsets.Length]; + CoverageTable[] tables = new CoverageTable[coverageOffsets.Length]; for (int i = 0; i < tables.Length; i++) { tables[i] = Load(reader, offset + coverageOffsets[i]); @@ -103,7 +108,7 @@ public static CoverageFormat2Table Load(BigEndianBinaryReader reader) // | RangeRecord | rangeRecords[rangeCount] | Array of glyph ranges — ordered by startGlyphID. | // +-------------+--------------------------+--------------------------------------------------+ ushort rangeCount = reader.ReadUInt16(); - var records = new CoverageRangeRecord[rangeCount]; + CoverageRangeRecord[] records = new CoverageRangeRecord[rangeCount]; for (int i = 0; i < records.Length; i++) { @@ -124,4 +129,15 @@ public static CoverageFormat2Table Load(BigEndianBinaryReader reader) return new CoverageFormat2Table(records); } + + internal sealed class EmptyCoverageTable : CoverageTable + { + private EmptyCoverageTable() + { + } + + public static EmptyCoverageTable Instance { get; } = new(); + + public override int CoverageIndexOf(ushort glyphId) => -1; + } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index bc12a772..a628a8ee 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -11,8 +11,6 @@ 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. /// @@ -53,9 +51,9 @@ public static AnchorTable Load(BigEndianBinaryReader reader, long offset) 2 => AnchorFormat2.Load(reader), 3 => AnchorFormat3.Load(reader), - // Harfbuzz (Anchor.hh) and FontKit appear to treat this as a default anchor and do not throw. + // Harfbuzz (Anchor.hh) treats this as an empty table and does not throw.. // NotoSans Regular can trigger this. See https://github.com/SixLabors/Fonts/issues/417 - _ => Empty, + _ => EmptyAnchorTable.Instance, }; } @@ -185,13 +183,15 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat => new(this.XCoordinate, this.YCoordinate); } - internal sealed class EmptyAnchor : AnchorTable + internal sealed class EmptyAnchorTable : AnchorTable { - public EmptyAnchor() + private EmptyAnchorTable() : base(0, 0) { } + public static EmptyAnchorTable Instance { get; } = new(); + public override AnchorXY GetAnchor( FontMetrics fontMetrics, GlyphShapingData data, diff --git a/tests/Images/ReferenceOutput/TestIssue_468-.png b/tests/Images/ReferenceOutput/TestIssue_468-.png new file mode 100644 index 00000000..d3e5ac92 --- /dev/null +++ b/tests/Images/ReferenceOutput/TestIssue_468-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a27aaadf3000161c517bc48f28461cc1ef9e19661705d94bdb830109548f2e73 +size 19306 diff --git a/tests/Images/ReferenceOutput/Test_Issue_470-.png b/tests/Images/ReferenceOutput/Test_Issue_470-.png new file mode 100644 index 00000000..31c0c99d --- /dev/null +++ b/tests/Images/ReferenceOutput/Test_Issue_470-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffdc80507e1e73418b9b1a6e89883eb76a58c9403d67201b1eece63c29bcc736 +size 36046 diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoSansArabic-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 00000000..3279938e Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansArabic-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/SofiaSansCondensed-ExtraLight-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/SofiaSansCondensed-ExtraLight-Regular.ttf new file mode 100644 index 00000000..76a32e6c Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/SofiaSansCondensed-ExtraLight-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/consola.ttf b/tests/SixLabors.Fonts.Tests/Fonts/consola.ttf new file mode 100644 index 00000000..e881ca4b Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/consola.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_468.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_468.cs new file mode 100644 index 00000000..3243933b --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_468.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.Fonts.Tests.Issues; + +public class Issues_468 +{ + [Fact] + public void TestIssue_468() + { + StringBuilder stringBuilder = new(); + stringBuilder + .AppendLine("Latin: The quick brown fox jumps over the lazy dog.") + .AppendLine("Arabic (RTL & Shaping): نص حكيم له سر قاطع وذو شأن عظيم"); + + string text = stringBuilder.ToString(); + FontCollection fontCollection = new(); + + string consola = fontCollection.Add(TestFonts.Consola).Name; + string arabic = fontCollection.Add(TestFonts.NotoSansArabicRegular).Name; + + FontFamily mainFontFamily = fontCollection.Get(consola); + Font mainFont = mainFontFamily.CreateFont(50, FontStyle.Regular); + + TextOptions options = new(mainFont) + { + FallbackFontFamilies = + [ + fontCollection.Get(arabic), + ], + }; + + // There are too many metrics to validate here so we just ensure no exceptions are thrown + // and the rendering looks correct by inspecting the snapshot. + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false); + } +} diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs index cce04fcf..1f9d79e5 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs @@ -10,16 +10,6 @@ public class Issues_469 [Fact] public void Test_Issue_469() { - const string arialFontName = "Arial"; - const string inconsolataFontName = "Inconsolata"; - const string nanumGothicCodingFontName = "NanumGothicCoding"; - const string cousineFontName = "Cousine"; - const string notoSansScThinFontName = "Noto Sans SC Thin"; - const string notoSansJpThinFontName = "Noto Sans JP Thin"; - const string notoNaskhArabicFontName = "Noto Naskh Arabic"; - const string sarabunFontName = "Sarabun"; - const string hindFontName = "Hind"; - StringBuilder stringBuilder = new(); stringBuilder.AppendLine("Latin: The quick brown fox jumps over the lazy dog.") .AppendLine("Cyrillic: Съешь же ещё этих мягких французских булок.") @@ -33,33 +23,33 @@ public void Test_Issue_469() .AppendLine("Devanagari (Conjuncts): ऋषियों को सताने वाले राक्षसों का अंत हो गया"); string text = stringBuilder.ToString(); - FontCollection fontCollection = new(); - fontCollection.Add(TestFonts.Arial); - fontCollection.Add(TestFonts.CousineRegular); - fontCollection.Add(TestFonts.HindRegular); - fontCollection.Add(TestFonts.NanumGothicCodingRegular); - fontCollection.Add(TestFonts.InconsolataRegular); - fontCollection.Add(TestFonts.NotoNaskhArabicRegular); - fontCollection.Add(TestFonts.NotoSansHKVariableFontWght); - fontCollection.Add(TestFonts.NotoSansJPRegular); - fontCollection.Add(TestFonts.NotoSansSCRegular); - fontCollection.Add(TestFonts.SarabunRegular); - FontFamily mainFontFamily = fontCollection.Get(arialFontName); + FontCollection fontCollection = new(); + string arial = fontCollection.Add(TestFonts.Arial).Name; + string cousine = fontCollection.Add(TestFonts.CousineRegular).Name; + string hind = fontCollection.Add(TestFonts.HindRegular).Name; + string nanumGothicCoding = fontCollection.Add(TestFonts.NanumGothicCodingRegular).Name; + string inconsolata = fontCollection.Add(TestFonts.InconsolataRegular).Name; + string notoNaskhArabic = fontCollection.Add(TestFonts.NotoNaskhArabicRegular).Name; + string notoSansJpThin = fontCollection.Add(TestFonts.NotoSansJPRegular).Name; + string notoSansScThin = fontCollection.Add(TestFonts.NotoSansSCRegular).Name; + string sarabun = fontCollection.Add(TestFonts.SarabunRegular).Name; + + FontFamily mainFontFamily = fontCollection.Get(arial); Font mainFont = mainFontFamily.CreateFont(30, FontStyle.Regular); TextOptions options = new(mainFont) { FallbackFontFamilies = [ - fontCollection.Get(inconsolataFontName), - fontCollection.Get(nanumGothicCodingFontName), - fontCollection.Get(cousineFontName), - fontCollection.Get(notoSansScThinFontName), - fontCollection.Get(notoSansJpThinFontName), - fontCollection.Get(notoNaskhArabicFontName), - fontCollection.Get(sarabunFontName), - fontCollection.Get(hindFontName), + fontCollection.Get(inconsolata), + fontCollection.Get(nanumGothicCoding), + fontCollection.Get(cousine), + fontCollection.Get(notoSansScThin), + fontCollection.Get(notoSansJpThin), + fontCollection.Get(notoNaskhArabic), + fontCollection.Get(sarabun), + fontCollection.Get(hind), ], }; diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_470.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_470.cs new file mode 100644 index 00000000..6f8d0859 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_470.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.Fonts.Tests.Issues; + +public class Issues_470 +{ + [Fact] + public void Test_Issue_470() + { + StringBuilder stringBuilder = new(); + stringBuilder.AppendLine("Latin: The quick brown fox jumps over the lazy dog.") + .AppendLine("Cyrillic: Съешь же ещё этих мягких французских булок.") + .AppendLine("Greek: Ζαφείρι δέξου πάγκαλο, βαθῶν ψυχῆς τὸ σῆμα.") + .AppendLine("Chinese: 敏捷的棕色狐狸跳过了懒狗") + .AppendLine("Japanese: いろはにほへと ちりぬるを") + .AppendLine("Korean: 다람쥐 헌 쳇바퀴에 타고파") + .AppendLine("Arabic (RTL & Shaping): نص حكيم له سر قاطع وذو شأن عظيم") + .AppendLine("Hebrew (RTL): דג סקרן שט בים מאוכזב ולפתע מצא חברה") + .AppendLine("Thai (Complex): เป็นมนุษย์สุดประเสริฐเลิศคุณค่า") + .AppendLine("Devanagari (Conjuncts): ऋषियों को सताने वाले राक्षसों का अंत हो गया"); + + string text = stringBuilder.ToString(); + + FontCollection fontCollection = new(); + string arial = fontCollection.Add(TestFonts.Arial).Name; + string sofia = fontCollection.Add(TestFonts.SofiaSansCondensedLight).Name; + + FontFamily mainFontFamily = fontCollection.Get(arial); + Font mainFont = mainFontFamily.CreateFont(30, FontStyle.Regular); + + TextOptions options = new(mainFont) + { + FallbackFontFamilies = + [ + fontCollection.Get(sofia), + ], + }; + + // There are too many metrics to validate here so we just ensure no exceptions are thrown + // and the rendering looks correct by inspecting the snapshot. + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false); + } +} diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index 19cad8df..ac0e7714 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -285,6 +285,12 @@ public static class TestFonts public static string SarabunRegular => GetFullPath("Sarabun-Regular.ttf"); + public static string NotoSansArabicRegular => GetFullPath("NotoSansArabic-Regular.ttf"); + + public static string Consola => GetFullPath("consola.ttf"); + + public static string SofiaSansCondensedLight => GetFullPath("SofiaSansCondensed-ExtraLight-Regular.ttf"); + public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile); public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile);