【问题标题】:How to handle rendering utf-8 characters with size >= 2B properly?如何正确处理大小 >= 2B 的渲染 utf-8 字符?
【发布时间】:2019-10-01 12:54:13
【问题描述】:

我想渲染 utf-8 大小 >= 2 字节的字符。我已经完成了一切。不过,有一个问题。当一个字符被绘制时,它后面还有一个东西image

为了获得字形数据,我使用 freetype。这是最小的实现,实际代码包含字距调整、SDF 等。

我认为需要解释的是地图集。方法 "TextureAtlas::PackTexture(data, w, h)" 打包纹理数据并返回位置、原点 - 左上角 - 在图集 w 和 h 范围内。因此,第一个字符的原点 = [0, 0],下一个宽度为 50 的字符的原点位于 [50, 0]。简短地说。

    enum
    {
        DPI = 72,
        HIGHRES = 64
    };

    struct Glyph
    {
        uint32 codepoint = -1;
        uint32 width = 0; 
        uint32 height = 0;

        Vector2<int> bearing = 0;
        Vector2<float> advance = 0.0f;
        float s0, t0, s1, t1;
    };

    class TextureFont
    {
    public:
        TextureFont() = default;

        bool Initialize();
        void LoadFromFile(const std::string& filePath, float fontSize);

        Glyph* getGlyph(const char8_t* codepoint);
        Glyph* FindGlyph(const char8_t* codepoint);

        uint32 LoadGlyph(const char8_t* codepoint);

        int InitFreeType(float size);

        char* filename;

        vector<Glyph> glyphs;
        TextureAtlas atlas;

        FT_Library library;
        FT_Face face;

        float fontSize = 0.0f;
        float ascender = 0.0f;
        float descender = 0.0f;
        float height = 0.0f;
    };  
int CharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end)
    {
        unsigned int c = (unsigned int)-1;
        const unsigned char* str = (const unsigned char*)in_text;
        if (!(*str & 0x80)) {
            c = (unsigned int)(*str++);
            *out_char = c;
            return 1;
        }
        if ((*str & 0xe0) == 0xc0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 2) return 1;
            if (*str < 0xc2) return 2;
            c = (unsigned int)((*str++ & 0x1f) << 6);
            if ((*str & 0xc0) != 0x80) return 2;
            c += (*str++ & 0x3f);
            *out_char = c;
            return 2;
        }
        if ((*str & 0xf0) == 0xe0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 3) return 1;
            if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) return 3;
            if (*str == 0xed && str[1] > 0x9f) return 3;
            c = (unsigned int)((*str++ & 0x0f) << 12);
            if ((*str & 0xc0) != 0x80) return 3;
            c += (unsigned int)((*str++ & 0x3f) << 6);
            if ((*str & 0xc0) != 0x80) return 3;
            c += (*str++ & 0x3f);
            *out_char = c;
            return 3;
        }
        if ((*str & 0xf8) == 0xf0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 4) return 1;
            if (*str > 0xf4) return 4;
            if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) return 4;
            if (*str == 0xf4 && str[1] > 0x8f) return 4; 
            c = (unsigned int)((*str++ & 0x07) << 18);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (unsigned int)((*str++ & 0x3f) << 12);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (unsigned int)((*str++ & 0x3f) << 6);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (*str++ & 0x3f);
            if ((c & 0xFFFFF800) == 0xD800) return 4;
            *out_char = c;
            return 4;
        }
        *out_char = 0;
        return 0;
    }

    bool TextureFont::Initialize()
    {
        FT_Size_Metrics metrics;

        if (!InitFreeType(fontSize * 100.0f)) {
            return false;
        }

        metrics = face->size->metrics;
        ascender = (metrics.ascender >> 6) / 100.0f;
        descender = (metrics.descender >> 6) / 100.0f;
        height = (metrics.height >> 6) / 100.0f;

        FT_Done_Face(face);
        FT_Done_FreeType(library);

        return true;
    }

    int TextureFont::InitFreeType(float size)
    {
        FT_Matrix matrix = {
            static_cast<int>((1.0 / HIGHRES) * 0x10000L),
            static_cast<int>((0.0)           * 0x10000L),
            static_cast<int>((0.0)           * 0x10000L),
            static_cast<int>((1.0)           * 0x10000L)};
        FT_Error error;
        error = FT_Init_FreeType(&library);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not Init FreeType!\n");
            FT_Done_FreeType(library);
            return 0;
        }

        error = FT_New_Face(library, filename, 0, &face);

        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not create a new face!\n");
            FT_Done_FreeType(library);
            return 0;
        }

        error = FT_Select_Charmap(face, FT_ENCODING_UNICODE);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not select charmap!\n");
            FT_Done_Face(face);
            return 0;
        }

        error = FT_Set_Char_Size(face, static_cast<ulong>(size * HIGHRES), 0, DPI * HIGHRES, DPI);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not set char size!\n");
            FT_Done_Face(face);
            return 0;
        }

        FT_Set_Transform(face, &matrix, NULL);

        return 1;
    }

    void TextureFont::LoadFromFile(const std::string& filePath, float fontSize)
    {
        atlas.Create(512, 1);
        std::fill(atlas.buffer.begin(), atlas.buffer.end(), 0);
        this->fontSize = fontSize;  
        this->filename = strdup(filePath.c_str());

        Initialize();
    }

    Glyph* TextureFont::getGlyph(const char8_t* codepoint)
    {
        if (Glyph* glyph = FindGlyph(codepoint)) {
            return glyph;
        }

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

        return nullptr;
    }

    Glyph* TextureFont::FindGlyph(const char8_t* codepoint)
    {
        Glyph* glyph = nullptr;
        uint32 ucodepoint;
        CharFromUtf8(&ucodepoint, (char*)codepoint, NULL);
        for (uint32 i = 0; i < glyphs.size(); ++i) {
            glyph = &glyphs[i];
            if (glyph->codepoint == ucodepoint) {
                return glyph;
            }
        }

        return nullptr;
    }

    uint32 TextureFont::LoadGlyph(const char8_t* codepoint)
    {
        FT_Error error = NULL;
        FT_Glyph ftGlyph = nullptr;
        FT_GlyphSlot slot = nullptr;
        FT_Bitmap bitmap;

        if (!InitFreeType(fontSize)) {
            return 0;
        }

        if (FindGlyph(codepoint)) {
            FT_Done_Face(face);
            FT_Done_FreeType(library);
            return 1;
        }

        unsigned int cp;
        CharFromUtf8(&cp, (char*)codepoint, NULL);
        uint32 glyphIndex = FT_Get_Char_Index(face, cp);

        int flag = 0;
        flag |= FT_LOAD_RENDER;
        flag |= FT_LOAD_FORCE_AUTOHINT;

        error = FT_Load_Glyph(face, glyphIndex, flag);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not load the glyph (line {})!\n", __LINE__);
            FT_Done_Face(face);
            FT_Done_FreeType(library);
            return 0;
        }

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

        uint32 srcWidth = bitmap.width / atlas.bytesPerPixel;
        uint32 srcHeight = bitmap.rows;

        uint32 tgtWidth = srcWidth;
        uint32 tgtHeight = srcHeight;

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

        uchar* destPointer = buffer.get();
        uchar* srcPointer = bitmap.buffer;

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

        auto origin = atlas.PackTexture(buffer.get(), { tgtWidth, tgtHeight });

        float x = origin.x;
        float y = origin.y;

        Glyph current;
        current.codepoint = cp;
        current.width = tgtWidth;
        current.height = tgtHeight;
        current.bearing.x = glyphLeft;
        current.bearing.y = glyphTop;
        current.s0 = x / (float)atlas.textureSize.w;
        current.t0 = y / (float)atlas.textureSize.h;
        current.s1 = (x + tgtWidth) / (float)atlas.textureSize.w;
        current.t1 = (y + tgtHeight) / (float)atlas.textureSize.h;

        current.advance.x = slot->advance.x / (float)HIGHRES;
        current.advance.y = slot->advance.y / (float)HIGHRES;

        glyphs.push_back(current);

        FT_Done_Glyph(ftGlyph);
        FT_Done_Face(face);
        FT_Done_FreeType(library);

        return 1;
    } 

要渲染一个字符串(在这种情况下是单个字符),我会遍历字符串大小,获取字形,更新图集并设置渲染数据。

文本是一个简单的四边形,带有适当的 uvs 纹理。 我认为没有必要解释AddVertexData里面的内容,因为它不会造成问题。

void DrawString(const std::u8string& string, float x, float y)
    {
        for (const auto& c : string) {
            auto glyph = textureFont.getGlyph(&c);

            auto& t = *(Texture2D*)texture.get();
            t.UpdateData(textureFont.atlas.buffer.data());

            float x0 = x + static_cast<float>(glyph->bearing.x);
            float y0 = y + (textureFont.ascender + textureFont.descender - static_cast<float>(glyph->bearing.y));
            float x1 = x0 + static_cast<float>(glyph->width);
            float y1 = y0 + static_cast<float>(glyph->height);

            float u0 = glyph->s0;
            float v0 = glyph->t0;
            float u1 = glyph->s1;
            float v1 = glyph->t1;

            //            position                uv                      color
            AddVertexData(Vector2<float>(x0, y0), Vector2<float>(u0, v0), 0xff0000ff);
            AddVertexData(Vector2<float>(x0, y1), Vector2<float>(u0, v1), 0xff0000ff);
            AddVertexData(Vector2<float>(x1, y1), Vector2<float>(u1, v1), 0xff0000ff);
            AddVertexData(Vector2<float>(x1, y0), Vector2<float>(u1, v0), 0xff0000ff);

            // indices for DrawElements() call
            // 0, 1, 2, 2, 3, 0
            AddRectElements();

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

ę 是 utf-8 size == 2,所以循环运行了两次,但是只渲染了 1 个字符并且不知道第二个字符(因为没有任何第二个字符),所以它渲染了空的四边形。

如何摆脱跟随我要渲染的角色的四边形?

【问题讨论】:

  • 使用const auto&amp; c : string 访问几个字符令人惊讶。
  • 注意:有两个问题:Unicode codepoints在UTF-8中需要1到4个字节,但是对于渲染一个字形/字符 您可能需要 8 个代码点(或更多)(修饰符),并且更多字符可以通过字体合并为连字(因此真正显示的字形)。 [草书字体使用许多连字(“根据草书的定义”)]。这是最佳实践的“参考”:unicode.org/reports/tr29/tr29-35.html

标签: c++ text utf-8 freetype freetype2


【解决方案1】:

在您的 DrawString 函数中,您有循环

for (const auto& c : string)

该循环将 逐字节 遍历字符串。所以如果字符串中包含两个字节的"ę"字符,那么第一次迭代会得到第一个字节,第二次迭代会得到第二个字节。

您不能在此处使用基于范围的for 循环,因为您需要跳过字符串中的字节。使用基于迭代器的循环或基于索引的循环。

例如

for (size_t i = 0; i < string.size(); /* nothing */) {
    // Here you need to get the number of bytes for the current character
    // Then you should increment the index by that amount
    i += byte_count_for_current_character;

    // ... rest of code
}

【讨论】:

    【解决方案2】:

    您的问题在DrawStringfor (const auto&amp; c : string)

    您应该跳过用于编码先前字形的额外字符,即与 0b10...... 匹配的字符:

    for (const auto& c : string) {
        if ((c & 0b1100'0000) == 0b1000'0000) {
            continue;
        }
    // ...
    }
    

    或前进到最后一个字形读取的字节数。

    【讨论】:

      【解决方案3】:

      对实际 UTF-8 解码函数 CharFromUtf8 的两次调用都忽略了它的返回值,即字符串指针应该前进的字节数。您应该有一个指针,而不是 for (const auto&amp; c : string),您应该在每次迭代时按返回值前进。

      此外,由于您将在该循环中使用 CharFromUtf8 函数,因此您将知道 Unicode 代码点和要前进的字节数。然后,您可以重构您的 TextureFont 以将 unsigned int(即代码点)作为参数,而不是让它进行 UTF-8 解码。这将是更好的关注点分离。

      【讨论】:

        【解决方案4】:

        其他答案已经确定了直接将基于范围的 for 循环与 std::u8string 变量一起使用的问题。假设基于代码点的枚举是您想要的(这可能不是因为,通常,正确的字形选择取决于周围的代码点;您可能想要迭代扩展的字形簇),您可以使用像 text_view 这样的库为代码点的迭代提供基于范围的支持。那个循环伤口看起来像:

        auto tv = make_text_view<utf8_encoding>(string);
        for (const auto& cp : tv) {
          ...
        }
        

        【讨论】:

        • 有趣,我不太明白correct glyph selection depends on surrounding code points; you probably want to iterate over extended grapheme clusterssurrounding codepoints 是什么意思,grapheme clusters 是什么意思?
        • 扩展字形簇 (EGC) 是一个 Unicode 术语,用于描述以特定方式相互关联的代码点序列。
        • 考虑这两种编码字母“á”的方式:U+00E1(带锐音的拉丁小写字母A)vs U+0061(拉丁小写字母A)+ U+0301(组合锐音) .第一个使用单个代码点表示字符;第二个使用两个代码点。两种形式都编码单个 EGC,并且两种形式都应使用单个字形呈现。请参阅 Unicode 规范化。
        • 这与表情符号特别相关。例如,家庭表情符号由选择家庭成员的代码点序列组成,这些代码点通过 U+200D(零宽度连接器)连接在一起。
        猜你喜欢
        • 1970-01-01
        • 2010-09-17
        • 2015-12-28
        • 2015-05-15
        • 1970-01-01
        • 2012-08-01
        • 2019-07-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多