背景

事情缘于同事调试代码中的一段未知bug,java的jni传输数据到unity层,发现在unity层中的emoji表情不能显示,处于对技术的好奇我研究了一下,刚开始以为是一个简单问题,随着深入和对资料的查阅发现这个是java中对于utf8字符处理方式给不熟悉的人挖了个坑,具体可以看:stack overflow的提问,java为了解决2个问题,使用了变种的utf-8:

第一,空字符(null character,U+0000)使用双字节的0xc0 0x80,而不是单字节的0x00。这保证了在已编码字符串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志字符串结尾的。当已编码字符串放到这样的语言中处理,一个嵌入的空字符将把字符串一刀两断;

第二,个不同点是基本多文种平面之外字符的编码的方法。在标准UTF-8中,这些字符使用4字节形式编码,而在改正的UTF-8中,这些字符和UTF-16一样首先表示为代理对(surrogate pairs),然后再像CESU-8那样按照代理对分别编码。这样改正的原因更是微妙:Java中的字符为16位长,因此一些Unicode字符需要两个Java字符来表示。语言的这个性质盖过了Unicode的增补平面的要求。尽管如此,为了要保持良好的向后兼容、要改变也不容易了。这个改正的编码系统保证了一个已编码字符串可以一次编为一个UTF-16码,而不是一次一个Unicode码点。不幸的是,这也意味着UTF-8中需要4字节的字符在变种UTF-8中变成需要6字节;

经过和同事的讨论和思考,其实有很多解决方案可以规避这个问题,但是我在想是否将变种的utf-8转换为标准的utf-8或者只针对emoji的utf-8处理为原始的utf-8,这样就将真正解决这个问题,于是开始上手解决。

使用php将变种utf-8编码变化为原始utf-8编码

从java获取的变种utf-8对应的数据是:0xED, 0xA0, 0xBE, 0xED, 0xB4, 0x93,这是6个字节,其中对于为什么变种utf-8会变成这种可以参考:这篇博客,在网上找了一圈,发现一些碎片代码,于是组合起来用php将emoji表情的变种utf-8数据还原utf-8,代码如下:

function VerifyValidUtf8($s)
{
    $s = preg_replace_callback('@(?:\xED[\xA0-\xBF][\x80-\xBF]){2}@', function ($m)
    {
        $bytes = unpack("C*", $m[0]); # always 6 bytes
        # create UCS-4 character from CESU-8 encoded surrogate pair in $bytes

        # 3 bytes CESU-8 to UNICODE high surrogate:
        $high = (($bytes[1] & 0x0F) << 12) + (($bytes[2] & 0x3F) << 6) + ($bytes[3] & 0x3F);
        # 3 bytes CESU-8 to UNICODE low surrogate:
        $low = (($bytes[4] & 0x0F) << 12) + (($bytes[5] & 0x3F) << 6) + ($bytes[6] & 0x3F);

        $codepoint = ($high & 0x03FF) << 10 | ($low & 0x03FF);
        $codepoint += 0x10000;
        return mb_convert_encoding(pack("N", $codepoint), "UTF-8", "UTF-32");
    }, $s);

    # replace unmatched surrogate pairs with U+FFFD REPLACEMENT CHARACTER
    return preg_replace('@\xED[\xA0-\xBF][\x80-\xBF]@', "\xEF\xBF\xBD", $s);
}

$str1 = chr(0xED).chr(0xA0).chr(0xBE).chr(0xED).chr(0xB4).chr(0x93);
echo "modified utf-8 : $str1 \n";
echo "orgin utf-8 : ".VerifyValidUtf8($str1)." \n";

打印的还原的utf8的数据:0xF0,0x9F,0xA4,0x93
在mac上输出的结果:

用c++重写

既然php可以实现变种utf-8编码还原为原始utf-8编码,那么其他语言肯定能实现,但是google了一圈没有发现c++类似代码,于是决定重写:

size_t Utf32_to_Utf8(uint32_t src, unsigned char* des)
{
   if (src == 0) return 0;

   static const char PREFIX[] = { 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
   static const uint32_t CODE_UP[] =
   {
      0x80,           // U+00000000 - U+0000007F
      0x800,          // U+00000080 - U+000007FF
      0x10000,        // U+00000800 - U+0000FFFF
      0x200000,       // U+00010000 - U+001FFFFF
      0x4000000,      // U+00200000 - U+03FFFFFF
      0x80000000      // U+04000000 - U+7FFFFFFF
   };

   size_t i, len = sizeof(CODE_UP) / sizeof(uint32_t);
   for(i = 0; i < len; ++i) {
      if (src < CODE_UP[i]) break;
   }

   if (i == len) return 0; // the src is invalid

   len = i + 1;
   if (des)
   {
      for(; i > 0; --i)
      {
         des[i] = static_cast<char>((src & 0x3F) | 0x80);
         src >>= 6;
      }
      des[0] = static_cast<char>(src | PREFIX[len - 1]);
   }

   return len;
}
int VerifyValidUtf8(unsigned char *in, uint32_t in_len, unsigned char *out, uint32_t &out_len)
{
   #define EMOJI_SIZE 6
   #define TO_UINT32(d) (uint32_t)(*(d))

   if (in_len <= 0 || *in == '\0') 
   {
      return in_len == 0 ? 0 : -1;
   }
   size_t step = 0;
   if (in_len >= EMOJI_SIZE) {
      uint32_t offset = 0;
      if (
            (
               (TO_UINT32(in+offset) & 0xED) == 0xED &&
               (TO_UINT32(in+offset+1) >= 0xA0 && TO_UINT32(in+offset+1) <= 0xBF) && 
               (TO_UINT32(in+offset+2) >= 0xA0 && TO_UINT32(in+offset+2) <= 0xBF)
            )
            &&
            (
               (TO_UINT32(in+offset+3) & 0xED) == 0xED &&
               (TO_UINT32(in+offset+4) >= 0x80 && TO_UINT32(in+offset+5) <= 0xBF) &&
               (TO_UINT32(in+offset+4) >= 0x80 && TO_UINT32(in+offset+5) <= 0xBF)
            )
         )
      {
         uint32_t high = ((TO_UINT32(in+offset) & 0x0F) << 12) + 
            ((TO_UINT32(in+offset+1) & 0x3F) << 6) + 
            (TO_UINT32(in+offset+2) & 0x3F);
         uint32_t low = ((TO_UINT32(in+offset+3) & 0x0F) << 12) + 
            ((TO_UINT32(in+offset+4) & 0x3F) << 6) + 
            (TO_UINT32(in+offset+5) & 0x3F);

         uint32_t codepoint = (high & 0x03FF) << 10 | (low & 0x03FF);
         codepoint += 0x10000;
         step = Utf32_to_Utf8(codepoint, out);
         in += EMOJI_SIZE;
         in_len -= EMOJI_SIZE;
         out += step;
         out_len += step;
      } 
   } 
   // 判断是否有跳跃,如果没有则自动+1
   if (step == 0)
   {
      *out++ = *in++;
      in_len--;
      out_len++;
   }

   return VerifyValidUtf8(in, in_len, out, out_len);
}
// 使用方法
int main()
{
   unsigned char ch1[] = {0xAB, 0xCD, '\n', 0xED, 0xA0, 0xBE, 0xED, 0xB4, 0x93, 0x00};
   unsigned char ch2[128] = {0x00};

   uint32_t len = 0; // 一定要初始化为0
   VerifyValidUtf8(ch1, sizeof(ch1)/sizeof(ch1[0]), ch2, len);

   fprintf(stdout, "modified utf-8 : %s\n", ch1);
   fprintf(stdout, "orgin utf-8  : %s, len:%d\n", ch2, len);

   return 0;
}

打印的还原的utf8的数据:0xF0,0x9F,0xA4,0x93
在mac上输出的结果:

上述代码大家可以借鉴,不过未经过全面的变种utf8编码的测试,可能存在bug,如果发现,请留言给我!

参考引用

(1)字符编码笔记:ASCII,Unicode和UTF-8