最近,我的 Fedora Linux 安装在操作系统 UI 和浏览器中显示表情符号时遇到了问题。这个问题促使我对字体配置项目进行了一些调查,但为了测试我的配置和字体,我需要从所有 Unicode 版本生成表情符号,这最终导致我编写了一个 Golang“脚本”来打印所有表情符号和一些表情符号有关其内部结构的信息。
在这次旅行中,我深入研究了表情符号的内部结构、它们的二进制表示形式,以及 Unicode 标准关于表情符号做出的一些奇怪/可爱的决定。
但首先,让我们快速退后一步,总结一些术语表。
我们可以将编码描述为语言的字母和该字母的二进制表示之间的“映射”或“翻译”。例如,传统的 ASCII 编码将字母 a 映射为十六进制 0x61(二进制 0b01100001)。编码示例包括 Microsoft (Windows 125x) 或 ISO (ISO/IEC 8859) 8 位代码页。
在这些固定的 8 位代码页中,使用的最小信息“量”是 8 位(1 字节),这意味着它们可以包含 256 个不同的字母/字符。通过重用 256 个二进制代码创建不同的代码页来支持多种语言。因此,在一个文本文件上写入这 3 个字节 [0xD0、0xE5、0xF2],使用希腊语 ISO 8859-7 读作“Πες”,或使用西方 ISO 8859-7 读作“Ðåò”(相同字节,解释不同)基于代码页)。
在某些时候,随着技术的进步,拥有许多不同的代码页并不能很好地扩展。因此,我们需要能够适合所有语言(以及更多语言)并且跨系统统一的东西。
[快进,留下大量历史和标准,到现在]
Unicode 标准旨在支持世界上所有可以数字化的书写系统。因此,使用上面的示例,在 Unicode 标准中,希腊字母“Π”的代码为 0x03A0,而拉丁大写字母 eth“Д的代码为 0x00D0,并且不再冲突。 Unicode 标准有多个版本,在撰写本文时,最新版本是 16.0(规范)。
但是等一下,这个“代码点”是什么?
在 Unicode 标准中,每个“字母”、控制字符、表情符号和每个定义的项目通常都有一个唯一的二进制值,称为“代码点”。该标准定义了所有代码点,每个代码点都包含纯代码/二进制信息。每个代码点的十六进制格式通常以 U 前缀编写。例如,希腊小写字母 Omega (ω) 代码点是 U 03C9。
那么我们实际上由谁来编码这些代码点?
将代码点编码为字节的第一部分是编码格式。根据标准:
编码形式指定如何将 Unicode 字符的每个整数(代码点)表示为一个或多个代码单元的序列。
编码形式使用术语“代码单元”来指代用于表示特定编码内的 Unicode 代码点的最小数据单元。
Unicode 标准定义了三种不同的编码形式:
这意味着根据所使用的编码形式,单个代码点或一系列代码点可能会进行不同的编码。
负责 Unicode 中实际二进制序列化的层称为编码方案,负责所有低级细节(例如字节序)。 Unicode 规范表 2-4:
|Encoding Scheme| Endian Order | BOM Allowed? | | ------------- | ----------------------------| ------------ | | UTF-8 | N/A | yes | | UTF-16 | Big-endian or little-endian | yes | | UTF-16BE | Big-endian | no | | UTF-16LE | Little-endian | no | | UTF-32 | Big-endian or little-endian | yes | | UTF-32BE | Big-endian | no | | UTF-32LE | Little-endian | no |
注意:几乎所有现代编程语言、操作系统和文件系统都使用 Unicode(及其编码方案之一)作为其本机编码。 Java和.NET使用UTF-16,而Golang使用UTF-8作为内部字符串编码(这意味着当我们在内存中创建任何字符串时,它都会以上述编码形式以Unicode编码)
Unicode 标准还定义了表情符号(很多)的代码点,并且(在与版本号混淆之后),表情符号“标准”的版本与 Unicode 标准并行发展。在撰写本文时,我们有表情符号“16.0”和 Unicode 标准“16.0”。
示例:
⛄ 没有雪的雪人 (U 26C4)
?笑脸笑眼三颗心 (U 1F970)
Unicode 定义了可以遵循表情符号基本代码点的修饰符,例如变体和肤色(我们不会探讨变体部分)。
我们有六种肤色修饰符(遵循 Fitzpatrick 等级),称为 EMOJI MODIFIER FITZPATRICK TYPE-X(其中 x 为 1 到 6),它们会影响所有人类表情符号。
浅肤色 (Fitzpatrick Type-1-2) (U 1F3FB)
中浅肤色 (Fitzpatrick Type-3) (U 1F3FC)
中等肤色 (Fitzpatrick Type-4) (U 1F3FD)
中深肤色 (Fitzpatrick Type-5) (U 1F3FE)
深色肤色 (Fitzpatrick Type-6) (U 1F3FF)
那么,例如,像所有人类表情符号一样,婴儿表情符号? (U 1F476),当不添加皮肤修饰剂时,会呈现中性黄色。相反,当肤色修改器跟随时,它会相应地发生变化。
? U 1F476
?? U 1F476 U 1F3FF
?? U 1F476 U 1F3FE
?? U 1F476 U 1F3FD
?? U 1F476 U 1F3FC
?? U 1F476 U 1F3FB
表情符号/Unicode 标准最奇怪但可爱的决定是,一些表情符号是通过使用零宽度连接器将其他表情符号连接在一起而定义的,而不需要独立的代码点。
因此,例如,当我们组合时:
白旗?️ (U 1F3F3 U FE0F)
零宽度连接器 (U 200D)
彩虹 ? (U 1F308)
它显示为彩虹旗?️? (U 1F3F3 U FE0F U 200D U 1F308)
或者, ?? ? => ???
甚至,?? ❤️? ?? => ??❤️???
这就像将表情符号挤压在一起,然后,噗?,一个新的表情符号出现了。多可爱啊?
我想创建一个包含所有表情符号的 Markdown 表,而 Unicode 表情符号序列表是其真相来源。
https://unicode.org/Public/emoji/16.0/emoji-sequences.txt
https://unicode.org/Public/emoji/16.0/emoji-zwj-sequences.txt
因此,我创建了一个 Golang 解析器(此处),它获取并解析这些序列文件,在序列文件中描述范围时生成每个表情符号,并打印一个包含每个表情符号的一些内部信息的 Markdown 表(例如 零件(如果已加入),或基础 肤色等)。
您可以在这里找到降价表。
该表的最后一列的格式为
str := "⌚" len([]rune(str)) // 1 len([]byte(str)) // 3
正如我们所讨论的,Golang 内部字符串编码是 UTF-8,这意味着,例如,对于时钟表情符号⌚,字节长度为 3(因为 UTF-8 生成 3 个字节来“写入”此代码点),码位长度为1。
Golang 符文 == Unicode 代码点
但是在连接表情符号的情况下 - 即使它“显示”为一个 - 我们有许多代码点(符文)甚至更多的字节。
str := "??❤️???" len([]rune(str)) // 10 len([]byte(str)) // 35
原因是:
??❤️??? : ?? ZWJ ❤️ ZWJ ? ZWJ ?? ?? : 1F469 1F3FC // ? skin tone modifier [2 code points] ZWJ : 200D // [1 code points] * 3 ❤️ : 2764 FE0F // ❤ VS16 for emoji-style [2 code points] ? : 1F48B // [1 code point] ?? : 1F468 1F3FE // ? skin tone modifier [2 code points]
?
值得一提的是,我们如何看待表情符号取决于我们的系统字体以及该字体支持哪些版本的表情符号。
我不知道字体渲染的确切内部原理以及它如何正确渲染连接的字体。也许这将是一个未来的职位。
到那时,干杯吗?
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3