帖子内容
Unicode 和文字渲染是个大坑。从一个小问题说起:我最近调研了一下中文文案中引号的使用问题,因为我知道有的人喜欢用弯引号(“”),有的人喜欢用直角引号(「」)。表面上看双方的理由主要是“国家标准推荐”和「计算机上更醒目好看,保证占一个汉字大小」,但我觉得背后真正的坑在于,中文没有自己的弯引号 Unicode 码位。 我们都知道中文逗号(,)和西文逗号(,)是两个不同的 Unicode 字符,中文括号(())和西文括号(())也是不同的字符。然而并没有不同的字符可以分别表示中文弯引号和西文弯引号,它们是相同的字符。这导致渲染效果完全取决于所用的字体,如果弯引号字符被中文字体渲染,一般会按中文排版习惯占一个汉字大小。如果被西文字体渲染,一般会非常窄,旁边需要按西文习惯加上空格才好看。 如果有两组不同的 Unicode 字符,一组会渲染出中文风格的弯引号,一组会渲染出西文风格的弯引号,那我相信很多人会主张中文文案排版中应该正确使用前者,就像对待逗号、括号等标点符号一样。但在现状下,很多人认为这个弯引号字符没法使用,转而使用新的,更适合渲染效果中文排版风格的直角引号字符,是可以理解的。 Unicode 为什么不单独收录中文弯引号?其实是有的,但它长这样:〝〞。为什么长这样?我也不清楚,反正只有“”和〝〞,自己选吧。 我并不认为这个问题完全是 Unicode 的错误导致的。Unicode 中有很多字符是多种语言共用的,需要知道语言才能正确渲染,弯引号并不是特例。实际上,几乎所有我们打出来的汉字都是这样的。中日韩统一表意文字必须配合相应语言的字体,才能正确渲染。如果一段简体中文文字被日文字体或繁体中文字体渲染,看起来就会不正确。 所以说,“拿到一串 Unicode 字符,不需要其他信息,就可以正确渲染”这种想法是完全错误的,但这恰恰是今天很多程序、网站、用户拥有的想法,或者即使不认同这个想法,也被迫要接受的。要想正确地渲染一串字符,首先至少要知道其中每个部分是哪种语言(它不一定只包含一种语言)。但我们键入 Unicode 字符串时并没有键入这种信息,该怎么办呢?今天主流的做法是,简体中文用户只安装简体中文字体(或者配置简体中文字体为中日韩统一表意文字中第一优先尝试的字体),简体中文的程序和网站对每个字符先尝试用西文字体渲染,如果失败,再尝试用简体中文字体渲染。这虽然是错误的做法,但能解决 99% 的问题。弯引号就这样总是被渲染成了西文字体,即使出现在中文上下文中。 把字体选择顺序反过来如何呢?先尝试简体中文字体,如果失败再尝试西文字体。这样一来,中文中的弯引号没问题了,但等你看一段西文文字时,就会发现里面的弯引号都渲染成了中文的样子,完全错误。 更聪明的字体选择算法能解决这个问题吗?我们确实可以看上下文来推测字符所属的语言。但 1. 如果两侧分别是中文和西文,怎么知道哪边是更优先?2. 看再多字符也没法知道一串汉字是简体中文还是繁体中文。3. 就算知道了上下文是简体中文,突然出现一个繁体中文姓名,应该用繁体中文渲染,也是完全有可能的。这种自动判断语言和字体的思路是行不通的。 综上所述,我真正想说明的问题是,目前没有任何方法,可以接受用户提交一个 Unicode 字符串,没有其他信息,然后正确地渲染。但这是很多地方的需求(比如 Telegram 和微信的消息正文、游戏中的用户昵称、各种浏览器面对没有认真标注语言的网页内容),必须选一个哪怕不正确的算法来做这件事。所以这是一个大坑,接受“这件事没法做对”的现实,选择适合你正在开发的项目的算法,接受它带来的问题,躺平,不要掉进坑里。 不过如果在处理的不是这个问题(渲染用户提交的字符串)的话,正确的做法就存在了。如果你要渲染自己生成的文案,那么正确的做法是首先为文案中的每个字符选择相应的 Unicode 字符,其次通过指定语言或直接指定字体来为每个字符分别选择你满意的字体(HTML 中用 lang 属性),最后如果仍然有合字或者字间距相关的问题字体处理不好,还可以考虑手工干预排版算法。听起来有点过于复杂?这正是印刷排版领域很多年以来的流程,也是唯一能保证最终效果令你满意的做法。