Unicode字符集和字形

早上老板报了一个生僻字,左边卒,右边瓦:
卒瓦
这字在unicode.org的Unihan中念“cèi”,在汉典中念“suì”
可是这字在输入法里敲不出来,在Windows、macOS、iOS和Android下的浏览器里,显示各不相同,这是为什么呢?

Windows下:
cei@Windows
只有Windows下显示是正常的。

macOS下:
cei@macOS
iOS下:
cei@iOS
Android下:
cei@Android

原因是这样的:这个字的Unicode编码是0x24B62,落在了Unicode CJK扩展B的集合中。不同操作系统对于不同编码范围的字符集的字形支持度是不一样的,即使在Windows下,不同的版本也是不一样的。上面的截图是在Win7下截到的,Win7针对扩展B自带了一个字库,而WinXP是没有的,因此如果是在WinXP下面,就应该也看不到这个字。

关于Unicode的概念

Unicode的目的就写在Unicode官网首页:让世界上使用任何语言的人们都能使用计算机。其实是让计算机能支持世界上任何一种语言。

语言是由字或字符组成的,拉丁语系下大部分语言都是通过有限的字母拼合而成,所以计算机只要支持这些有限的字母即可。而像汉字这种象形文字则包含了几万个字,如果计算机要支持世界上所有的语言体系,就必须给每个语言下的每个字符编个号,大家都认这个编号。过去不同国家会制定自己的编码标准,也就是给自己语言体系中的字符编号,不同标准之间可能会冲突。比如同一个号码序列,在欧洲表示“abc”,在中国就表示“你我他”,数据在相互交换之前就必须指明了我的编码采用的什么标准,到了对端再依照不同的标准做转换。这相当于给计算机限定了国界,可是互联网是无国界的。这不只是跟互联网文化相冲突,也给跨区域使用带来了现实上的不便。为了解决这个问题,Unicode组织说:“我来指定一套规范,能覆盖所有语言,给所有语言下的所有字符规定一套编码,大家都遵守我的编码规范,这个问题自然就解决了!”

Unicode码位的组织

到2016年的Unicode 9.0为止,Unicode共使用了从0到0x10FFFF的编码,每一个数字代表一个字符。为了便于理解,它把这些码位按照前16位和后16位做了逻辑分区,前16位每个编码叫做一个Plane(平面),于是每个平面下就包含了0xFFFF个码位。例如前面说的“卒瓦”的编码是0x24B62,就可以在Plane2下的0x4B62处找到。

Basic Multilingual Plane(BMP)

把Plane0下包含的编码称作多语言基本面 Basic Multilingual Plane,简称BMP。Unicode的编码方案在制定时考虑到了各语言的书写、使用方便,尽量把常用的往前排,符合哈夫曼编码原则;尽量把一个区域的字符编在相邻的位置。BMP下包含了世界各语言下比较常用的字符,所以才被称为基本面~在这个BMP中,包含了27,973个汉字。
Plane

Supplementary Multilingual Plane(SMP)

把Plane1下包含的编码称作多语言补充面 Supplementary Multilingual Plane,简称SMP。位于这个面字符或者不太常用,或者是为了特殊含义专门造的,或者是表意符号,或者是BMP字符的不常用的变种。

Supplementary Ideographic Plane(SIP)

把Plane2下包含的编码称作表意补充面 Supplementary Ideographic Plane,简称SIP。这个区域主要是为中日韩象形文字开辟的一块扩展空间,用于存放不适合放到BMP中的生僻字。这个区域除了有少量常用CJK字符外(如广东话里的常用字),绝大部分都是非常罕见的生僻字。

Supplementary Special-purpose Plane(SSP)

把Plane14下包含的编码称作特殊用途补充面 Supplementary Special-purpose Plane,简称SSP。这个区域的字符基本上都是更不常用的格式控制符。

Private Use Planes

还剩下Plane15、Plane16两个面,用于私有用途。这两个区域包含了131,068个字符,用于补充BMP中6400个代理去码位,具体细节在后面讲到UTF-16编码时再解释。

汉字在Unicode中的分布

在Unicode中,中日韩的汉字集中在几个区域,但是它们之间是混在一起的。这几个区域分别是:

块名称 位置 汉字个数 描述
CJK Unified Ideographs 4E00–9FFF 20,991 常用字
CJK Unified Ideographs Extension A 3400–4DBF 6591 生僻字
CJK Unified Ideographs Extension B 20000–2A6DF 42,719 古汉字遗留的生僻字
CJK Unified Ideographs Extension C 2A700–2B73F 4,159 古汉字遗留的生僻字
CJK Unified Ideographs Extension D 2B740–2B81F 223 现在还在用,但使用较少
CJK Unified Ideographs Extension E 2B820–2CEAF 5,775 古汉字遗留的生僻字
CJK Compatibility Ideographs F900–FAFF 511 重复的变体字
CJK Compatibility Ideographs Supplement 2F800–2FA1F 543 变体字
总计 81,432

关于变体字,有些看起来是一样的,比如F902 車 8ECA 車F907 龜 F908 龜 9F9C 龜,这是因为在某些编码中同时存在了同一个字的若干写法(就是字形相似,且编码不同),为了兼容,才给了这么个区域,Unicode 组织不建议再使用这些字符。

关于编码的实现方案

过去一直把Unicode标准与实现方案搞不清楚,Unicode标准给每个字符规定一个数字编号,由于这个数字较长,在计算机里具体怎么存储和传输,也需要定义一个规范了。比如同样都是说2016年10月8日,有的书写规范写成2016/10/08,有的就写成16/10/08,还有16/08/10的……

既然编码方案都统一了,为什么实现方案不统一成一个呢?这是因为实现方案是根据用途不同而设计的,在不同的使用场景下有各自的优势。比如UTF-8是变长实现方案,Unicode中常用的字符集中在前面,UTF-8采用较少的字节就能表示,不常用的字符集中在后面,UTF-8需要使用较长的字节来表示。这种实现方案的好处就是节省字节数,缺点就是比较复杂,因为它是变长的。

定长的方案也有:UTF-32,跟Unicode标准完全一致,直观明了,但是缺点就是浪费字节数。

还有UTF-16,长期以来我以为Windows默认采用的UNCIODE编码就是UTF-16,其实正宗的UTF-16也是类似UTF-8的变长编码方式,以后有空再详细介绍。而Windows采用的只是它的阉割版,用定长的2字节表示BMP中的字符,至于其他平面大于等于0x10000的字符,就不能支持了。

几个平台下的字体支持

编码规范收录了,编码实现方案支持了,还需要有字体支持才能显示出来。拿python+fonttools写段脚本,可以跑出来每个平台下的字体文件各自包含什么区域的多少汉字。我把代码放在这里了。结果如下:

系统 字体 CJK UI ExtA ExtB ExtC ExtD ExtE CJK CI CJK CIS
WinXP 微软雅黑 20,909 6,582 8 0 0 0 21 0
WinXP 宋体 20,902 52 0 0 0 0 21 0
Win7 微软雅黑 20,909 6,582 8 0 0 0 21 0
Win7 宋体 20,910 6,582 0 0 0 0 21 0
Android7.1 Droid Sans Fall back 20,902 6,582 0 0 0 0 302 0
macOS 苹方 20,929 6,582 1,088 0 0 0 95 11
iOS9 苹方 20,929 6,582 1,088 0 0 0 95 11

貌似各自在ExtB上区别不大,反而苹方还有一千多个字,Windows下宋体字库完全没有对ExtB上的支持,为什么Win7可以正常显示这个字呢?

Windows的字体链接对生僻字的支持

字体链接

在Windows下有种技术叫做字体链接。可以给指定的字体注册一串链接字体,当该字体不支持某个字形的时候,系统会依次尝试链接串上的其它字体,直到找出能支持的字体把他显示出来,或者把一串链接字体都遍历完后依然找不到,才不显示。
在注册表位置HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink,找到某一字体,比如宋体SimSun,发现该项的值为:

1
2
3
4
5
MICROSS.TTF,108,122
MICROSS.TTF
MINGLIU.TTC,PMingLiU
MSMINCHO.TTC,MS PMincho
BATANG.TTC,Batang

可是我把这些字体遍历之后发现他们也没有ExtB的字体。

SurrogateFallback

还有一个注册表项专门记录了ExtB的字体位置:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\LanguagePack\SurrogateFallbackPlane2下的值为SimSun-ExtB。然后去到C:\Windows\Fonts下找到SimSun-ExtB,发现其对应的字体文件为simsunb.ttf。跑一下这个文件结果如下:

字体 CJK UI ExtA ExtB ExtC ExtD ExtE CJK CI CJK CIS
simsunb 0 0 42,711 0 0 0 0 0