Emoji的奥秘

2021-04-12 ⏳8.6分钟(3.5千字)

最近读到一篇写 emoji 的文章,叫 Emoji under the hood,写得非常通透。我联系了作者 Nikita,拿到了翻译许可。周末翻译了一把,分享给大家。

过去几周,我一直忙于给 Skija 项目添加 emoji 支持。自从发明字母🅰️之后,Emoji可以说是人类通信史上最大的发明了。我很乐意跟大家分享 emoji 的核心奥秘。

注意:有些 emoji 可能没法在你的设备正常显示。

Unicode 简介

大家可能已经知道了,在计算机内部,所有文字都用数字表示。一个字符对应一个数字(一组完整的对应规则称为一种编码)。最常用的编码就是所谓的Unicode。Unicode有两个变种:UTF-8和UTF-16。

Unicode 一共分配了 2²¹(约200万)个字符,人们称之为码点(codepoint)。程序员们对不住了,21不能被8整除🤷。这200多万的码点中实际只使用了大约15万字符。

这15万字符包含了地球🌍上用过的所有文符。包括:好多已经没用人使用的语言的文字,好多奇奇怪怪的的字母,像是 𝔣𝔲𝔫𝔫𝔶 𝕝𝕖𝕥𝕥𝕖𝕣𝕤,sɹǝʇʇǝl uʍop-ǝpᴉsdn,㎓(GHz的单字符版本), ⤘(右向双箭头配箭尾和双竖线符号),ꙮ(七眼怪兽),甚至还有一只鸭子𐦖:

Unicode 鸭子符号

另外,大家可以查看Unicode的埃及象形文字区(U+13000–U+1342F),这里面还有更奇怪的字符:

埃及象形文字

基本 emoji

最简单的 emoji 就是 Unicode 表中的一个字符。多数简单的 emoji 都被规到 U+1F300–1F6FF 和 U+1F900–1FAFF 两个区域。

基本 emoji 符号

所以 emoji 跟其他字母没什么两样:你可以在文本框输入、复制、粘贴 emoji。emoji 也可以在纯文本文档里面正常展示,你也可以在微博中加入 emoji。如果你输入字母“A”,计算机看到的是 U+0041。如果你输入的是“🌵”,计算机看到的是 U+1F335。没有太大区别。

Emoji 字体

但为什么 emoji 会显示成图片呢?或者位图字体。一般字体都是黑白色的矢量图形,没啥意思。你也可以给字符创建 jpg位图字体。

Emoji 字体

操作系统一般都会内置一种 emoji 字体。macOS/iOS 内置的是 Apple Color Emoji 字体。Windows 内置的是 Segoe UI Emoji 字体。Android 内置的是 Noto Color Emoji 字体。

我看了一下,Apple Color Emoji是一种 160x160 的点阵字体,Noto是一种 128x128 的点阵字体,而 Segoe 是一种矢量彩色字体🆒。

这也是为啥同一个 emoji 在不同的设备上长得不一样。这个跟同一个字符在不同字体下显示效果不同是一个意思。除此之外,很多应用也会自带 emoji 字体。WhatsApp、Twitter 和 Facebook 带用自己的 emoji 字体。

不同 emoji 字体的显示效果

字体回退

现在讨论一下字体的渲染。我们不可能用Apple Color Emoji或者Segoe UI Emoji字体显示文本(除非你很小,不识字)。所以,如何在文本(假设是Helvetica字体)中显示 emoji 呢?

答案是字体回退。这也是西里尔字母在Clubhouse或Medium上很难看的罪魁祸首。

西里尔字母显示效果

当你输入字符的时候,比如说输入U+1F419,计算机首先在当前字体中查找。假设当前字体是 San Francisco。San Francisco 没有 U+1F419 对应的字形(glyph),所以操作系统开始查询其他安装的字体,看有什么字体包含 U+1F419 的字形。

只有 Apple Color Emoji 字体含有 U+1F419,所以系统使用它渲染 U+1F419(剩下的字母还是使用当前字体渲染)。最后,你看到了🐙。所以,不管你选用哪种字体,emoji 的显示效果都是一样的:

不同字体下 emoji 显示效果相同

变体选择符-16

有些 emoji 表情并不在 Emoji 编码区。实际上,支持象形文字的字体最早可以追溯到1993年。我们可以看一下杂项符号区(Miscellaneous Symbols)U+2600-26FF和装饰符号(Dingbats)区U+2700-27FF:

杂项符号和装饰符号

这些符号跟我们用的其他字母没什么两样:它们都占一个码点,黑白色,而且很多字体都支持显示这些符号。例如,下图是在我电脑中所有支持黑色剪刀✂︎ (U+2702)字体的显示效果:

剪刀符号

你猜怎么着,Apple Color Emoji 字体在设计之初就支持显示剪刀符号 U+2702,显示效果如下:

剪刀 Emoji

现在问题来了。如果✂︎和✂️共用一个码点(U+2702),而且除 Apple Color Emoji 字体之外其他高优先字体也支持显示这个码点,操作系统怎么知道什么时候显示✂︎什么时候显示✂️呢?

这就需要用到 变体选择符(U+FE0F。变体选择符一共有十六种,彩色 emoji 用的是 VS-16,后面不再说明。译者注。)。它的作用就是告诉系统当前字符需用 emoji 字体渲染。

简单而优雅。毕竟像☠︎ 和☠️的含义相同,只是显示上略有区别,没有必要分配新的码点。

字素集(Grapheme clusters)

现在我们得解决另一个问题:有此 emoji 需要使用两个码点。也就说,我们需要一种方法来指定字符的边界。

这就需要用到字素集了。字素集是一组连续的码点,但被一个字形展示和识别。

字素集并非 emoji 专享,普通字符在展示的时候也会用到。比如“Ü”是一个字素集,是由两个码点组成:大写字母U(U+0055),后面跟一个连音符号(U+0308)。

在用程序处理字素集可不容易。如果想截取字符串的前10个字符,你不能简单的使用substring(0,10),因为这可能会截断 emoji 字符(可能会让你痛苦万分,所以不要这样做。译者:这一句的意思是说可能产生意想不到的问题。)。

反转字符串也非常困难。U+263A U+FE0F 有意义,反转之后 U+FE0F U+263A却没有意义。

反转 Emoji

最后,你也不能简单地通过 .length 读取字符串的长度。其实也可以,只是结果可能出乎你的预料。如果你是程序员,可以在浏览器的控制台查看”🤦🏼‍♂️“.length(结果是7,译者注)。

程序员小贴士:如果需要处理文本,请务必使用支持字素集的库。如果是 C、C++和JVM程序员,可以用ICU库,Swift处带字素集支持。如果是其他语言,只能靠自己研究了。

字素集意识训练月,有人要参加吗?禁止截断字素集。我是在跟谁开玩笑吗?

另外,我有提过Ų̷̡̡̨̫͍̟̯̣͎͓̘̱̖̱̣͈͍̫͖̮̫̹̟̣͉̦̬̬͈͈͔͙͕̩̬̐̏̌̉́̾͑̒͌͊͗́̾̈̈́̆̅̉͌̋̇͆̚̚̚͠ͅ这个字符吗?这是一个字素群,它的长度是65,是不分割的整体。睡觉🛌 :)

肤色修饰符

大多数人形相关的 Emoji 都是黄色的。人们在2015年为 emoji 引入肤色支持。当时没有为每种肤色的 emoji 组合分配新的码点,而是引入了五个新码点:🏻🏼🏽🏾🏿U+1F3FB-U+1F3FF。

肤色修饰符一般不单独使用,而是追加到现有的 emoji 后面。它们一起组成一个合字(ligature):👋(U+1F44B挥手)跟 🏽(U+1F3FD灰色)组合就变成了👋🏽。

👋🏽没有自己的码点(它的编码是U+1F44B U+1F3FD)但有自己的显示效果。通过这五个修饰符,~280 个人形 emoji 就产生了 1680 种肤色变种。这是不肤色的舞蹈演员:🕺🕺🏻🕺🏼🕺🏽🕺🏾🕺🏿。

零宽度连接符

假设你的朋友发了一张苹果照片,是她在自家花园种的。你该如何回复呢?你可能会回复👩🌾。

但是,如果你在这两个 emoji 之间加一个 U+200D 字符,它们就会变成👩‍🌾(女性农民)。

U+200D就是所谓的零宽度连接符(ZERO-WIDTH JOINER,简写为ZWJ)。它跟肤色修饰符有点类似,但是这次我们可以将两个单独的 emoji 合成新的 emoji。并非所有的 emoji 组合会产生新 emoji。但多数都可以,只是有些组合方式比较怪异。

下面是一些例子:

我还发现一处奇葩的地方:头发的颜色是用ZWJ实现的,但皮肤的颜色却不用ZWJ。求真相。

可惜,有些 emoji 不是通过 ZWJ 组全 emoji 实现的。我猜它们是没赶上好时候:

怎么输入ZWT呢?你不需要直接输入。你可以直接复制这个字符串“‍”。注意,它很特别,看不见,摸不着,但它就在那儿。

ZWJ的另一个用武之地是构造家庭和人际关系相关的 emoji,下面是不完整的示例列表:

旗帜

Unicode 标准包含国旗标志。但 Windows 平台因为某些原因不支持显示。如果你是用 Windows 平台的浏览器阅读本文,我只能说抱歉了。

国旗也没有独立的码点。而是由双字符连字(ligature)来表示。

这不是真正的字母,而是一组定义在U+1F1E6到U+1F1FF范围的地区标识符号。这些符号专门用于构造国旗标识。

如果你随机组合两个字符会怎样呢?什么也不会发生:🇽🇾(除非你的文本编辑功能有问题)。

这是完整的字母表:🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭 🇮 🇯 🇰 🇱 🇲 🇳 🇴 🇵 🇶 🇷 🇸 🇹 🇺 🇻 🇼 🇽 🇾 🇿。如果你想自己试试,可以随意复制并组合它们。一共有258种合法的组合。你能都找出来吗?

双字符连字有一个很有意思的副作用:’‘.join(reversed(’🇺🇦‘)) =>’🇦🇺’

标签序列

双字母连字确实很酷,但要不要更酷一些?来一个32字母连字怎么样?这个时候就需要标签序列了。

标签序列由三部分构成:开头是一个普通的 emoji,之后是一串拉丁字母(U+E0020..E007E),最后是结束符(U+E0020..E007E CANCEL TAG)。

当前只有英格兰、苏格兰和威尔士的旗帜使用标签序列:

键位符

键位符很简单,但为了内容完整,我们也提一下。键位符j使用另一套序列规则。

规则是这样的:数字、星号和井号,加 U+FE0F 变成 emoji,再加上 U+20E3 变成带方框的键位符。

* + FE0F + 20E3 = *️⃣

总共只有12个:

#️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣

Unicode 更新

Unicode每年都会更新。每次更新的主要内容就是 emoji。比如,2020年3月发布的Unicode 13就添加了55个 emoji。

在我写作本文的时候,最新的 macOS(11.2.3)和 iOS(14.4.1)都还不支持 Unicode 13 的 emoji,比如:

😮‍💨, ❤️‍🔥, 🧔‍♀ or 👨🏻‍❤️‍💋‍👨🏼

后来的读者可以比较一下,这是我在2021年3月看到的效果:

Unicode 13 的 emoji 显示效果

感谢ZWJ,我大概可以猜到这些 emoji 的显示效果。

结论

总结一下,一共有七种 emoji 造字法:

  1. 单个码点 🧛 U+1F9DB
  2. 单个码点+变体符号 ☹︎(U+2639) + U+FE0F = ☹️
  3. 皮肤符号 🤵(U+1F935) + 🏽(U+1F3FD) = 🤵🏽
  4. 连接符号 👨 + ZWJ + 🏭 = 👨‍🏭
  5. 旗帜符号 🇦 + 🇱 = 🇦🇱
  6. 标签序列 🏴 + gbsct + U+E007F = 🏴󠁧󠁢󠁳󠁣󠁴󠁿
  7. 键位序列 * + U+FE0F + U+20E3 = *️⃣

前四种方法也可以组合使用来构造非常复杂的 emoji:

U+1F6B5 🚵 个人山地骑行
+ U+1F3FB 浅色皮肤
+ U+200D ZWJ
+ U+2640 ♀️女性标志
+ U+FE0F 变体标志
= 🚵🏻‍♀️ 浅色皮肤的女性山地骑行

如果你是程序员,牢记使用ICU库处理以下操作:

谷歌关键词是“Grapheme Cluster”。注意了,它适用于 emoji、西方语言的变音字符,以及印度和朝鲜的文字。

我就知道这些了。希望理解 emoji 原理能对你的工作带来帮助……哈哈,开玩笑了。希望 emoji 能给你带来欢乐,这就够了✌️