c++fontsgraphicstruetypefreetype

Rendering TrueType fonts as fixed size (like in a terminal emulator)


Context: I'm working on a terminal emulator that currently uses a bitmap font (in my case, it is just a PNG file with the glyphs located at the positions that can be calculated as per their order in ASCII with the font resolution being provided in pixels such as 8x16). This was a simple but working solution that was very easy to implement. Now I want to work with TrueType fonts. I'm using the libfreetype2 to extract bitmap data for glyphs.

So my idea is that I'm going to extract ASCII characters from the TTF, turn them into SDL_Surfaces which can be blitted directly to the screen (this is what I do right now except I'm extracting pixel data from the PNG).

Now the issue with TrueType fonts of course is that the characters are not fixed width and therefore only occupy space only what they need. On the other hand, the terminal is obviously expecting the characters to be fixed width, as that I assume are generally how terminal emulators work (correct me if I am wrong).

What's the best way to do this for something like my usecase?

So my solution was to create an $n \times n$ bitmap (here $n$ is the pixel size of the font such as 16), and then transfer all the bitmap data to this buffer, shifting it to center it horizontally, and aligning the bottom of the FreeType bitmap with the bottom of the $n \times n$ grid. This of course seems to work for the most part except for letters such as 'g', 'y', 'p', 'q' where it looks awkward.

My attempt:

#define SCREEN_W 800
#define SCREEN_H 600

#include <iostream>
#include <string>
#include <SDL2/SDL.h>
#include <stdint.h>
#include <ft2build.h>
#include FT_FREETYPE_H

class TrueTypeFont
{
public:
    TrueTypeFont(std::string filename, int mw, int mh)
    {
        err = FT_Init_FreeType(&library);
        if (err)
        {
            std::cerr << "Failed to start freetype." << std::endl;
        }

        err = FT_New_Face(library, filename.c_str(), 0, &face);
        this->mono_height = mh;
        this->mono_width = mw;

        /* Create 256 SDL_Surfaces that can be blitted at request */
        err = FT_Set_Pixel_Sizes(face, mono_width, mono_height);

        for (int i = 0; i < 256; i++)
        {
            FT_UInt glyph_index = FT_Get_Char_Index(face, i);
            err = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
            FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);

            uint8_t *src = face->glyph->bitmap.buffer;

            /* Allocate a fixed size buffer */
            uint8_t* letter_buf = (uint8_t*)calloc(mono_height * mono_width, sizeof(uint8_t));
            letter_buffers[i] = letter_buf;

            int height = face->glyph->bitmap.rows;
            int width = face->glyph->bitmap.width;
            /* Center the bitmap horizontally into our fixed size buffer */
            int offsetx = (mono_width - width) / 2;
            /* Match the botom with the bottom of the fixed size buffer */
            int offsety = (mono_height - height);

            /* Copy data from source buffer into our fixed size buffer */
            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    uint8_t val = src[i * width + j];
                    /* Remove anti-aliasing, just have 1 or 0. */
                    if(val > 128)
                        letter_buf[(offsety + i) * mono_width + (offsetx + j)] = 255;
                    
                }
            }

            /* Create SDL_Surface from bitmap */
            letter_surfaces[i] = SDL_CreateRGBSurfaceFrom(letter_buf, mono_width, mono_height, 8, mono_width, 0, 0, 0, 0xff);

            /* Set the palette colors accordingly 0 = black, 255 = white */
            SDL_Color colors[256];
            for (int j = 0; j < 256; j++)
            {
                colors[j].r = colors[j].g = colors[j].b = j;
                colors[j].a = j;
            }
            SDL_SetPaletteColors(letter_surfaces[i]->format->palette, colors, 0, 256);
            /* Make black transparent */
            SDL_SetColorKey(letter_surfaces[i], SDL_TRUE, 0);
        }
    }

    /* Draw a string at x,y */
    void Puts(SDL_Surface* screen, int x, int y, std::string s)
    {
        SDL_Rect r;
        r.x = x;
        r.y = y;
        r.w = this->mono_width;
        r.h = this->mono_height;
        for(int i = 0; i < s.length(); i++)
        {
            SDL_Surface* glyph = this->GetGlyph(s[i]);
            SDL_BlitSurface(glyph, NULL, screen, &r);
            r.x += this->mono_width / 2;
        }
    }


    SDL_Surface* GetGlyph(char c)
    {
        return letter_surfaces[c];
    }

    /* Destructor */
    ~TrueTypeFont()
    {
        for(int i = 0; i < 256; i++)
        {
            SDL_FreeSurface(this->letter_surfaces[i]);
            free(this->letter_buffers[i]);
        }
    }

private:
    int mono_width, mono_height;
    FT_Library library;
    FT_Face face;
    FT_Error err;
    SDL_Surface* letter_surfaces[256];
    uint8_t* letter_buffers[256];
};

int main(void)
{
    SDL_Init(SDL_INIT_EVERYTHING);

    SDL_Window *window = SDL_CreateWindow("FreeType Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_W, SCREEN_H, SDL_WINDOW_SHOWN);
    SDL_Surface *window_surface = SDL_GetWindowSurface(window);

    TrueTypeFont arial("arial.ttf", 16, 16);
    TrueTypeFont proggy("ProggyClean.ttf", 16, 16);

    bool running = true;
    while (running)
    {
        SDL_Event ev;
        while (SDL_PollEvent(&ev))
        {
            if (ev.type == SDL_QUIT)
            {
                running = false;
            }
        }
        SDL_Rect r;
        SDL_FillRect(window_surface, NULL, 0x0000A0);
        arial.Puts(window_surface, 0, 0, "Hello World!");
        proggy.Puts(window_surface, 200, 200, "The quick brown fox jumps over the lazy dog. {} [] @ # $ * &");
        SDL_UpdateWindowSurface(window);
    }
    return 0;
}

The result (I need reputation):

TrueType Output

You can see that the 'p' and 'q' are definitely wrongly rendered.

I'm not really sure if I'm doing things the way they're supposed to be done.


Solution

  • You don't take the bearing of the glyph into consideration (see: https://freetype.org/freetype2/docs/tutorial/glyph-metrics-3.svg).

    The bearing information can be retrieved (after rendering the glyph bitmap) like:

    FT_FaceRec::FT_GlyphSlotRec::bitmap_left; //horizontal bearing
    FT_FaceRec::FT_GlyphSlotRec::bitmap_top;  //vertical bearing
    
    //e.g. 
    face->glyph->bitmap_left; //x bearing
    face->glyph->bitmap_top;  //y bearing
    

    See also: https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyphslotrec

    To calculate the drawing position:

    x = pen_x + glyph->bitmap_left;
    y = pen_y - glyph->bitmap_top; 
    
    //or 
    //y = pen_y - (glyph_height - glyph->bitmap_top);
    //where glyph_height = glyph->bitmap.rows
    

    See also: https://freetype.org/freetype2/docs/tutorial/step1.html#section-7

    Once you've calculated the 'correct' position of the glyph, then go and adjust it into the (fixed sized) cell.