Skip to content

Commit dff139d

Browse files
Update rounding, optimize processing
1 parent e8b7074 commit dff139d

File tree

11 files changed

+275
-35
lines changed

11 files changed

+275
-35
lines changed

src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,41 +1872,172 @@ private float Round(float value)
18721872
{
18731873
switch (this.state.RoundState)
18741874
{
1875+
case RoundMode.Off:
1876+
// FreeType's Round_None with compensation = 0.
1877+
return value;
1878+
18751879
case RoundMode.ToGrid:
1876-
return value >= 0 ? (float)Math.Round(value) : -(float)Math.Round(-value);
1880+
{
1881+
// Round_To_Grid with compensation = 0.
1882+
if (value >= 0F)
1883+
{
1884+
float val = (float)Math.Floor(value + 0.5F);
1885+
if (val < 0F)
1886+
{
1887+
val = 0F;
1888+
}
1889+
1890+
return val;
1891+
}
1892+
else
1893+
{
1894+
float val = -(float)Math.Floor(-value + 0.5F);
1895+
if (val > 0F)
1896+
{
1897+
val = 0F;
1898+
}
1899+
1900+
return val;
1901+
}
1902+
}
1903+
18771904
case RoundMode.ToHalfGrid:
1878-
return value >= 0 ? (float)Math.Floor(value) + 0.5f : -((float)Math.Floor(-value) + 0.5f);
1879-
case RoundMode.ToDoubleGrid:
1880-
return value >= 0 ? (float)(Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2) : -(float)(Math.Round(-value * 2, MidpointRounding.AwayFromZero) / 2);
1905+
{
1906+
// Round_To_Half_Grid with compensation = 0.
1907+
if (value >= 0F)
1908+
{
1909+
float val = (float)Math.Floor(value) + 0.5F;
1910+
if (val < 0F)
1911+
{
1912+
val = 0.5F;
1913+
}
1914+
1915+
return val;
1916+
}
1917+
else
1918+
{
1919+
float val = -((float)Math.Floor(-value) + 0.5F);
1920+
if (val > 0F)
1921+
{
1922+
val = -0.5F;
1923+
}
1924+
1925+
return val;
1926+
}
1927+
}
1928+
18811929
case RoundMode.DownToGrid:
1882-
return value >= 0 ? (float)Math.Floor(value) : -(float)Math.Floor(-value);
1930+
{
1931+
// Round_Down_To_Grid with compensation = 0.
1932+
if (value >= 0F)
1933+
{
1934+
float val = (float)Math.Floor(value);
1935+
if (val < 0F)
1936+
{
1937+
val = 0F;
1938+
}
1939+
1940+
return val;
1941+
}
1942+
else
1943+
{
1944+
float val = -(float)Math.Floor(-value);
1945+
if (val > 0F)
1946+
{
1947+
val = 0F;
1948+
}
1949+
1950+
return val;
1951+
}
1952+
}
1953+
18831954
case RoundMode.UpToGrid:
1884-
return value >= 0 ? (float)Math.Ceiling(value) : -(float)Math.Ceiling(-value);
1955+
{
1956+
// Round_Up_To_Grid with compensation = 0.
1957+
if (value >= 0F)
1958+
{
1959+
float val = (float)Math.Ceiling(value);
1960+
if (val < 0F)
1961+
{
1962+
val = 0F;
1963+
}
1964+
1965+
return val;
1966+
}
1967+
else
1968+
{
1969+
float val = -(float)Math.Ceiling(-value);
1970+
if (val > 0F)
1971+
{
1972+
val = 0F;
1973+
}
1974+
1975+
return val;
1976+
}
1977+
}
1978+
1979+
case RoundMode.ToDoubleGrid:
1980+
{
1981+
// Round_To_Double_Grid: grid step is 0.5 pixels.
1982+
const float step = 0.5F;
1983+
1984+
if (value >= 0F)
1985+
{
1986+
float val = step * (float)Math.Floor((value / step) + 0.5F);
1987+
if (val < 0F)
1988+
{
1989+
val = 0F;
1990+
}
1991+
1992+
return val;
1993+
}
1994+
else
1995+
{
1996+
float val = -step * (float)Math.Floor((-value / step) + 0.5F);
1997+
if (val > 0F)
1998+
{
1999+
val = 0F;
2000+
}
2001+
2002+
return val;
2003+
}
2004+
}
2005+
18852006
case RoundMode.Super:
18862007
case RoundMode.Super45:
1887-
float result;
1888-
if (value >= 0)
2008+
{
2009+
// Round_Super / Round_Super_45 with compensation = 0.
2010+
float period = this.roundPeriod;
2011+
float phase = this.roundPhase;
2012+
float threshold = this.roundThreshold;
2013+
2014+
if (value >= 0F)
18892015
{
1890-
result = value - this.roundPhase + this.roundThreshold;
1891-
result = (float)Math.Truncate(result / this.roundPeriod) * this.roundPeriod;
1892-
result += this.roundPhase;
1893-
if (result < 0)
2016+
float val = value - phase + threshold;
2017+
val = (float)Math.Floor(val / period) * period;
2018+
val += phase;
2019+
2020+
if (val < 0F)
18942021
{
1895-
result = this.roundPhase;
2022+
val = phase;
18962023
}
2024+
2025+
return val;
18972026
}
18982027
else
18992028
{
1900-
result = -value - this.roundPhase + this.roundThreshold;
1901-
result = -(float)Math.Truncate(result / this.roundPeriod) * this.roundPeriod;
1902-
result -= this.roundPhase;
1903-
if (result > 0)
2029+
float val = -value - phase + threshold;
2030+
val = (float)Math.Floor(val / period) * period;
2031+
val = -val - phase;
2032+
2033+
if (val > 0F)
19042034
{
1905-
result = -this.roundPhase;
2035+
val = -phase;
19062036
}
1907-
}
19082037

1909-
return result;
2038+
return val;
2039+
}
2040+
}
19102041

19112042
default:
19122043
return value;

src/SixLabors.Fonts/TextLayout.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,18 @@ private static TextBox ProcessText(ReadOnlySpan<char> text, TextOptions options)
175175
// Update the positions of the glyphs in the completed collection.
176176
// Each set of metrics is associated with single font and will only be updated
177177
// by that font so it's safe to use a single collection.
178-
foreach (TextRun textRun in textRuns)
178+
Font? lastFont = null;
179+
for (int i = 0; i < textRuns.Count; i++)
179180
{
181+
TextRun textRun = textRuns[i];
182+
183+
if (textRun.Font == lastFont)
184+
{
185+
continue;
186+
}
187+
180188
textRun.Font!.FontMetrics.UpdatePositions(positionings);
189+
lastFont = textRun.Font;
181190
}
182191

183192
foreach (Font font in fallbackFonts)
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
927 KB
Binary file not shown.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Text;
5+
using SixLabors.Fonts.Unicode;
6+
7+
namespace SixLabors.Fonts.Tests;
8+
9+
public class HintingTests
10+
{
11+
public static TheoryData<string, string> HintingTestData { get; } = new()
12+
{
13+
// Arial and Tahoma are legacy TrueType fonts whose bytecode was written
14+
// for pre-ClearType rasterizers. Under a v40-style interpreter (vertical
15+
// hinting only, no horizontal grid-fitting, no backward-compatibility
16+
// constraints), both fonts generally render cleanly, but small differences
17+
// in horizontal features, joins and bar heights can occur at low ppem.
18+
// This behaviour matches FreeType v40 expectations for older fonts that
19+
// relied on full-axis grid fitting in legacy engines.
20+
{ TestFonts.Arial, nameof(TestFonts.Arial) },
21+
{ TestFonts.Tahoma, nameof(TestFonts.Tahoma) },
22+
23+
// Modern ClearType-hinted OpenType fonts (for example Open Sans) are
24+
// authored for the same vertical-dominant model used by v40 and therefore
25+
// render consistently and predictably under these semantics.
26+
{ TestFonts.OpenSansFile, nameof(TestFonts.OpenSansFile) },
27+
};
28+
29+
[Theory]
30+
[MemberData(nameof(HintingTestData))]
31+
public void Test_Hinting_Robustness(string path, string name)
32+
{
33+
const string copy = "The quick brown fox jumps over the lazy dog.";
34+
FontCollection collection = new();
35+
FontFamily family = collection.Add(path);
36+
Font font = family.CreateFont(5);
37+
38+
int fontSize = 5;
39+
int start = 0;
40+
int end = copy.GetGraphemeCount();
41+
int length = (end - start) + 1; // include the line ending.
42+
List<TextRun> textRuns = [];
43+
StringBuilder stringBuilder = new();
44+
while (fontSize < 64)
45+
{
46+
stringBuilder.AppendLine(copy);
47+
TextRun run = new()
48+
{
49+
Start = start,
50+
End = end,
51+
Font = new Font(font, fontSize),
52+
};
53+
54+
textRuns.Add(run);
55+
fontSize += 1;
56+
start += length;
57+
end += length;
58+
}
59+
60+
string text = stringBuilder.ToString();
61+
62+
TextOptions options = new(font)
63+
{
64+
TextRuns = textRuns,
65+
HintingMode = HintingMode.Standard,
66+
};
67+
68+
TextLayoutTestUtilities.TestLayout(
69+
text,
70+
options,
71+
properties: name);
72+
}
73+
}

tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
using System.Text;
77
using SixLabors.Fonts.Tests.ImageComparison;
88
using SixLabors.ImageSharp;
9+
using SixLabors.ImageSharp.Formats.Png;
910
using SixLabors.ImageSharp.PixelFormats;
1011

1112
namespace SixLabors.Fonts.Tests.TestUtilities;
1213

1314
public static class TestImageExtensions
1415
{
16+
private static readonly PngEncoder Encoder = new()
17+
{
18+
CompressionLevel = PngCompressionLevel.BestCompression,
19+
FilterMethod = PngFilterMethod.Adaptive
20+
};
21+
1522
public static string DebugSave(
1623
this Image image,
1724
string extension = null,
@@ -25,7 +32,7 @@ public static string DebugSave(
2532
}
2633

2734
string path = Path.Combine(outputDirectory, $"{test}{FormatTestDetails(properties)}.{extension ?? "png"}");
28-
image.Save(path);
35+
image.Save(path, Encoder);
2936

3037
return path;
3138
}

tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ public void CanRenderEmojiFont_With_COLRv1()
4242
TextLayoutTestUtilities.TestLayout(
4343
text,
4444
options,
45-
includeGeometry: true);
45+
includeGeometry: true,
46+
customDecorations: true);
4647
}
4748

4849
[Fact]
@@ -74,6 +75,7 @@ public void CanRenderEmojiFont_With_SVG()
7475
TextLayoutTestUtilities.TestLayout(
7576
text,
7677
options,
77-
includeGeometry: true);
78+
includeGeometry: true,
79+
customDecorations: true);
7880
}
7981
}

tests/SixLabors.Fonts.Tests/TestFonts.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ public static class TestFonts
267267

268268
public static string Arial => GetFullPath("arial.ttf");
269269

270+
public static string Tahoma => GetFullPath("tahoma.ttf");
271+
270272
public static string CousineRegular => GetFullPath("Cousine-Regular.ttf");
271273

272274
public static string HindRegular => GetFullPath("Hind-Regular.ttf");

0 commit comments

Comments
 (0)