更新

Unicode官方关于编码转换的项目:ICU

ICU项目下载地址

可以在ICU中使用的字符映射表下载地址

下面的内容择优选用。

问题

使用FreeType库获取字体的字形数据用于在OpenGL程序中渲染文字时,遇到了编码问题,即无法根据包含中文的字符串(UTF-8编码,窄字符串)正确的得到中文字体数据。

查询得知FreeType库函数FT_Load_Char的参数应当是Unicode编码值,而正常字符无法表示Unicode编码值,只能使用宽字符。然而尝试了网络上的许多方法,只有调用Windows库函数的版本可以使用。

代码实现

几经斟酌,决定自己写个utf-8与Unicode之间的转换函数:

#ifndef SOLVENCODE_H_INCLUDED
#define SOLVENCODE_H_INCLUDED

#include <iostream>

namespace buluguy{

/**
 * 由 MinGW 源码知:
 *     Windows 系统中宽字符被定义为 2 个字节( unsigned short )
 *         :MinGW/x86_64-w64-mingw32/include/stddef.h:l313~l319
 *         解释是为了 MS(Windows系统)的运行时兼容性
 *     Linux 系统中宽字符被定义为 4 个字节( int )
 *         :MinGW/lib/gcc/x86_64-w64-mingw32/8.1.0/include/stddef.h:l325~l330
 * 
 * 源代码具体位置可能随着版本的更新而不同 *
 *
 * RFC3269 规定了utf8字节数量的极限值依赖于 unicode
 * 旧版本的 Unicode 决定了 utf8 最多有3个字节
 * 因此 2 个字节的 Windows 宽字符满足需要
 * 然而 unicode6.1 规定了 unicode 编码可以具有更高的值
 * 因此 utf8 最大长度将达到 4 个字节
 * 所以 Windows 的宽字符将无法满足 unicode6.1 以及更新的标准
 */


/**
 * 下面的是自定义的 4 字节的宽字符和与之对应的宽字符串
 * 相当于将标准字符串中的 char 替换为 unsigned int
 * 这两个类仅用于对于编码的存储而不能使用更高级的功能
 * 诸如文件流,缓冲流等
 * 不过需要的话也可以很轻松的配置,只需要对着GCC源码依样画葫芦即可
 * lwchar_t : long wchar_t
 * lwstring : lwchar_t string
 */

typedef unsigned int lwchar_t;
typedef std::basic_string<lwchar_t> lwstring;

/**
 * 事实上 WINAPI 中已经定义了编码转换函数 MultiByteToWideChar/WideCharToMultiByte
 * 并且这两个函数支持多种编码与宽字符(Unicode编码)的互相转换
 * 但是 WINAPI 只能在Windows上起作用,所以有了以下 UTF-8 编码与 Unicode 编码的互转函数
 * 其中宽字符分别使用 wchar_t 和自定义的 lwchar_t 来实现
 * 不用 C++ 标准库 locale 和 codecvt 进行转换的原因是它们在 Windows 上并不好使
 */

char utf8StringToWstring(const std::string& str, std::wstring& wstr);
char utf8WstringToString(const std::wstring& wstr, std::string& str);

char utf8StringToLwstring(const std::string& str, buluguy::lwstring& lwstr);
char utf8LwstringToString(const buluguy::lwstring& lwstr, std::string& str);



}



namespace buluguy
{

/**
 * [utf8StringToWstring UTF-8窄字符串 to Unicode宽字符串]
 * @param  str  [需要转换的窄字符串]
 * @param  wstr [宽字符串容器]
 * @return      [转换状态{0:正常}{1:内存分配失败}{2:码流断裂}]
 */
char utf8StringToWstring(const std::string& str, std::wstring& wstr)
{
    unsigned int wslength = 0, slength = str.size();
    wchar_t *cache = ( wchar_t* )malloc( (slength + 1) * sizeof(wchar_t) );
    if( !cache ) { return 1; }

    char chrT;
    short int byteNum;
    wchar_t wchrT;
    // 用于剔除utf-8编码首字节中表示字节数的部分
    static char rejector[] = {0x3f, 0x1f, 0x0f};

    for (unsigned int i = 0; i < slength; ++i)
    {
        if( str[i] & 0x80 )
        {
            // 取反并右移5位
            // 进一步的安全:取与 0x07(0b00000111) 的位AND
            chrT = (~str[i]) >> 5;
            byteNum = 3;
            // 操作后byteNum是该utf8字符总字节数
            for(; chrT; byteNum--) { chrT >>= 1; }

            // 不完整的编码
            if( (i + byteNum--) > slength ) { return 2; }

            // 首先进行无符号强制转换,然后复制操作会进行隐式类型转换
            wchrT = ( wchar_t )( unsigned )( str[i] & rejector[byteNum] );

            // 计算余下的字节
            for(; byteNum; byteNum--)
            {
                wchrT = ( wchrT << 6 ) | ( wchar_t )( unsigned )( str[++i] & 0x3f );
            }

            *(cache + wslength) = wchrT;
        }

        // ASCII code, directly
        else
        {
            *(cache + wslength) = ( wchar_t )( unsigned )str[i];
        }
        wslength++;
    }
    *( cache + wslength ) = '\0';
    // 字符串赋值将在 '\0' 处自动截断
    wstr = cache;
    free(cache);
    return 0;
}

// unicode basic definition field
// unicode基本定义范围:0~FFFF
/**
 * [utf8WstringToString Unicode宽字符串 to UTF-8窄字符串]
 * @param  wstr [需要转换的宽字符串]
 * @param  str  [窄字符串容器]
 * @return      [转换状态{0:正常}{1:内存分配失败}{2:码流断裂}]
 */
char utf8WstringToString(const std::wstring& wstr, std::string& str)
{
    unsigned int wslength = wstr.size(), slength = 0;
    char* cache = ( char* )malloc( (4 * wslength + 1) * sizeof(char) );
    if( !cache ) { return 1; }

    for ( unsigned int i = 0; i < wslength; ++i )
    {
        if ( wstr[i] & 0xff80 )
        {
            // 3byte
            if( wstr[i] & 0xf800)
            {
                *(cache + slength++) = ((char)(wstr[i] >> 12) & 0x0f) | 0xe0;
                *(cache + slength++) = ((char)(wstr[i] >> 6) & 0x3f) | 0x80;
                *(cache + slength)   = ((char)(wstr[i]) & 0x3f) | 0x80;
            }
            // 2byte
            else
            {
                *(cache + slength++) = ((char)(wstr[i] >> 6) & 0x1f) | 0xc0;
                *(cache + slength)   = ((char)(wstr[i]) & 0x3f) | 0x80;
            }
        }
        else
        {
            *(cache + slength) = (char)wstr[i];
        }
        slength++;
    }
    *( cache + slength ) = '\0';
    // 字符串赋值将在 '\0' 处自动截断
    str = cache;
    free(cache);
    return 0;
}


/**
 * [utf8StringToLwstring UTF-8窄字符串 to Unicode宽字符串]
 * @param  str   [需要转换的窄字符串]
 * @param  lwstr [宽字符串容器]
 * @return       [转换状态{0:正常}{1:内存分配失败}{2:码流断裂}]
 */
char utf8StringToLwstring(const std::string& str, buluguy::lwstring& lwstr)
{
    unsigned int wslength = 0, slength = str.size();
    lwchar_t *cache = ( lwchar_t* )malloc( (slength + 1) * sizeof(lwchar_t) );
    if( !cache ) { return 1; }

    char chrT;
    short int byteNum;
    lwchar_t wchrT;
    // 用于剔除utf-8编码首字节中表示字节数的部分
    static char rejector[] = {0x3f, 0x1f, 0x0f, 0x07};

    for (unsigned int i = 0; i < slength; ++i)
    {
        if( str[i] & 0x80 )
        {
            // 取反并右移4位
            // 进一步的安全:取与 0x0f(0b00001111) 的位AND
            chrT = (~str[i]) >> 4;
            byteNum = 4;
            // 操作后byteNum是该utf8字符总字节数
            for(; chrT; byteNum--) { chrT >>= 1; }

            // 不完整的编码
            if( (i + byteNum--) > slength ) { return 2; }

            // 首先进行无符号强制转换,然后复制操作会进行隐式类型转换
            wchrT = ( lwchar_t )( unsigned )( str[i] & rejector[byteNum] );

            // 计算余下的字节
            for(; byteNum; byteNum--) { wchrT = ( wchrT << 6 ) | ( lwchar_t )( unsigned )( str[++i] & 0x3f ); }

            *(cache + wslength) = wchrT;
        }

        else
        {
            *(cache + wslength) = ( lwchar_t )( unsigned )str[i];
        }
        wslength++;
    }
    *( cache + wslength ) = '\0';
    // 字符串赋值将在 '\0' 处自动截断
    lwstr = cache;
    free(cache);
    return 0;
}

/**
 * [utf8LwstringToString Unicode宽字符串 to UTF-8窄字符串]
 * @param  lwstr [需要转换的宽字符串]
 * @param  str   [窄字符串容器]
 * @return       [转换状态{0:正常}{1:内存分配失败}{2:码流断裂}]
 */
char utf8LwstringToString(const buluguy::lwstring& lwstr, std::string& str)
{
    unsigned int wslength = lwstr.size(), slength = 0;
    char* cache = ( char* )malloc( (4 * wslength + 1) * sizeof(char) );
    if( !cache ) { return 1; }

    for ( unsigned int i = 0; i < wslength; ++i )
    {
        if ( lwstr[i] & 0xffffff80 )
        {
            // 4byte
            if ( lwstr[i] & 0x001f0000 )
            {
                *(cache + slength++) = ((char)(lwstr[i] >> 18) & 0x07) | 0xf0;
                *(cache + slength++) = ((char)(lwstr[i] >> 12) & 0x3f) | 0x80;
                *(cache + slength++) = ((char)(lwstr[i] >> 6) & 0x3f) | 0x80;
                *(cache + slength)   = ((char)(lwstr[i]) & 0x3f) | 0x80;
            }
            // 3byte
            else if ( lwstr[i] & 0x0000f800)
            {
                *(cache + slength++) = ((char)(lwstr[i] >> 12) & 0x0f) | 0xe0;
                *(cache + slength++) = ((char)(lwstr[i] >> 6) & 0x3f) | 0x80;
                *(cache + slength)   = ((char)(lwstr[i]) & 0x3f) | 0x80;
            }
            // 2byte
            else
            {
                *(cache + slength++) = ((char)(lwstr[i] >> 6) & 0x1f) | 0xc0;
                *(cache + slength)   = ((char)(lwstr[i]) & 0x3f) | 0x80;
            }
        }
        else
        {
            *(cache + slength) = (char)lwstr[i];
        }
        slength++;
    }
    *( cache + slength ) = '\0';
    // 字符串赋值将在 '\0' 处自动截断
    str = cache;
    free(cache);
    return 0;
}




} // namespace buluguy


#endif // SOLVENCODE_H_INCLUDED

同理,可以写出utf-16和utf-32与Unicode之间的转换函数(因为它们都是为了传输和存储的需要w而对Unicode的重编码),不再赘述。

但是GBK等地区编码是独立的编码,与Unicode之间没有重编码或兼容的关系(0~127除外),所以它们之间的转换只能通过映射得到,也就是说必须事先知道每一个GBK码对应的Unicode码。我想没有这种需求的话还是不必搞了。

对于编码的理解

Unicode码就相当于一个数字,每个数字对应一个唯一的字符,Unicode码的范围足以表示世界上所有语言需要的字符,只是这个数字有可能太大,无法用一个字节(byte)表示。为了标准,Unicode码统一用宽字符表示。但是如果每个字符都用Unicode码表示的话,将会浪费大量的存储空间并可能造成错误。

如:u+0003 的二进制(Windows上宽字符为2byte)为 00000000 00000011,前一个字节的值为0,这是一个控制字符,可能会对字符串的处理造成严重影响。并且这个值完全可以省略掉。

于是utf-8等编码就通过不同的规则将这个数字转化为连续的几个数字(具体见百科),相当于加密。只不过加密后的字符将不会出现意料外的0值,并且对大部分是英文的文件的存储有一定的优化(全部是汉字的话反而不如直接存储Unicode节省空间,Windows下)。

utf类编码都是如此。

地区编码如GB2312、GBK、BIG5等是独立的专门为自己所在地区语言制定的编码标准。它们对字符的排序有自己的规则,这些规则对输入法友好,如GBK中汉字的顺序与拼音和笔画相关。但这也导致它们与Unicode编码不能兼容。如果希望写它们与Unicode之间的转换函数的话,必须要有GBK与Unicode的字符映射表。