Search code examples
c++openglfontstext-renderingfreetype2

How do I align Glyphs along the baseline with Freetype?


I have written a function that creates an image from a font using Freetype2. My goal is to create an OpenGL-texture with all characters from the font. But first, I want to make sure the texture gets created properly, thus I store it as an image with stb_image_write for now. This is my progress so far:

The first few letters from the image

As you can see, the glyphs are not aligned along the baseline, but along the top of the image. So I decided to introduce a variable called "yOffset", which describes the amount of pixels that each glyph has to be moved down to align them properly.

According to an answer in Render FreeType text with flipped ortho, difference between top and baseline of glyph, this offset can be calculated with

yOffset = (Glyph with highest y-Bearing)->bitmap_top - glyph->bitmap_top

This value is stored in "maxAscent" with the first loop, which finds out the metrics of each glyph.

Here's a visual representation of the glyph-metrics:

enter image description here

Here's my function:

static void loadFontImage(const std::string& fontPath, unsigned int fontSize, const std::string& imagePath) {
        FT_Library library;
        FT_Face face;

        unsigned int imageWidth = 0;
        unsigned int imageHeight = 0;
        unsigned int maxAscent = 0;

        if (FT_Init_FreeType(&library)) {
            std::cerr << "Could not initialize font library" << std::endl;
            std::exit(-1);
        }

        if (FT_New_Face(library, fontPath.c_str(), 0, &face)) {
            std::cerr << "Could not load font '" << fontPath << "'" << std::endl;
            std::exit(-1);
        }

        FT_Set_Char_Size(face, 0, fontSize * 64, 300, 300);

        FT_GlyphSlot glyph = face->glyph;

        for (unsigned int c = 32; c < 256; c++) {
            if (c == 127) continue;
            if (FT_Load_Char(face, c, FT_LOAD_RENDER)) continue;

            imageWidth += glyph->bitmap.width;

            if (glyph->bitmap.rows > (int)imageHeight) {
                imageHeight = glyph->bitmap.rows;
            }

            if (glyph->bitmap_top > (int)maxAscent) {
                maxAscent = glyph->bitmap_top;
            }
        }

        unsigned int size = imageWidth * imageHeight;
        unsigned char* buffer = new unsigned char[size];
        unsigned int xOffset = 0;
        unsigned int yOffset = 0;

        for (unsigned int c = 32; c < 256; c++) {
            if (c == 127) continue;
            if (FT_Load_Char(face, c, FT_LOAD_RENDER)) continue;

            yOffset = maxAscent - glyph->bitmap_top;

            for (unsigned int x = 0; x < glyph->bitmap.width; x++) {
                for (unsigned int y = 0; y < glyph->bitmap.rows; y++) {
                    unsigned int imageIndex = (x + xOffset) + (y + yOffset) * imageWidth;
                    unsigned int bitmapIndex = x + y * glyph->bitmap.width;
                    buffer[imageIndex] = glyph->bitmap.buffer[bitmapIndex];
                }
            }

            xOffset += glyph->bitmap.width;
        }

        stbi_write_png(imagePath.c_str(), imageWidth, imageHeight, 1, buffer, imageWidth);

        delete[] buffer;
    }
}

But since I have introduced the yOffset, an "index out of bounds"-type exception occurs only at certain glyphs, because the height of the glyph plus the yOffset is larger than the image height.

I assume this is because my formula for "maxAscent" is not correct, and therefor my yOffset is too large for certain characters. Thus my question: Is this indeed the proper formula, and if it is, what else might be wrong with my algorithm? Is there perhaps an easier way to achieve proper alignment?


Solution

  • I solved this issue with a simple fix, after I noticed that the actual height of the image was not calculated correctly. This is because the height of the "tallest" glyph does not represent the actual height of the image. For example, a character might be smaller than the tallest glyph, but placed higher above the baseline, and thus being out of bounds.

    I introduced another variable called "maxDescent", which stores the maximum amount of pixels below the baseline. It is calculated as following in the first loop:

    if ((glyph->metrics.height >> 6) - glyph->bitmap_top > (int)maxDescent) {
        maxDescent = (glyph->metrics.height >> 6) - glyph->bitmap_top;
    }
    

    Together with "maxAscent", which stores the maximum amount of pixels above the baseline, I can calculate the actual height needed for the image by simply adding them together.

    imageHeight = maxAscent + maxDescent
    

    Here's a part from the result:

    Resulting Font Image