更新
下面的内容择优选用。
问题
使用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的字符映射表。