During an in-depth look at text rendering for OpenGL with SFML, I came across by comparing my text output with the output of the same text in Firefox (102.5.0esr 64bit) that the underlying library FreeType provides the glyphs cleanly, but the text still looks different - because the kerning is missing. This circumstance will be examined in more detail here.
The Problem
Let me start with a picture, that says more than 1000 words. You can see the same text:
- in my widget (line 1) - rendered with SFML (based on OpenGL and FreeType),
- in a rudimentary OpenGL window (line 2) - rendered with FreeType (and self made kerning) and
- in Firefox 102.5.0esr 64bit (line 3) - the reference.
I'll zoom in on the first 3 words:
You can see how space is wasted in the first line between "tt" (Formatted) and "Te"(Text) - in other words, the kerning is missing.
Kerning
For those who are not so familiar with the details of text rendering - Wikipedia says:
Quote:
In typography, kerning is the process of adjusting the spacing between characters in a proportional font, usually to achieve a visually pleasing result. Kerning adjusts the space between individual letterforms, while tracking (letter-spacing) adjusts spacing uniformly over a range of characters.
Cause
Since Firefox used Roboto-Regular as the default sans-serif font, I used this for all tests. Roboto is a fairly new font and kerning for this font is not implemented via the "old" kerning table, but GPOS. Unfortunately, I haven't found a way to enable GPOS in FreeType yet.
Not only SFML, but also a lot of other libraries rely on FreeType - so the question is obvious whether there isn't a solution for FreeType.
The Solutions
Self-Made Kerning
I use the Learn OpenGL - Text Rendering article as a basis (a source code download is available below the image, that shows the sample application running) to demonstrate this approach. With three small additions, I can provide a basic kerning and produce the second line (see my first image - green text on olive background).
First: A class, that provides convenient access to a simple kerning map.
class CharTupleToKernMap : public std::map<uint64_t, uint32_t>
{
public:
inline void add(wchar_t previous, wchar_t current, uint32_t kern)
{
uint64_t charTuple = (((uint64_t)previous) << 32) + (uint64_t)current;
this->insert(std::pair<uint64_t, uint32_t>(charTuple, kern));
}
inline std::map<uint64_t, uint32_t>::iterator get(wchar_t previous,
wchar_t current)
{
uint64_t charTuple = (((uint64_t)previous) << 32) + (uint64_t)current;
return this->find(charTuple);
}
};
Second: The initialization of the map.
...
Kernings.add('a', 't', 1 << 6);
Kernings.add('b', 'l', 1 << 6);
Kernings.add('e', 'l', 1 << 6);
Kernings.add('e', 't', 1 << 6);
Kernings.add('e', 'x', 1 << 6);
Kernings.add('e', 'y', 1 << 6);
Kernings.add('n', 't', 1 << 6);
Kernings.add('t', 'a', 1 << 6);
Kernings.add('t', 'e', 1 << 6);
Kernings.add('t', 'l', 1 << 6);
Kernings.add('t', 't', 1 << 6);
Kernings.add('x', 'e', 1 << 6);
Kernings.add('y', 'e', 1 << 6);
Kernings.add('T', 'e', 2 << 6);
Kernings.add('T', 'M', 1 << 6);
...
Third: The application of the map.
...
FT_UInt previous_glyph_index = 0;
wchar_t previous_char = 0;
...
if (FT_HAS_KERNING(face))
{
FT_UInt current_glyph_index = FT_Get_Char_Index(face, (FT_ULong)*c);
if (previous_glyph_index != 0)
{
FT_Vector delta;
FT_Get_Kerning(face, previous_glyph_index, current_glyph_index,
FT_KERNING_DEFAULT, &delta);
kerning = delta.x >> 6;
}
previous_glyph_index = current_glyph_index;
}
else
{
wchar_t current_char = (wchar_t)*c;
if (previous_char != 0)
{
auto kernIndex = Kernings.get(previous_char, *c);
if (kernIndex != Kernings.end())
kerning = kernIndex->second >> 6;
}
previous_char = current_char << 32;
}
...
But beware: This is not a complete solution to the problem! On the one hand, not all required character combinations are covered here. On the other hand, the effort increases linearly the more fonts are to be supported - because each font has its own kerning.
In a second step, this approach could be integrated into SFML (or comparable libraries) through a derived or replacing Font class.
Use Older Fonts
The FreeType API documentation for FT_Get_Kerning states:
Quote:
Kerning for OpenType fonts implemented in a ‘GPOS’ table is not supported; use FT_HAS_KERNING to find out whether a font has data that can be extracted with FT_Get_Kerning.
And as far as I know, this is also true for newer TTF fonts. So using, e.g., NotoSans instead of Roboto solves the problem.
Support FreeType
There is an announcement from Wed, 05 Jan 2000: I can finally announce that full GPOS support is available! GSUB, GPOS, and GDEF support now complies to OpenType 1.2.
That was back in the days of FreeType 1.3. As far as I know, GPOS is still not in the standard in the current FreeType 2.12 version.
Use Alternatives
The FreeType 2 Tutorial states:
Quote:
Not all font formats contain kerning information, and not all kerning formats are supported by FreeType; in particular, for TrueType fonts, the API can only access kerning via the ‘kern’ table. OpenType kerning via the ‘GPOS’ table is not supported! You need a higher-level library like HarfBuzz, Pango, or ICU, since GPOS kerning requires contextual string handling.