Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia

Quality Losses (Related to Kerning) in Text Rendering for OpenGL with FreeType

5.00/5 (6 votes)
17 Dec 2022CPOL3 min read 8.7K  
High-end quality in text rendering concerns not only the characters, but also the character spaces - and here FreeType is not quite up to date anymore: The kerning used by FreeType is not always available (especially with newer fonts).
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:

  1. in my widget (line 1) - rendered with SFML (based on OpenGL and FreeType),
  2. in a rudimentary OpenGL window (line 2) - rendered with FreeType (and self made kerning) and
  3. in Firefox 102.5.0esr 64bit (line 3) - the reference.

Image 1

I'll zoom in on the first 3 words:

Image 2

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.

Image 3

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.

C++
///////////////////////////////////////////////////////////////////////////////////
/// @brief Provide convenient access to a simple kerning map
///////////////////////////////////////////////////////////////////////////////////
class CharTupleToKernMap : public std::map<uint64_t, uint32_t>
{
public:
    ///////////////////////////////////////////////////////////////////////////////
    /// @brief Add a character combination with the associated kerning
    ///
    /// @param previous  The previous character
    /// @param current   The current character
    /// @param kern      The kerning in 16.16 fractional points
    ///
    /// @remark On UNIX 'wchar_t' is typically 4 bytes
    ///////////////////////////////////////////////////////////////////////////////
    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));
    }

    ///////////////////////////////////////////////////////////////////////////////
    /// @brief Get the iterator of a character combination
    ///
    /// @param previous  The previous character
    /// @param current   The current character
    ///
    /// @return The iterator on success, or the end() iterator otherwise
    ///////////////////////////////////////////////////////////////////////////////
    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.

C++
...

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.

C++
...

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)