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