Skip to content

Commit 310c95b

Browse files
Copy changes from #492
1 parent 46125b0 commit 310c95b

File tree

4 files changed

+159
-40
lines changed

4 files changed

+159
-40
lines changed

src/SixLabors.Fonts/TextLayout.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,22 @@ VerticalOrientationType.Rotate or
11951195
// Work out the scaled metrics for the glyph.
11961196
GlyphMetrics metric = metrics[i];
11971197

1198+
// Adjust the advance for the last decomposed glyph to add
1199+
// tracking if applicable.
1200+
if (options.Tracking != 0 && decomposedAdvance > 0 && i == decomposedAdvances.Length - 1)
1201+
{
1202+
if (isHorizontalLayout || shouldRotate)
1203+
{
1204+
float scaleAX = pointSize / glyph.ScaleFactor.X;
1205+
decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAX;
1206+
}
1207+
else
1208+
{
1209+
float scaleAY = pointSize / glyph.ScaleFactor.Y;
1210+
decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAY;
1211+
}
1212+
}
1213+
11981214
// Convert design-space units to pixels based on the target point size.
11991215
// ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph.
12001216
float scaleY = pointSize / metric.ScaleFactor.Y;

src/SixLabors.Fonts/TextOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public TextOptions(TextOptions options)
4444
this.VerticalAlignment = options.VerticalAlignment;
4545
this.LayoutMode = options.LayoutMode;
4646
this.KerningMode = options.KerningMode;
47+
this.Tracking = options.Tracking;
4748
this.ColorFontSupport = options.ColorFontSupport;
4849
this.FeatureTags = new List<Tag>(options.FeatureTags);
4950
this.TextRuns = new List<TextRun>(options.TextRuns);
@@ -171,6 +172,13 @@ public float LineSpacing
171172
/// </summary>
172173
public KerningMode KerningMode { get; set; }
173174

175+
/// <summary>
176+
/// Gets or sets the tracking (letter-spacing) value.
177+
/// Tracking adjusts the spacing between all characters uniformly and is measured in em.
178+
/// Positive values increase spacing, negative values decrease spacing, and zero applies no adjustment.
179+
/// </summary>
180+
public float Tracking { get; set; }
181+
174182
/// <summary>
175183
/// Gets or sets the positioning mode used for rendering decorations.
176184
/// </summary>

tests/SixLabors.Fonts.Tests/TextLayoutTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,95 @@ public void TrueTypeHinting_CanHintSmallOpenSans(char c, FontRectangle expected)
967967
Assert.Equal(expected, actual, Comparer);
968968
}
969969

970+
public static TheoryData<string, float, float, float[]> FontTrackingHorizontalData
971+
= new()
972+
{
973+
{ "aaaa", 0.0f, 134.0f, [2.9f, 38.5f, 74.0f, 109.6f] },
974+
{ "aaaa", 0.1f, 153.3f, [2.9f, 44.9f, 86.8f, 128.8f] },
975+
{ "aaaa", 1.0f, 326.1f, [2.9f, 102.5f, 202.0f, 301.6f] },
976+
{ "awwa", 0.0f, 162.1f, [2.9f, 36.3f, 85.9f, 137.6f] },
977+
{ "awwa", 0.1f, 181.4f, [2.9f, 42.7f, 98.7f, 156.8f] },
978+
{ "awwa", 1.0f, 354.1f, [2.9f, 100.3f, 213.9f, 329.6f] },
979+
};
980+
981+
[Theory]
982+
[MemberData(nameof(FontTrackingHorizontalData))]
983+
public void FontTracking_SpaceCharacters_WithHorizontalLayout(string text, float tracking, float width, float[] characterPosition)
984+
{
985+
Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(64);
986+
TextOptions options = new(font)
987+
{
988+
Tracking = tracking,
989+
};
990+
991+
FontRectangle actual = TextMeasurer.MeasureSize(text, options);
992+
Assert.Equal(width, actual.Width, Comparer);
993+
994+
Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan<GlyphBounds> bounds));
995+
Assert.Equal(characterPosition, bounds.ToArray().Select(x => x.Bounds.X), Comparer);
996+
}
997+
998+
public static TheoryData<string, float, float, float[]> FontTrackingVerticalData
999+
= new()
1000+
{
1001+
{ "aaaa", 0.0f, 296.9f, [33.5f, 120.7f, 207.9f, 295.0f] },
1002+
{ "aaaa", 0.1f, 316.1f, [33.5f, 127.1f, 220.7f, 314.2f] },
1003+
{ "aaaa", 1.0f, 488.9f, [33.5f, 184.7f, 335.9f, 487.0f] },
1004+
{ "awwa", 0.0f, 296.9f, [33.5f, 121.2f, 208.4f, 295.0f] },
1005+
{ "awwa", 0.1f, 316.1f, [33.5f, 127.6f, 221.2f, 314.2f] },
1006+
{ "awwa", 1.0f, 488.9f, [33.5f, 185.2f, 336.4f, 487.0f] },
1007+
};
1008+
1009+
[Theory]
1010+
[MemberData(nameof(FontTrackingVerticalData))]
1011+
public void FontTracking_SpaceCharacters_WithVerticalLayout(string text, float tracking, float width, float[] characterPosition)
1012+
{
1013+
Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(64);
1014+
TextOptions options = new(font)
1015+
{
1016+
Tracking = tracking,
1017+
LayoutMode = LayoutMode.VerticalLeftRight,
1018+
};
1019+
1020+
FontRectangle actual = TextMeasurer.MeasureSize(text, options);
1021+
Assert.Equal(width, actual.Height, Comparer);
1022+
1023+
Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan<GlyphBounds> bounds));
1024+
Assert.Equal(characterPosition, bounds.ToArray().Select(x => x.Bounds.Y), Comparer);
1025+
}
1026+
1027+
[Theory]
1028+
[InlineData("\u1B3C", 1, 83.8)]
1029+
[InlineData("\u1B3C\u1B3C", 1, 83.8)]
1030+
public void FontTracking_DoNotAddSpacingAfterCharacterThatDidNotAdvance(string text, float tracking, float width)
1031+
{
1032+
Font font = new FontCollection().Add(TestFonts.NotoSansBalineseRegular).CreateFont(64);
1033+
TextOptions options = new(font)
1034+
{
1035+
Tracking = tracking,
1036+
};
1037+
1038+
FontRectangle actual = TextMeasurer.MeasureSize(text, options);
1039+
Assert.Equal(width, actual.Width, Comparer);
1040+
}
1041+
1042+
[Theory]
1043+
[InlineData("\u093f", 1, 48.4)]
1044+
[InlineData("\u0930\u094D\u0915\u093F", 1, 225.6)]
1045+
[InlineData("\u0930\u094D\u0915\u093F\u0930\u094D\u0915\u093F", 1, 419)]
1046+
[InlineData("\u093fa", 1, 145.5f)]
1047+
public void FontTracking_CorrectlyAddSpacingForComposedCharacter(string text, float tracking, float width)
1048+
{
1049+
Font font = new FontCollection().Add(TestFonts.NotoSansDevanagariRegular).CreateFont(64);
1050+
TextOptions options = new(font)
1051+
{
1052+
Tracking = tracking,
1053+
};
1054+
1055+
FontRectangle actual = TextMeasurer.MeasureSize(text, options);
1056+
Assert.Equal(width, actual.Width, Comparer);
1057+
}
1058+
9701059
[Fact]
9711060
public void CanMeasureTextAdvance()
9721061
{

tests/SixLabors.Fonts.Tests/TextOptionsTests.cs

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public TextOptionsTests()
2424
public void ConstructorTest_FontOnly()
2525
{
2626
Font font = FakeFont.CreateFont("ABC");
27-
var options = new TextOptions(font);
27+
TextOptions options = new(font);
2828

2929
Assert.Equal(72, options.Dpi);
3030
Assert.Empty(options.FallbackFontFamilies);
@@ -38,7 +38,7 @@ public void ConstructorTest_FontWithSingleDpi()
3838
{
3939
Font font = FakeFont.CreateFont("ABC");
4040
const float dpi = 123;
41-
var options = new TextOptions(font) { Dpi = dpi };
41+
TextOptions options = new(font) { Dpi = dpi };
4242

4343
Assert.Equal(dpi, options.Dpi);
4444
Assert.Empty(options.FallbackFontFamilies);
@@ -51,7 +51,7 @@ public void ConstructorTest_FontWithSingleDpi()
5151
public void ConstructorTest_FontWithOrigin()
5252
{
5353
Font font = FakeFont.CreateFont("ABC");
54-
var origin = new Vector2(123, 345);
54+
Vector2 origin = new(123, 345);
5555
TextOptions options = new(font) { Origin = origin };
5656

5757
Assert.Equal(72, options.Dpi);
@@ -65,7 +65,7 @@ public void ConstructorTest_FontWithOrigin()
6565
public void ConstructorTest_FontWithSingleDpiWithOrigin()
6666
{
6767
Font font = FakeFont.CreateFont("ABC");
68-
var origin = new Vector2(123, 345);
68+
Vector2 origin = new(123, 345);
6969
const float dpi = 123;
7070
TextOptions options = new(font) { Dpi = dpi, Origin = origin };
7171

@@ -80,13 +80,13 @@ public void ConstructorTest_FontWithSingleDpiWithOrigin()
8080
public void ConstructorTest_FontOnly_WithFallbackFonts()
8181
{
8282
Font font = FakeFont.CreateFont("ABC");
83-
FontFamily[] fontFamilies = new[]
84-
{
83+
FontFamily[] fontFamilies =
84+
[
8585
FakeFont.CreateFont("DEF").Family,
86-
FakeFont.CreateFont("GHI").Family
87-
};
86+
FakeFont.CreateFont("GHI").Family,
87+
];
8888

89-
var options = new TextOptions(font)
89+
TextOptions options = new(font)
9090
{
9191
FallbackFontFamilies = fontFamilies
9292
};
@@ -102,14 +102,14 @@ public void ConstructorTest_FontOnly_WithFallbackFonts()
102102
public void ConstructorTest_FontWithSingleDpi_WithFallbackFonts()
103103
{
104104
Font font = FakeFont.CreateFont("ABC");
105-
FontFamily[] fontFamilies = new[]
106-
{
105+
FontFamily[] fontFamilies =
106+
[
107107
FakeFont.CreateFont("DEF").Family,
108-
FakeFont.CreateFont("GHI").Family
109-
};
108+
FakeFont.CreateFont("GHI").Family,
109+
];
110110

111111
const float dpi = 123;
112-
var options = new TextOptions(font)
112+
TextOptions options = new(font)
113113
{
114114
Dpi = dpi,
115115
FallbackFontFamilies = fontFamilies
@@ -126,13 +126,13 @@ public void ConstructorTest_FontWithSingleDpi_WithFallbackFonts()
126126
public void ConstructorTest_FontWithOrigin_WithFallbackFonts()
127127
{
128128
Font font = FakeFont.CreateFont("ABC");
129-
FontFamily[] fontFamilies = new[]
130-
{
129+
FontFamily[] fontFamilies =
130+
[
131131
FakeFont.CreateFont("DEF").Family,
132-
FakeFont.CreateFont("GHI").Family
133-
};
132+
FakeFont.CreateFont("GHI").Family,
133+
];
134134

135-
var origin = new Vector2(123, 345);
135+
Vector2 origin = new(123, 345);
136136
TextOptions options = new(font)
137137
{
138138
FallbackFontFamilies = fontFamilies,
@@ -150,13 +150,13 @@ public void ConstructorTest_FontWithOrigin_WithFallbackFonts()
150150
public void ConstructorTest_FontWithSingleDpiWithOrigin_WithFallbackFonts()
151151
{
152152
Font font = FakeFont.CreateFont("ABC");
153-
FontFamily[] fontFamilies = new[]
154-
{
153+
FontFamily[] fontFamilies =
154+
[
155155
FakeFont.CreateFont("DEF").Family,
156-
FakeFont.CreateFont("GHI").Family
157-
};
156+
FakeFont.CreateFont("GHI").Family,
157+
];
158158

159-
var origin = new Vector2(123, 345);
159+
Vector2 origin = new(123, 345);
160160
const float dpi = 123;
161161
TextOptions options = new(font)
162162
{
@@ -176,20 +176,20 @@ public void ConstructorTest_FontWithSingleDpiWithOrigin_WithFallbackFonts()
176176
public void GetMissingGlyphFromMainFont()
177177
{
178178
Font font = FakeFont.CreateFontWithInstance("ABC", "ABC", out Fakes.FakeFontInstance abcFontInstance);
179-
FontFamily[] fontFamilies = new[]
180-
{
181-
FakeFont.CreateFontWithInstance("DEF", "DEF", out Fakes.FakeFontInstance defFontInstance).Family,
182-
FakeFont.CreateFontWithInstance("GHI", "GHI", out Fakes.FakeFontInstance ghiFontInstance).Family
183-
};
179+
FontFamily[] fontFamilies =
180+
[
181+
FakeFont.CreateFontWithInstance("DEF", "DEF", out Fakes.FakeFontInstance _).Family,
182+
FakeFont.CreateFontWithInstance("GHI", "GHI", out Fakes.FakeFontInstance _).Family,
183+
];
184184

185-
var options = new TextOptions(font)
185+
TextOptions options = new(font)
186186
{
187187
FallbackFontFamilies = fontFamilies,
188188
ColorFontSupport = ColorFontSupport.None
189189
};
190190

191191
ReadOnlySpan<char> text = "Z".AsSpan();
192-
var renderer = new GlyphRenderer();
192+
GlyphRenderer renderer = new();
193193
TextRenderer.RenderTextTo(renderer, text, options);
194194

195195
GlyphRendererParameters glyph = Assert.Single(renderer.GlyphKeys);
@@ -204,20 +204,20 @@ public void GetMissingGlyphFromMainFont()
204204
public void GetGlyphFromFirstAvailableInstance(char character, string instance)
205205
{
206206
Font font = FakeFont.CreateFontWithInstance("ABC", "ABC", out Fakes.FakeFontInstance abcFontInstance);
207-
FontFamily[] fontFamilies = new[]
208-
{
207+
FontFamily[] fontFamilies =
208+
[
209209
FakeFont.CreateFontWithInstance("DEF", "DEF", out Fakes.FakeFontInstance defFontInstance).Family,
210-
FakeFont.CreateFontWithInstance("EFGHI", "EFGHI", out Fakes.FakeFontInstance efghiFontInstance).Family
211-
};
210+
FakeFont.CreateFontWithInstance("EFGHI", "EFGHI", out Fakes.FakeFontInstance efghiFontInstance).Family,
211+
];
212212

213-
var options = new TextOptions(font)
213+
TextOptions options = new(font)
214214
{
215215
FallbackFontFamilies = fontFamilies,
216216
ColorFontSupport = ColorFontSupport.None
217217
};
218218

219219
ReadOnlySpan<char> text = new[] { character };
220-
var renderer = new GlyphRenderer();
220+
GlyphRenderer renderer = new();
221221
TextRenderer.RenderTextTo(renderer, text, options);
222222
GlyphRendererParameters glyph = Assert.Single(renderer.GlyphKeys);
223223
Assert.Equal(GlyphType.Standard, glyph.GlyphType);
@@ -233,7 +233,7 @@ public void GetGlyphFromFirstAvailableInstance(char character, string instance)
233233
}
234234

235235
[Fact]
236-
public void CloneTextOptionsIsNotNull() => Assert.True(this.clonedTextOptions != null);
236+
public void CloneTextOptionsIsNotNull() => Assert.NotNull(this.clonedTextOptions);
237237

238238
[Fact]
239239
public void DefaultTextOptionsApplyKerning()
@@ -312,7 +312,8 @@ public void NonDefaultClone()
312312
VerticalAlignment = VerticalAlignment.Bottom,
313313
DecorationPositioningMode = DecorationPositioningMode.GlyphFont,
314314
WrappingLength = 42F,
315-
FeatureTags = new List<Tag> { FeatureTags.OldstyleFigures },
315+
Tracking = 66F,
316+
FeatureTags = new List<Tag> { FeatureTags.OldstyleFigures }
316317
};
317318

318319
TextOptions actual = new(expected);
@@ -326,12 +327,13 @@ public void NonDefaultClone()
326327
Assert.Equal(expected.WrappingLength, actual.WrappingLength);
327328
Assert.Equal(expected.DecorationPositioningMode, actual.DecorationPositioningMode);
328329
Assert.Equal(expected.FeatureTags, actual.FeatureTags);
330+
Assert.Equal(expected.Tracking, actual.Tracking);
329331
}
330332

331333
[Fact]
332334
public void CloneIsDeep()
333335
{
334-
var expected = new TextOptions(this.fakeFont);
336+
TextOptions expected = new(this.fakeFont);
335337
TextOptions actual = new(expected)
336338
{
337339
KerningMode = KerningMode.None,
@@ -343,6 +345,8 @@ public void CloneIsDeep()
343345
TextJustification = TextJustification.InterCharacter,
344346
DecorationPositioningMode = DecorationPositioningMode.GlyphFont,
345347
WrappingLength = 42F
348+
WrappingLength = 42F,
349+
Tracking = 66F,
346350
};
347351

348352
Assert.NotEqual(expected.KerningMode, actual.KerningMode);
@@ -354,6 +358,7 @@ public void CloneIsDeep()
354358
Assert.NotEqual(expected.WrappingLength, actual.WrappingLength);
355359
Assert.NotEqual(expected.DecorationPositioningMode, actual.DecorationPositioningMode);
356360
Assert.NotEqual(expected.TextJustification, actual.TextJustification);
361+
Assert.NotEqual(expected.Tracking, actual.Tracking);
357362
}
358363

359364
private static void VerifyPropertyDefault(TextOptions options)
@@ -369,5 +374,6 @@ private static void VerifyPropertyDefault(TextOptions options)
369374
Assert.Equal(LayoutMode.HorizontalTopBottom, options.LayoutMode);
370375
Assert.Equal(DecorationPositioningMode.PrimaryFont, options.DecorationPositioningMode);
371376
Assert.Equal(1, options.LineSpacing);
377+
Assert.Equal(0, options.Tracking);
372378
}
373379
}

0 commit comments

Comments
 (0)