起因

最近在开发的时候有一个需求:因为空间原因比较小等原因某些位置展示不下,需要进行截断展示,发现国旗图标(🇸🇪)按照这个格式解析不出来,具体排查发现这个国旗图标对应的UTF-8二进制格式是:

111110000
210011111
310000111
410111000
511110000
610011111
710000111
810101010

发现并不满足标准的UTF-8格式,而是两个UTF-8字符编码拼接起来的。

初步排查

通常来讲提到的UTF-8都是基础的基础格式:https://www.rfc-editor.org/rfc/rfc3629#page-5

根据查阅了相关资料及文章后发现国旗图标是按照一定规则由2个字符组合出来的,大致代码如下:

1OFFSET = 0x1F1A5
2def get_country_flag(code):
3    return chr(OFFSET + ord(str.upper(code[0]))) + chr(OFFSET + ord(str.upper(code[1])))

其参数为国家代码,如CN,US等,返回值为对应国家国旗的emoji图标,就是说国旗编码是由两个Unicode字符组合而成的,那么解决办法就是如果当前字符是在 0x1F1E60x1F1FF之间,判断下一个字符是否也在这个范围之间,如果在的话,那么就认为这两个共同组成一个显示的文字。

新的问题

处理完国旗问题以后,同事又发了几个新的字符:👩🏻👩🏼👩🏽👩🏾👩🏿,拆解这几个字符会发现和国旗一样是由两个Unicode编码组成的,查阅相关资料后发现emoji实际上是有"皮肤"的概念的,即正常图标+颜色修饰,修饰字符的范围为 0x1F3FB-0x1F3FF

紧接着同事又发了下面这个东西:

1
2
3Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞
4
5

这个东西实际上是对 ZALGO!的这五个字符的修饰,就是每个字母后面都跟着大量的用于修饰前面字符的编码。

查阅相关资料后发现这些组合字符的范围分为几段:0x0300 - 0x036F0x1DC0 - 0x1DFF0x20D0 - 0x20FF0xFE20 - 0xFE2F,这些编码一定是依赖于前一个字符存在而存在的。

后来找到了这样两个网址:

https://www.unicode.org/Public/emoji/14.0/emoji-sequences.txt

https://www.unicode.org/Public/emoji/14.0/emoji-zwj-sequences.txt

这里面是Unicode14.0的基础emoji和组合emoji图标,在这里面又发现了几个之前没注意到的编码:0x200D0xFE0F

0x200D是用于将前面一个编码和后面一个编码“粘起来”的编码,0x200D前后共通组成一个用于显示的字符。

0xFE0F是用于修饰前面一个编码的(根据相关文档介绍, 0xFE00-0xFE0F均是,但是常见的只有 0xFE0E0xFE0F),具体用处还没太搞清楚。

此外,还有除了常规两个编码拼接起来的国旗以外,还有三个Unicode 5.0加入的由七个编码组成的特殊国旗:🏴󠁧󠁢󠁥󠁮󠁧󠁿、🏴󠁧󠁢󠁳󠁣󠁴󠁿、🏴󠁧󠁢󠁷󠁬󠁳󠁿,这三个还需要特殊处理。

在查找问题的过程中还发现了这样一个网站:https://www.compart.com/en/unicode/,可以很方便的查询单个Unicode编码的用处。

总结

目前很多程序使用的都是UTF-8或者UTF-16编码,需要先拿到每个字符的Unicode编号,这个在大部分语言中都是内置提供的,不需要特殊处理,直接使用即可。

  1. 从前往后读取的过程中,需要判断当前字符是否是国旗编码所在范围内,如果在的话需要判断是否能与下一个(普通国旗)或多个字符(🏴󠁧󠁢󠁥󠁮󠁧󠁿🏴󠁧󠁢󠁳󠁣󠁴󠁿🏴󠁧󠁢󠁷󠁬󠁳󠁿)组成国旗,如果能的话需要和前一个合并处理。
  2. 判断下一个字符是否是修饰编码或者组合编码(如 0xFE0F0x0300等),如果是的话那么也需要合并到前一个处理。
  3. 上述判断完成后还需要判断下一个字符是否是 0x200D,如果是的话,那么需要再向下读取前至少1个,具体数量重复上述1-2步骤进行判断。

综合以上信息可以解决目前遇到的大部分的Unicode字符截断展示的问题,但这里面有两个问题:

  1. 目前接触到的类型还比较少,可能还有尚没有接触到的编码,遇到了还需要进行补充。
  2. Unicode版本是一直在更新的,如果新版本发布了可能还需要跟进相关的适配。