How to calculate width and height of any string properly?

0

I want to draw perfect border around my text. Problem is that text is rendered a bit too high compared to border. I am using orthographics camera with X facing right and Y facing down. glm code: glm::ortho(0.0f, w, h, 0.0f);

To unpack? a font I use freetype.

enum
{
    DPI = 72, //72 or 96
    HIGHRES = 64
};
FT_Library library = nullptr;
FT_Face face = nullptr;

float ascender;
float descender;

float size = 50.0f;

void InitFreeType()
{
        FT_Matrix matrix = {
            static_cast<int>((1.0 / (float)HIGHRES) * 0x10000L),
            static_cast<int>((0.0) * 0x10000L),
            static_cast<int>((0.0) * 0x10000L),
            static_cast<int>((1.0) * 0x10000L) };

        FT_Init_FreeType(&library);

        FT_New_Face(library, "arial.ttf", 0, &face);

        FT_Select_Charmap(face, FT_ENCODING_UNICODE);
        FT_Set_Char_Size(face, static_cast<ulong>(size * (float)HIGHRES), 0, DPI * HIGHRES, DPI);

        FT_Set_Transform(face, &matrix, NULL);

        metrics = face->size->metrics;
        ascender = (metrics.ascender >> 6) * 0.01f;
        descender = (metrics.descender >> 6) * 0.01f;
        height = (metrics.height >> 6) * 0.01f;
        linegap = height - ascender + descender;

        FT_Done_Face(face);
        FT_Done_FreeType(library);
}

glyphs are then packed into a texture atlas

struct Kerning
{
    uint32 codepoint = -1;
    float kerning = 0.0f;
};

struct Glyph
{
    // rect is a struct with floats x, y, w and h
    Rect uv = 0.0f;
    std::vector<Kerning> kerning;

    Vector2<int> bearing = 0;
    Vector2<float> advance = 0.0f;
    Vector2<int> size = 0;

    uint32 codepoint = -1;
};

std::vector<Glyph> glyphs;

void LoadGlyph(uint32 codepoint)
{
    FT_Load_Char(face, codepoint, FT_LOAD_RENDER);

    FT_GlyphSlot slot = face->glyph;
    FT_Bitmap bitmap = slot->bitmap;
    int glyphTop = slot->bitmap_top;
    int glyphLeft = slot->bitmap_left;

    int rightPadding = 1;
    int bottomPadding = 1;

    // atlas.BYTES_PER_PIXEL = 1 since, right now it uses only R channel, may use more later, though.
    int srcWidth = bitmap.width / atlas.BYTES_PER_PIXEL;
    int srcHeight = bitmap.rows;

    int tgtWidth = srcWidth + rightPadding;
    int tgtHeight = srcHeight + bottomPadding;

    auto buffer = std::make_unique<uchar[]>(tgtWidth * tgtHeight * atlas.BYTES_PER_PIXEL);

    uchar* destPointer = buffer.get() + tgtWidth * atlas.BYTES_PER_PIXEL;
    uchar* srcPointer = bitmap.buffer;

    for(int i = 0; i < srcHeight; ++i) {
        memcpy(destPointer, srcPointer, bitmap.width);
        destPointer += tgtWidth * atlas.BYTES_PER_PIXEL;
        srcPointer += bitmap.pitch;
    }

    // gives top-left corner position of the glyph inside the atlas
    auto origin = atlas.PackTexture(buffer.get(), { tgtWidth, tgtHeight });

    Glyph current;
    current.codepoint = codepoint;
    current.size.x = tgtWidth - rightPadding;
    current.size.y = tgtHeight;
    current.bearing.x = glyphLeft;
    current.bearing.y = glyphTop;
    current.uv.x = (float)origin.x / (float)atlas.atlasSize.w;
    current.uv.y = (float)origin.y / (float)atlas.atlasSize.h;
    current.uv.w = (float)current.size.w / (float)atlas.atlasSize.w;
    current.uv.h = (float)current.size.h / (float)atlas.atlasSize.h;
    current.advance.x = slot->advance.x / (float)HIGHRES;
    current.advance.y = slot->advance.y / (float)HIGHRES;   

    glyphs.push_back(current);

    GenerateKerning();

    FT_Done_Face(face);
    FT_Done_FreeType(library);
}

void GenerateKerning()
{
    int glyphIndex = 0;
    int prevIndex = 0;
    Glyph* prevG = nullptr;
    FT_Vector kerning;

    /* For each glyph couple combination, check if kerning is necessary */
    /* Starts at index 1 since 0 is for the special backgroudn glyph */
    for(auto& g : glyphs) {
        glyphIndex = FT_Get_Char_Index(face, g.codepoint);
        g.kerning.clear();
        for(int j = 1; j < glyphs.size(); ++j) {
            prevG = &glyphs[j];
            prevIndex = FT_Get_Char_Index(face, prevG->codepoint);
            FT_Get_Kerning(face, prevIndex, glyphIndex, FT_KERNING_UNFITTED, &kerning);

            if(kerning.x) {
                Kerning k = { prevG->codepoint, kerning.x / static_cast<float>(HIGHRES * HIGHRES) };
                g.kerning.push_back(k);
            }
        }
    }
}

float getKerning(Glyph* glyph, uint32 codepoint)
{
    for(const auto& kerning : glyph->kerning) {
        if(kerning.codepoint == codepoint) {
            return kerning.kerning;
        }
    } return 0.0f;
}

Glyph* FindGlyph(uint32 codepoint)
{
    for(auto& glyph : glyphs) {
        if(glyph.codepoint == codepoint) {
            return &glyph;
        }
    } return nullptr;
}

Glyph* getGlyph(uint32 codepoint)
{
    if(Glyph* glyph = FindGlyph(codepoint)) {
        return glyph;
    }

    if(LoadGlyph(codepoint)) {
        return FindGlyph(codepoint);
    }

    return nullptr;
}

and now it times to rendering part

struct Font
{
    Font() 
    {
        InitFreeType();

        //create a texture, add the texture atlas data to it... code that does not matter in this case
    }

    void DrawString(Renderer2D& renderer, const std::string& string, float x, float y, uint32 color)
    {
        float advancedX = x;
        for(int i = 0; i < string.size(); /* nothing */) {
            const char* c = &string[i];

            //AdvanceIndexAndGetCodepoint() there is no need to show this method, does not change any rendering stuff
            uint32 codepoint = AdvanceIndexAndGetCodepoint(i, c);

            auto glyph = getGlyph(codepoint);
            advancedX += getKerning(glyph, codepoint);

            float finalX = advancedX + static_cast<float>(glyph->bearing.x);
            float finalY = y + (textureFont.ascender + textureFont.descender - static_cast<float>(glyph->bearing.y));
            float w = static_cast<float>(glyph->size.w);
            float h = static_cast<float>(glyph->size.h);

            auto uv = glyph->uv;

            renderer.FillRect(finalX, finalY, w, h, texture, uv);

            advancedX += glyph->advance.x;
        }
    }

    Texture* texture;
}

There are few functions that are not shown because they do not change any rendering stuff, such as AdvanceIndexAndGetCodepoint. There is a problem with getStringWidth/Height() or getGlyphWidth/Height(), or even my freetype initialization is wrong.

It is pointless pointless to show you guys whole Renderer2D class, it works fine.

float getGlyphWidth(uint32 codepoint)
{
    float result = 0;
    auto glyph = textureFont.getGlyph(codepoint);
    float kerning = textureFont.getKerning(glyph, codepoint);

    result += kerning;
    result += glyph->advance.x;
    return result;
}

float Font::getStringWidth(const std::string& string)
{
    float result = 0.0f;
    for(uint i = 0; i < string.size(); /* nothing */) {
        const char* c = &string[i];
        uint32 codepoint = AdvanceIndexAndGetCodepoint(i, c);
        result += getGlyphWidth(codepoint);
    }

    return result;
}

float getGlyphHeight(uint32 codepoint)
{
    float min = 0.0f;
    float max = 0.0f;
    auto glyph = textureFont.getGlyph(codepoint);
    float height = static_cast<float>(glyph->size.h);
    float offset = static_cast<float>(glyph->bearing.y) - height;
    if(offset < min)
        min = offset;
    if(height > max)
        max = height;
    return  abs(min) + abs(max);
}

//returns the highest glyph, so all glyphs can fit to the border
float getStringHeight(const std::string& string)
{
    float result = 0.0f;
    for(uint i = 0; i < string.size(); /* nothing */) {
        const char* c = &string[i];
        uint32 codepoint = AdvanceIndexAndGetCodepoint(i, c);
        result = Max(result, getGlyphHeight(codepoint));
    }

    return result;
}

and with this code the border can be rendered

void main()
{
    // create window, context

    Font font;

    Renderer2D renderer;

    bool quit = false;
    while(!quit) {
        float textX = 100.0f;
        float textY = 100.0f;
        float textW = getStringWidth("Jorg");
        float textH = getStringHeight("Jorg");

                                                      //ABGR
        renderer.FillRect(textX, textY, textW, textH, 0xff000000);
        renderer.DrawString("Jorg", textX, textY, 0xffffffff, font);
    }
}

result: enter image description here

as you can see J is above black rectangle and width is also not exactly correct.

what I expect: enter image description here

I obtained this effect by doing random numer calculations

for string position I subtracted 7 from X and added 6 to Y

and from border size I subtracted 26 from width and 8 from height.

Of course it cannot be done this way, because for different font sizes those numers will change.

How can I calculate width and height of any string properly?

c++
text
2d
rendering
freetype
asked on Stack Overflow Jun 7, 2020 by Shout

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0