最近,我的 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 defines modifiers that could follow an emoji's base code point, such as variation and skin tone (we will not explore the variation part).
We have six skin tone modifiers (following the Fitzpatrick scale) called EMOJI MODIFIER FITZPATRICK TYPE-X (where x is 1 to 6), and they affect all human emojis.
Light Skin Tone (Fitzpatrick Type-1-2) (U+1F3FB)
Medium-Light Skin Tone (Fitzpatrick Type-3) (U+1F3FC)
Medium Skin Tone (Fitzpatrick Type-4) (U+1F3FD)
Medium-Dark Skin Tone (Fitzpatrick Type-5) (U+1F3FE)
Dark Skin Tone (Fitzpatrick Type-6) (U+1F3FF)
So, for example, like all human emojis, the baby emoji ? (U+1F476), when not followed by a skin modifier, appears in a neutral yellow color. In contrast, when a skin color modifier follows it, it changes accordingly.
? U+1F476
?? U+1F476 U+1F3FF
?? U+1F476 U+1F3FE
?? U+1F476 U+1F3FD
?? U+1F476 U+1F3FC
?? U+1F476 U+1F3FB
The most strange but cute decision of the Emoji/Unicode Standard is that some emojis have been defined by joining others together using the Zero Width Joiner without a standalone code point.
So, for example, when we combine:
White Flag ?️ (U+1F3F3 U+FE0F) +
Zero Width Joiner (U+200D) +
Rainbow ? (U+1F308)
It appears as Rainbow Flag ?️? (U+1F3F3 U+FE0F U+200D U+1F308)
Or, ?? + ? => ???
Or even, ?? + ❤️ + ? + ?? => ??❤️???
It's like squeezing emojis together, and then, poof ?, a new emoji appears. How cute is that?
I wanted to create a Markdown table with all emojis, and the Unicode emoji sequence tables are the source of truth for that.
https://unicode.org/Public/emoji/16.0/emoji-sequences.txt
https://unicode.org/Public/emoji/16.0/emoji-zwj-sequences.txt
So I created a Golang parser (here) that fetches and parses those sequence files, generates each emoji when a range is described in the sequence file, and prints a markdown table with some internal information for each one (like the parts in case it joined, or the base + skin tone, etc.).
You can find the markdown table here.
The last column of this table is in this format
str := "⌚" len([]rune(str)) // 1 len([]byte(str)) // 3
As we discussed, Golang internal string encoding is UTF-8, which means that, for example, for clock emoji ⌚ the byte length is 3 (because the UTF-8 produces 3 bytes to "write" this code point), and the code point length is 1.
Golang rune == Unicode Code Point
But in the case of joined emoji -even if it "appears" as one- we have many code points (runes) and even more bytes.
str := "??❤️???" len([]rune(str)) // 10 len([]byte(str)) // 35
And the reason is that:
??❤️??? : ?? + 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]
?
It is worth mentioning that how we see emojis depends on our system font and which versions of emoji this font supports.
I don't know the exact internals of font rendering and how it can render the joined fonts correctly. Perhaps it will be a future post.
Til then, cheers ?
以上是Unicode、表情符號和一點 Golang的詳細內容。更多資訊請關注PHP中文網其他相關文章!