Efficient text rendering with Freetype

Posted by The Phoenix on Mon, 24 Jan 2022 02:36:59 +0100

Efficient text rendering with Freetype

FreeType is a free font rendering software library. It is written in C language. The design is small, efficient, highly customizable and portable. At the same time, it can produce high-quality output (font image) in most vector and bitmap font formats.

Freetype use

Header file

/*	Contains various macro declarations that will be used later to #include the appropriate public FreeType 2 header file */
#include <ft2build.h>		
/* Contains freetype2 API header files.
Macro ft should be used_ FREETYPE_ H to achieve this, as shown in the following example.*/
#include FT_FREETYPE_H
#include FT_BITMAP_H
#include FT_OUTLINE_H

Library initialization

To initialize the freetype library, you need to create an FT named library_ A variable of type library and call the function FT_Init_FreeType.

FT_Library  library;
error = FT_Init_FreeType( &library );
if ( error )
{
  ... an error occurred during library initialization ...
}
  • Create a new instance of the FreeType 2 library and set the handle library for it

  • Load each module supported by FreeType in the library, such as TrueType, Type 1, CID - Key & OpenType / CFF fonts.

Load font appearance

The project needs to load fonts from files. Here, take loading from files as an example

By calling FT_New_Face creates a new font appearance. A font appearance describes a given font and style. For example, "Times New Roman Regular" and "Times New Roman Italic" correspond to two different font appearances.

FT_Library  library;   /* handle to library     */
FT_Face     face;      /* handle to face object */

error = FT_Init_FreeType( &library );
if ( error ) { ... }

error = FT_New_Face( library,
                     "/usr/share/fonts/truetype/arial.ttf",
                     0,
                     &face );
if ( error == FT_Err_Unknown_File_Format )
{
  ... the font file could be opened and read, but it appears
  ... that its font format is unsupported
}
else if ( error )
{
  ... another error code means that the font file could not
  ... be opened or read, or that it is broken...
}

FT_New_Face parameter description

  • Library: handle to the FreeType library instance that creates the font appearance object
  • filepathname: font file pathname (standard C string)
  • face_index:
    • Generally, font formats allow multiple font appearances to be embedded in a file
    • This index tells you which appearance to load and returns an error if its value is too large
    • But index 0 is always valid
    • To know how much appearance a given font file contains, set face_ Set index to - 1 and check face - > num_ The value of faces, which indicates how many appearances are embedded in the font file
  • face: a pointer to the appearance of the font. If an error occurs, it is set to NULL.

To create a font interface from a file:

/**
 * @brief               load font
 * @details             
 *     Loads a font from a file based on the font attributes \n 
 * passed in and stores it in an array of fonts in CfontManager
 * @param library       Font library, initialize before use
 * @param filename      The font path
 * @param face_index    font face, Usually 0
 * @param tall          font tall in pixels, also known as font size 
 * @param bold          bold or not
 * @param italic        italic or not
 * @param antialias     antialias or not
 * @return true         create successfully
 * @return false        Font creation failed
 */
bool CFont::create(FT_Library library, const char* filename, FT_Long face_index, int tall, bool bold, bool italic, bool antialias)
{
    FT_Error err;
    
    // bind the font face in the file to m_face 
    if ((err = FT_New_Face(library, filename, face_index, &m_face)) != FT_Err_Ok)
    {
        printf("FT_New_Face() Error %d\n", err);
        return false;
    }
    
    // Sets the render pixel size in preparation for bitmap generation
    if ((err = FT_Set_Pixel_Sizes(m_face, 0, tall)) != FT_Err_Ok)
    {
        printf("FT_Set_Pixel_Sizes() Error %d\n", err);
        return false;
    }

    string path = filename;
    m_name = CStringUtil::getNameFromSrc(path);

    m_library = library;
    m_antialias = antialias;
    m_bold = bold;
    m_tall = tall;

    if (italic)
    {
        FT_Matrix m;
        m.xx = 0x10000L;
        m.xy = 0.5f * 0x10000L;
        m.yx = 0;
        m.yy = 0x10000L;
        FT_Set_Transform(m_face, &m, NULL);
    }
    

    return true;
}

Read appearance data

Font appearance object is an integrated description of all data describing font appearance. Generally, you can directly use - > to access the data in the font appearance object, or you can see the data contained in the font appearance object in the debugging window. Its complete data is defined by ft_ Given by facerec

The following is a detailed description of some core data:

  • num_glyphs: this variable provides the number of glyphs available in the font appearance. A glyph can be understood as a character image
  • face_flags: a 32-bit integer containing bit flags that describe some surface properties. For example, flag FT_FACE_FLAG_SCALABLE the font format that represents the appearance of a font is scalable and can be rendered as characters of any size. For more information, read the FreeType 2 API reference.
  • units_per_EM: this field is only valid for scalable formats (otherwise set to 0). It represents the number of font units covered by EM.
  • num_fixed_sizes: this field gives the number of bitmaps embedded in the current font appearance. A strike can be understood as a series of font data rendered for a given character pixel size
  • available_sizes: a point to FT_ Bitmap_ Pointer to the size element array. Each FT_Bitmap_Size indicates the horizontal and vertical character pixel size of each strike that appears in the appearance.

Set pixel size

In the previous step, we learned the pixel size of the optional character rendering. Only after the pixel size of the character is correctly set, can the character be rendered into a bitmap.

FreeType 2 uses size objects to model all information related to a given character size for a given font appearance. For example, for a 12 point character size, the size object holds the values of some indicators, such as the top half (or bottom half) of the letter or the text height, expressed in 1 / 64 of the pixel (however, these values are rounded to an integer, i.e. a multiple of 64)). When calling ft_ New_ When the face function (or one of its siblings) is, it automatically creates a new size object for the returned font appearance. This size object can be accessed directly as face - > size.

FreeType provides the following two ways to set the character size:

Method 1: FT_Set_Char_Size

error = FT_Set_Char_Size(
          face,    /* handle to face object           */
          0,       /* char_width in 1/64th of points  */
          16*64,   /* char_height in 1/64th of points */
          300,     /* horizontal device resolution    */
          300 );   /* vertical device resolution      */

be careful

  • The character width and height are specified in 1 / 64 of the dot. The point is the physical distance, equal to 1 / 72 inch. Usually, it is not equivalent to a pixel.
  • A character width value of 0 means "same as the character height", and a character height value of 0 means "same as the character width". Otherwise, you can specify different character widths and heights.
  • Horizontal and vertical device resolutions are expressed in dots per inch or dpi. For display devices such as screens, the standard value is 72 or 96 dpi. Resolution is used to calculate the character pixel size according to the character point size. A horizontal resolution of 0 means "same as vertical resolution", and a vertical resolution of 0 means "same as horizontal resolution". If both values are zero, both dimensions use 72 dpi.

Method 2: FT_Set_Pixel_Sizes

error = FT_Set_Pixel_Sizes(
          face,   /* handle to face object */
          0,      /* pixel_width           */
          16 );   /* pixel_height          */

This example sets the character pixel size to 16 × 16 pixels. As mentioned earlier, a value of 0 for one dimension means "same as the other dimension". Notice that both functions return error codes. Usually, when trying to set the pixel size to face - > fixed_ Fixed size font formats (such as FNT or PCF) can cause errors when values are not listed in the sizes array.

Load glyph symbols

**Step 1 convert character codes to glyph indexes**

Typically, applications want to load glyph images based on character codes, which are unique values that define a given encoded character. For example, code 65 (0x41) represents the character "a" in ASCII encoding. A face object contains one or more tables called charmaps, which are used to convert character codes into font indexes. For example, most older TrueType fonts contain two character mappings: one for converting Unicode character codes to glyph indexes and the other for converting Apple Roman codes to glyph indexes. Such fonts can be used on Windows (using Unicode) and older MacOS versions (using apple Roman). Also note that a given charmap may not map to all glyphs that exist in the font.

By default, FreeType attempts to select the Unicode character set when creating a new font appearance object. If the font does not contain such a character mapping table, it simulates the Unicode character mapping table according to the font name. Note that if the font name is not standard, the font may be omitted.

FT_Get_Char_Index

glyph_index = FT_Get_Char_Index( face, charcode );

FreeType provides the above interface to obtain font index

Note that this is one of the rare FreeType functions that does not return an error code. However, when the given character code has no font image on the face, the value 0 will be returned. By convention, it always corresponds to a special font image, called missing font, which is usually displayed as a box or a space.

**Step 2 read the data to the font slot by self indexing**

Once the font index is available, the corresponding font image can be loaded. The latter can be stored in font files in various formats. For fixed size formats such as FNT or PCF, each image is a bitmap.

Glyph images are always stored in special objects called glyph slots. As the name suggests, a glyph slot is a container that can hold one glyph image at a time. It can be a bitmap, outline or other thing. Each font appearance object has a separate font slot object that can be accessed as face - > glyph.

error = FT_Load_Glyph(
          face,          /* handle to face object */
          glyph_index,   /* glyph index           */
          load_flags );  /* load flags, see below */

load_ The flags value is a set of bit flags used to indicate some special operations. Default FT_LOAD_DEFAULT is 0. This function attempts to load the corresponding font image from the font appearance. If a bitmap corresponding to the font and pixel size is found, it is loaded into the slot. Embedded bitmaps are always preferred over native image formats because we assume that they are a higher quality version of the same glyph. This can be done by using FT_LOAD_NO_BITMAP flag to change.

Step 3 render glyph slots to bitmaps

error = FT_Render_Glyph( face->glyph,   /* glyph slot  */
                         render_mode ); /* render mode */

Parameter render_mode is a set of bit flags that specify how glyph images are rendered. Default FT_RENDER_MODE_NORMAL renders an antialiasing overlay bitmap (also known as a pixel map) with 256 gray levels because this is the default. If you want to generate a 1-bit monochrome bitmap, you can also use FT_RENDER_MODE_MONO. More values are available for FT_Render_Mode enumeration value.

Once you have a bitmap glyph image, you can access it directly through glyph - > bitmap (a simple descriptor of bitmap or pixel graph) and through glyph - > bitmap_left and glyph - > bitmap_top locate it. Note that bitmap_left is the horizontal distance from the current brush position to the leftmost boundary of the font bitmap, and bitmap_top is the vertical distance from the brush position (on the baseline) to the uppermost boundary of the glyph bitmap. Indicates that the upward distance is positive.

/**
 * @brief Reads character data from glyphs and caches it
 * @param code          character code 
 * @param metrics       character metrics 
 * @param screen        pixel matrix        
 * @param position      pixel position
 */
void CFont::loadChar(int code, glyphMetrics *metrics, unsigned char* screen, Vec2i& position)
{
    FT_Error err;
    FT_UInt glyph_index = FT_Get_Char_Index(m_face, (FT_ULong)code);
    // glyph_index is valid
    if (glyph_index > 0)
    {
        if ((err = FT_Load_Glyph(m_face, glyph_index, FT_OUTLINE_HIGH_PRECISION)) == FT_Err_Ok)
        {
            // get font glyph
            FT_GlyphSlot glyph = m_face->glyph;

            FT_Render_Mode render_mode = m_antialias ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO;

            if (m_antialias && m_bold)
            {
                if ((err = FT_Outline_EmboldenXY(&glyph->outline, 64, 64)) != FT_Err_Ok)
                {
                    printf("FT_Outline_EmboldenXY() Error %d\n", err);
                }
            }

            if ((err = FT_Render_Glyph(glyph, render_mode)) == FT_Err_Ok)
            {
                // get bitmap
                FT_Bitmap *bitmap = &glyph->bitmap;

                switch (bitmap->pixel_mode)
                {
                    //MONO mode uses only 1 bit to save every pixel, only black and white.
                    case FT_PIXEL_MODE_MONO:
                    {
                        if (!m_antialias && m_bold)
                        {
                            if ((err = FT_Bitmap_Embolden(m_library, bitmap, 60, 0)) != FT_Err_Ok)
                            {
                                printf("FT_Bitmap_Embolden() Error %d\n", err);
                            }
                        }

                        CChar* tm_char = new CChar(code, ImageType::Mono);
                        Draw_MONO_Bitmap(glyph, screen, position, tm_char);
                        //Draw_Bitmap(glyph, screen, position, tm_char);
                        break;
                    }
                    //GRAY mode 1 pixel is saved with 1 byte.
                    case FT_PIXEL_MODE_GRAY:
                    {
                        CChar* tm_char = new CChar(code, ImageType::Gray);
                        Draw_Bitmap(glyph, screen, position, tm_char);
                        //Draw_Bitmap(glyph->bitmap.buffer, glyph->bitmap.pitch,
                            //glyph->bitmap.rows, data, 30);

                        //ConvertGRAYToRGBA(bitmap, m_rgba);
                        break;
                    }
                    default:
                    {
                        //memset(m_rgba, 0xFF, m_rgbaSize);
                        break;
                    }
                }

                return;
            }
            else
            {
                printf("FT_Render_Glyph() Error %d\n", err);
            }
        }
        else
        {
            printf("FT_Load_Glyph() Error %d\n", err);
        }
    }

    memset(metrics, 0, sizeof(glyphMetrics));

    return ;
}

Design of font rendering module

In order to improve the rendering efficiency of font and reduce hardware communication and interaction, the pixel copy strategy is adopted here

The whole project includes four core categories, which are introduced one by one below:

CChar

It mainly stores the font data, character code, type and bitmap data of characters. In order to unify and facilitate operation, the bitmap data uniformly stores the gray mode. After loading and rendering characters, the character objects will be stored in the cache of font class, and priority will be given to searching from the cache when rendering fonts

enum class ImageType {
    Gray,   /**< GRAY Mode 1 pixel is saved with 1 byte */ 
    Mono    /**< MONO Each pixel of the mode is only saved with 1 bit, only black and white */ 
};

class CChar
{
public:
    CChar(int code, ImageType type);
    virtual ~CChar();

    void setInfo(glyphMetrics* metrics);
    void getInfo(glyphMetrics* metrics);
    unsigned char* getOrCreateBuffer(size_t size);
    //unsigned char& operator [] (size_t i);
    int code() const;
    ImageType type() const;


private:
    int              m_code;            /**< char code. */
    unsigned char*   m_data;            /**< char bitmap data. */
    glyphMetrics     m_metrics;         /**< char metrics data. */
    size_t           m_size;            /**< char bitmap data size. */
    ImageType        m_type;            /**< char bitmap type. */
};

CFont

CFont is the core class for the module to deal with fonts, which is responsible for font loading, character rendering and caching; CFont object is an object instantiated by font parameters. Different font sizes of the same font need to be stored with different objects. It can be understood that a CFont corresponds to a font appearance of Freetype

class CFont
{
public:
    CFont();
    virtual ~CFont();

    bool                    create(FT_Library library, const char *filename, FT_Long face_index, int tall, bool bold, bool italic, bool antialias);

    int                     getFontTall(void) const;

    void                    renderChar(int code, glyphMetrics *metrics, unsigned char* screen, Vec2i& position);

    void                    setColor(Color color);
    void                    setOutline(bool type);
    void                    setOutlineColor(Color color);

    string                  fontName() const;

private:

    void                    loadChar(int code, glyphMetrics *metrics, unsigned char* screen, Vec2i& position);

    void                    Draw_Bitmap(FT_GlyphSlot& glyph, unsigned char* screen, Vec2i& position, CChar* charBuffer);
    void                    Draw_Bitmap(CChar* charBuffer, unsigned char* screen, Vec2i& position);

    void                    Draw_MONO_Bitmap(FT_GlyphSlot& glyph, unsigned char* screen, Vec2i& position, CChar* charBuffer);
    void                    Draw_MONO_Bitmap(CChar* charBuffer, unsigned char* screen, Vec2i& position);

    bool                    isFontEdge(unsigned char*  buffer,int row, int col,int width,int height);

    //typedef                 std::map<int, CChar *> TCharMap;
    //TCharMap                m_chars;
    CBuffer<int, CChar>     m_charBuffers;                  /**< char buffer. */


    FT_Library              m_library;                      /**< font library. */
    FT_Face                 m_face;                         /**< font face. */

    bool                    m_antialias;                    /**< font property: antialias. */
    bool                    m_bold;                         /**< font property: bold. */
    bool                    m_outline;                      /**< font property: outline. */

    Color                   m_color;                        /**< font color: font. */
    Color                   m_outline_color;                /**< font color: outline. */
    Color                   m_bg_color;                     /**< font color: background. */

    int                     m_tall;                         /**< font property: tall (or size). */
    string                  m_name;                         /**< font property: font name. */

};

A key problem here is how to copy pixels to make characters neat and beautiful. This requires character typesetting (or geometric data). The official website gives the specific meaning and diagram of typesetting parameters. Please understand:

Width this is the width of the glyph image bounding box. It is independent of the layout direction.

Height this is the height of the glyph image bounding box. It is independent of the layout direction. Be careful not to associate it with ft_ Size_ The height field in the metrics structure is confused.

horiBearingX for horizontal text layout, this is the horizontal distance from the current cursor position to the leftmost edge of the font image bounding box.

Horibearingyfor horizontal text layout, this is the vertical distance from the current cursor position (on the baseline) to the uppermost boundary of the glyph image bounding box.

horiAdvance for horizontal text layout, this is to increase the horizontal distance of the pen position when the font is drawn as part of the text string.

vertBearingX for vertical text layout, this is the horizontal distance from the current cursor position to the leftmost edge of the glyph image bounding box.

vertBearingY for vertical text layout, this is the vertical distance from the current cursor position (on the baseline) to the uppermost boundary of the glyph image bounding box.

vertAdvance for vertical text layout, this is the vertical distance used to increase the pen position when the glyph is drawn as part of the text string.

The following is an example of a copied pixel:

/**
 * @brief               Draws Gray mode characters to the pixel matrix
 * @details         
 *      From glyphs to load layout data and bitmap data, \n
 * copy to pixel matrix and cache
 * @param glyph         font glyph
 * @param screen        pixel matrix
 * @param position      pixel position
 * @param charBuffer    character buffer
 */
void CFont::Draw_Bitmap(FT_GlyphSlot& glyph, unsigned char* screen, Vec2i& position, CChar* charBuffer) {
    FT_Bitmap *bitmap = (&glyph->bitmap);

    if (bitmap == NULL) {
        printf("bitmap error!\n");
        return;
    }

    int span = glyph->metrics.horiAdvance / 64;
    int posX = glyph->metrics.horiBearingX / 64;
    int posY = m_tall - glyph->metrics.horiBearingY / 64;

    int width = glyph->metrics.width / 64;
    int height = glyph->metrics.height / 64;

    unsigned char* data = charBuffer->getOrCreateBuffer(width * height);

    for (int i = 0; i < height; i++) {
        int index = (posY + i + position[1]) * screenWidth * 4 + (posX + position[0]) * 4;
        for (int j = 0; j < width; j++) {
            int color = bitmap->buffer[i * width + j];
            data[i * width + j] = color;
            if (color == 0)
            {
                screen[index++] = m_bg_color[3];
                screen[index++] = m_bg_color[2];
                screen[index++] = m_bg_color[1];
                screen[index++] = m_bg_color[0];

                //cout << " ";
            }
            else
            {
                if(m_outline && isFontEdge(bitmap->buffer, i, j, width, height))
                {
                    screen[index++] = m_outline_color[3];
                    screen[index++] = m_outline_color[2];
                    screen[index++] = m_outline_color[1];
                    screen[index++] = m_outline_color[0];

                    //cout<< "+";
                }
                else
                {
                    screen[index++] = color * 1.0 / 255 * m_color[3];
                    screen[index++] = color * 1.0 / 255 * m_color[2];
                    screen[index++] = color * 1.0 / 255 * m_color[1];
                    screen[index++] = color * 1.0 / 255 * m_color[0];
                    //cout << "*";
                }
            }
        }
    }

    cout << endl;

    position[0] += span;

    glyphMetrics* metrics = new glyphMetrics;


    metrics->width = glyph->metrics.width ;
    metrics->height = glyph->metrics.height;

    metrics->horiBearingX = glyph->metrics.horiBearingX;
    metrics->horiBearingY = glyph->metrics.horiBearingY;
    metrics->horiAdvance  = glyph->metrics.horiAdvance;
    metrics->vertBearingX = glyph->metrics.vertBearingX;
    metrics->vertBearingY = glyph->metrics.vertBearingY;

    charBuffer->setInfo(metrics);

    if (metrics != 0x00)
        delete metrics;

    m_charBuffers.addBuffer(charBuffer->code(), charBuffer);
    /*
    if ((m_rear + 1) > m_capacity)
        m_chars.insert({charBuffer->code(), charBuffer});
    else {

    }*/
    //writePngFile("./test.png", data, span, 30, 8);
}

CFontManager

Responsible for the storage of loaded fonts and matching the correct fonts according to the font parameters of external rendering

class CFontManager
{
public:
    CFontManager();
    ~CFontManager();

    int         initialize(const char* path);
    int         createFont(const char *filename, int face, int tall, bool bold, bool italic, bool antialias);//Create font
    int         getFontTall(int font_index);
    void        renderChar(int font_index, int code, unsigned char* screen, Vec2i& position, Color color);
    int         getFontIndex(string name, int fontSize);
    void        setStartPosition(Vec2i pos);

private:
    FT_Library      m_library;
    vector<CFont*>  m_fonts;

    Vec2i           m_start;

    string          m_fontPath;
};

CFontRender

Responsible for font rendering. The core is a screen size pixel array. All rendering is operating this pixel data

class CFontRender
{
public:
    CFontRender(int screenWidth, int screenHeight);
    virtual ~CFontRender();

    int initialize(const char* path);

    void loadFont(const char* path, int face, int tall, bool bold, bool italic, bool antialias);

    int render(wstring text, Vec2i pos, int fontSize, string fontName, Color fontColor);
    int render(wstring text, Vec2i pos, int fontIndex, Color fontColor);

    void exportScreenImage(const char* path);

    CFontManager* getFontManager() const;

    void clearScreen();

    unsigned char* getData() const;


private:
    unsigned char* m_screen;
    CFontManager * m_fontManager;

};

Here is a rendering:

reference resources:

[OpenGL] use FreeType library to load fonts and draw text in GL

Free Type Tutorial

Topics: OpenGL