diff --git a/SixLabors.Fonts.sln b/SixLabors.Fonts.sln index 70cd3021..a7d974b2 100644 --- a/SixLabors.Fonts.sln +++ b/SixLabors.Fonts.sln @@ -73,6 +73,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnicodeTestData", "UnicodeT EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SixLabors.Fonts.Benchmarks", "tests\SixLabors.Fonts.Benchmarks\SixLabors.Fonts.Benchmarks\SixLabors.Fonts.Benchmarks.csproj", "{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawWithImageSharp", "samples\DrawWithImageSharp\DrawWithImageSharp.csproj", "{01863664-6C7E-61F2-F74B-7D451FFDC3C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Release|Any CPU.Build.0 = Release|Any CPU + {01863664-6C7E-61F2-F74B-7D451FFDC3C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01863664-6C7E-61F2-F74B-7D451FFDC3C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01863664-6C7E-61F2-F74B-7D451FFDC3C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01863664-6C7E-61F2-F74B-7D451FFDC3C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -113,6 +119,7 @@ Global {ABB6E111-672F-4846-88D6-C49C6CD01606} = {249327CF-1415-428B-8EEA-8C7705B1DE8F} {654DD381-B93D-4459-B669-296F5D9172ED} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} {FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} + {01863664-6C7E-61F2-F74B-7D451FFDC3C2} = {71A3911C-D6B9-4EBE-9691-2FE28BDF462E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {38F4B47F-4F74-40F5-8707-C0EF1D0BDF92} diff --git a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj index 65ab1b05..cab0193e 100644 --- a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj +++ b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj @@ -46,8 +46,7 @@ - - + diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 6136836c..0f0aaeab 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using IOPath = System.IO.Path; @@ -218,7 +219,7 @@ public static void RenderText(Font font, string text, int width, int height) using var img = new Image(width, height); img.Mutate(x => x.Fill(Color.White)); - IPathCollection shapes = TextBuilder.GenerateGlyphs(text, new RichTextOptions(font) { Origin = new Vector2(50f, 4f) }); + IPathCollection shapes = TextBuilder.GeneratePaths(text, new RichTextOptions(font) { Origin = new Vector2(50f, 4f) }); img.Mutate(x => x.Fill(Color.Black, shapes)); Directory.CreateDirectory(IOPath.GetDirectoryName(fullPath)); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 0094a263..f35f0858 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -17,7 +17,7 @@ internal sealed class GlyphPositioningCollection : IGlyphShapingCollection /// /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids, point size, and mtrics. /// - private readonly List glyphs = new(); + private readonly List glyphs = []; /// /// Initializes a new instance of the class. @@ -149,6 +149,11 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; List orphans = []; + + Tag vert = FeatureTags.VerticalAlternates; + Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = FeatureTags.VerticalAlternatesForRotation; + for (int i = 0; i < this.glyphs.Count; i++) { GlyphPositioningData current = this.glyphs[i]; @@ -173,6 +178,15 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) // cache the original in the font metrics and only update our collection. TextAttributes textAttributes = shape.TextRun.TextAttributes; TextDecorations textDecorations = shape.TextRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + foreach (Tag feature in shape.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; + } + GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); { // If the glyphs are fallbacks we don't want them as @@ -183,7 +197,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) } } - if (!hasFallBacks) + if (metrics.GlyphType != GlyphType.Fallback) { if (j == 0) { @@ -191,8 +205,12 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) this.glyphs.RemoveAt(i); } + // We only want a single dimensional advance for positioning. + GlyphShapingBounds bounds = isVertical + ? new(0, 0, 0, metrics.AdvanceHeight) + : new(0, 0, metrics.AdvanceWidth, 0); + // Track the number of inserted glyphs at the offset so we can correctly increment our position. - GlyphShapingBounds bounds = new(0, 0, metrics.AdvanceWidth, metrics.AdvanceHeight); this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, pointSize, metrics.CloneForRendering(shape.TextRun))); replacementCount++; } @@ -259,6 +277,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) hasFallBacks = true; } + // We only want a single dimensional advance for positioning. GlyphShapingBounds bounds = isVertical ? new(0, 0, 0, metrics.AdvanceHeight) : new(0, 0, metrics.AdvanceWidth, 0); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 7a2759b7..8f1c227e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -125,7 +125,13 @@ public bool TryUpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollecti // We want to assign the same feature lookups to individual sections of the text rather // than the text as a whole to ensure that different language shapers do not interfere // with each other when the text contains multiple languages. - GlyphShapingData nextData = collection[i + 1]; + int ni = i + 1; + GlyphShapingData nextData = collection[ni]; + if (!collection.ShouldProcess(fontMetrics, ni)) + { + break; + } + ScriptClass next = CodePoint.GetScriptClass(nextData.CodePoint); if (next != current && current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && diff --git a/src/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs b/src/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs index 421b602c..c1cc2792 100644 --- a/src/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs +++ b/src/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs @@ -19,7 +19,10 @@ public ushort GetAdvancedWidth(int glyphIndex) { if (glyphIndex >= this.advancedWidths.Length) { - return this.advancedWidths[0]; + // Records are indexed by glyph ID. As an optimization, the number of records can + // be less than the number of glyphs, in which case the advance width value of the + // last record applies to all remaining glyph IDs. + return this.advancedWidths[^1]; } return this.advancedWidths[glyphIndex]; @@ -29,7 +32,7 @@ internal short GetLeftSideBearing(int glyphIndex) { if (glyphIndex >= this.leftSideBearings.Length) { - return this.leftSideBearings[0]; + return this.leftSideBearings[^1]; } return this.leftSideBearings[glyphIndex]; diff --git a/tests/Images/ReferenceOutput/Test_Issue_469-.png b/tests/Images/ReferenceOutput/Test_Issue_469-.png new file mode 100644 index 00000000..40428f5b --- /dev/null +++ b/tests/Images/ReferenceOutput/Test_Issue_469-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f8f61c67021cb34baa00dba4f708aa5881b4ed4b247468dba4e87fdfd696f86 +size 47229 diff --git a/tests/SixLabors.Fonts.Tests/Fonts/Cousine-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/Cousine-Regular.ttf new file mode 100644 index 00000000..f67a6c8f Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/Cousine-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/Hind-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/Hind-Regular.ttf new file mode 100644 index 00000000..d354dbbc Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/Hind-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/Inconsolata-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/Inconsolata-Regular.ttf new file mode 100644 index 00000000..94747bd0 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/Inconsolata-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NanumGothicCoding-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NanumGothicCoding-Regular.ttf new file mode 100644 index 00000000..ba77a9da Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NanumGothicCoding-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoNaskhArabic-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoNaskhArabic-Regular.ttf new file mode 100644 index 00000000..f69f2546 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoNaskhArabic-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoSansHK-VariableFont_wght.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansHK-VariableFont_wght.ttf new file mode 100644 index 00000000..8c0cd9c4 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansHK-VariableFont_wght.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoSansJP-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansJP-Regular.ttf new file mode 100644 index 00000000..cdd8f083 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansJP-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoSansSC-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansSC-Regular.ttf new file mode 100644 index 00000000..4c2831f7 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoSansSC-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/Sarabun-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/Sarabun-Regular.ttf new file mode 100644 index 00000000..161c9ac9 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/Sarabun-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Fonts/arial.ttf b/tests/SixLabors.Fonts.Tests/Fonts/arial.ttf new file mode 100644 index 00000000..98b05d90 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/arial.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs new file mode 100644 index 00000000..cce04fcf --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_469.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.Fonts.Tests.Issues; + +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: Съешь же ещё этих мягких французских булок.") + .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(); + 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); + 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), + ], + }; + + // 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/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index 74a10e68..d23cfc6e 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -3,7 +3,6 @@ True AnyCPU;x64;x86 - 10 @@ -29,8 +28,8 @@ Comment out this constant declaration to disable all tests based upon image generation. This allows us to make breaking changes to the Fonts API without breaking the tests. --> - + $(DefineConstants);SUPPORTS_DRAWING + true @@ -48,7 +47,7 @@ - + diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index 5b4cfecb..19cad8df 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -265,6 +265,26 @@ public static class TestFonts public static string NotoColorEmojiRegular => GetFullPath("NotoColorEmoji-Regular.ttf"); + public static string Arial => GetFullPath("arial.ttf"); + + public static string CousineRegular => GetFullPath("Cousine-Regular.ttf"); + + public static string HindRegular => GetFullPath("Hind-Regular.ttf"); + + public static string NanumGothicCodingRegular => GetFullPath("NanumGothicCoding-Regular.ttf"); + + public static string InconsolataRegular => GetFullPath("Inconsolata-Regular.ttf"); + + public static string NotoNaskhArabicRegular => GetFullPath("NotoNaskhArabic-Regular.ttf"); + + public static string NotoSansHKVariableFontWght => GetFullPath("NotoSansHK-VariableFont_wght.ttf"); + + public static string NotoSansJPRegular => GetFullPath("NotoSansJP-Regular.ttf"); + + public static string NotoSansSCRegular => GetFullPath("NotoSansSC-Regular.ttf"); + + public static string SarabunRegular => GetFullPath("Sarabun-Regular.ttf"); + public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile); public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile); @@ -314,7 +334,7 @@ private static Stream Clone(this Stream src) return ms; } - private static string GetFullPath(string path) + public static string GetFullPath(string path) { string root = Path.GetDirectoryName(new Uri(typeof(TestFonts).GetTypeInfo().Assembly.CodeBase).LocalPath); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index df83e8a4..e046a3c7 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -7,9 +7,8 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tests.TestUtilities; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.Text; +using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; #endif @@ -74,7 +73,7 @@ public static void TestLayout( extended.Insert(0, "G"); using Image img2 = new(Configuration.Default, imageWidth, imageHeight, Color.White.ToPixel()); - IReadOnlyList glyphs = TextBuilder.GenerateGlyphs2(text, options); + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, options); img2.Mutate(ctx => ctx.Fill(Color.Black, glyphs));