> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/10-an-overview-of-technologies-supporting-the-use-of-colour-emoji-fonts-in-latex.md).

# 在 LaTeX 中使用彩色 emoji 字体所依赖技术的概览

## 引言

本文概述了各种 [背景主题](#which-topics-do-we-cover) 与在 LaTeX 中使用 OpenType 彩色字体排版彩色 emoji 相关。我们尝试提供广泛的资料，以满足不同兴趣和专业水平的读者。为使文章保持可控，我们对某些主题的覆盖省略了许多技术细节，但希望这些内容足以为你探索在 LaTeX 中排版彩色 emoji 提供指引。

**更新（2023 年 7 月）**：本文首次发表于 2021 年 8 月，并于 2023 年 7 月修订，以更新关于 [使用基于 SVG 的 OpenType 彩色字体与 LuaHBTeX](#using-svg-based-opentype-color-fonts-with-luahbtex).

### 我们涵盖哪些主题？

本文涵盖以下一般主题：

* Unicode：将 emoji 编码为字符，并在文本处理和排版应用中规定其预期行为的标准。
* OpenType 彩色字体：一种专门字体，可为 LaTeX 文档中显示的 emoji 字符提供彩色呈现。
* 文本整形：介绍复杂文字语言和 emoji 排版中的一个关键组件。
* HarfBuzz：LuaHBTeX 的一个组件，它支持高级多语言排版，并允许使用 OpenType 彩色字体在 LaTeX 中排版 emoji。
* 不同的 TeX 引擎：探讨它们对 OpenType 彩色字体的支持，并选择要使用的 TeX 引擎。
* LuaHBTeX 的 HarfBuzz API：介绍背后的“魔法” [文本整形](#the-concept-of-text-shaping) 在 LuaHBTeX 中。

### 彩色 emoji 的三种排版方式

可以使用 LaTeX 通过三种主要方法排版彩色 emoji：

1. 使用 TikZ、MetaPost 或 Asymptote 等标准 LaTeX 图形工具绘制 emoji。
2. 使用存储在外部文件中的预先准备好的 emoji 图形插入 emoji。
3. 将 emoji 视为 Unicode 编码文本，并使用 [文本整形](#the-concept-of-text-shaping) 与 [OpenType 彩色字体](#opentype-color-fonts) 对其进行排版。

在 LaTeX 文档中包含彩色 emoji 的实际选项取决于用于编译该文档的 TeX 引擎；也就是说，你使用的是：

* pdfLaTeX：pdfTeX 引擎 + LaTeX；
* XeLaTeX：XeTeX 引擎 + LaTeX；
* LuaLaTeX：LuaHBTeX 引擎（从 TeX Live 2020 开始）+ LaTeX。

这三种 TeX 引擎都可以使用 LaTeX 工具或宏包来绘制 emoji，或使用 `\includegraphics{...}` 来插入存储在外部图形文件中的 emoji。当你需要一种不依赖于用于编译 LaTeX 文档的 TeX 引擎的解决方案时，绘制或导入图形是排版 emoji 的理想技术。

然而，如果你的工作流程允许灵活选择特定的 TeX 引擎，并且你更希望使用 OpenType 彩色字体和基于 Unicode 的文本处理，那么你需要 LuaTeX 的最新版本，即 LuaHBTeX。从 TeX Live 2020 开始，LuaHBTeX 用于编译基于 LuaLaTeX 格式的 LaTeX 文档。

## 关于 Unicode 和 emoji 字符的背景

### 字符编码

计算机使用一系列数值（整数）来存储、传输和处理文本，这些数值表示文本的组成 *字符*。可靠的文本处理需要文本的生产者和消费者就应使用哪些整数值来表示文本流中的各个字符达成一致。换句话说，该文本的 *@* *编码是什么？* 编码是一组约定好的整数值，用于表示某一组字符：每个字符都由所使用编码中的一个整数值表示。

### 进入 Unicode

从历史上看，在 8 位文本时代，曾使用过许多不同的字符编码，这总会引发 *编码不匹配*：文本的生产者和消费者错误地假定了不同的编码，从而导致文本处理错误。任何使用 TeX/LaTeX 多年的人都可能遇到过输入文本与用于排版文档的字体之间的编码不匹配。如果文档字体配置为使用与文本不同的编码，排版出的 PDF 中很可能会出现缺失或错误的字符。

这些历史上的编码问题可以通过一种为世界上所有字符进行编码的国际标准来解决：Unicode。Unicode 标准并非静态，而是会定期更新，以在其编码方案中纳入更多字符和文字（书写系统）。还有一个 [提议新字符的正式审查流程](http://www.unicode.org/pending/proposals.html) 以及一个专门的 [新 emoji 字符方案](https://www.unicode.org/emoji/proposals.html).

### Unicode 有多少个字符？

Unicode 理论上最多可编码 1,114,112 个字符。这 1,114,112 个整数值中的每一个都称为一个 *码位*：用于标识每个字符所分配的整数值。然而，由于各种技术原因，只有 [1,112,064 个码位](https://en.wikipedia.org/wiki/Unicode#Architecture_and_terminology) 可分配给实际字符：2048 个码位不可分配，且禁止在符合 Unicode 的文本中使用。

在撰写本文时（本文的首个版本），Unicode 标准第 13 版已将总计 143,859 个码位分配给实际字符，其中包括 [3304 个如今被编码为 emoji 的字符](https://www.unicode.org/L2/L2020/20114r-family-emoji-explor.pdf) （见该文档第 2 页）。Unicode 编码字符数量的增长在文章 [Unicode 有多少个字符？](https://www.babelstone.co.uk/Unicode/HowMany.html) 以及在一个 [维基百科条目](https://en.wikipedia.org/wiki/Unicode#Versions).

### Unicode 平面

全部 1,114,112 个 Unicode 码位被分为 17 个所谓的平面：从平面 0 到平面 16，每个平面包含 65536 个码位值，总计 $$17\times2^{16} = 1,114,112$$ 个字符。平面 0 被称为 [基本多文种平面](https://en.wikipedia.org/wiki/Plane_\(Unicode\)#Basic_Multilingual_Plane)，编码最常用的字符。平面 1–16 被称为 [补充平面](http://unicode.org/glossary/#supplementary_planes).

### emoji 的兴起

新字符随着人类交流方式的变化而出现，而移动电话技术催生了这样一组字符：emoji，它们在 1990 年代后期于日本演化而来。毫不奇怪， [Unicode emoji 常见问题](https://unicode.org/faq/emoji_dingbats.html) 指出

> “emoji 这个词源自日语中的 [絵](http://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=%E7%B5%B5) （e ≅ 图片）+ [文字](http://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=%E6%96%87) （moji ≅ 书写字符）。”

对 emoji 背景和历史发展的感兴趣读者可能会对这篇 [Unicode 介绍](https://unicode.org/reports/tr51/#Introduction) 或文章 [I second that emoji：emoji 的标准、结构与社会生产](https://firstmonday.org/ojs/index.php/fm/article/view/9381).

直到 2010 年，随着 [Unicode 标准 6.0 版的发布](https://www.unicode.org/versions/Unicode6.0.0/)，许多 emoji 才被正式认定为 *字符* 作为独立字符。Unicode 13.0 编码了 [3304 个字符作为 emoji](https://www.unicode.org/L2/L2020/20114r-family-emoji-explor.pdf) （见该文档第 2 页），而 Unicode 13.1 列出 [共 3521 个 emoji](https://unicode.org/emoji/charts/emoji-counts.html).

### Emoji 生活在更高的平面上

Unicode 将许多 emoji 字符分配到了基本多文种平面（BMP）之外的码位，编码在 [平面 1 中](https://en.wikibooks.org/wiki/Unicode/Character_reference/1F000-1FFFF) 其码位范围为 1F000–1FFFF——这对任何希望 *复制和粘贴* 将 emoji 字符粘贴到 Overleaf 编辑器（代码编辑器或可视化编辑器）的人来说具有重要影响。Overleaf 的文本编辑器目前只能处理基本多文种平面内的字符，尽管我们希望未来的升级能引入对非 BMP 字符的支持。请注意，此限制仅影响粘贴到将通过 Overleaf 编辑器编辑的文件中的文本里的非 BMP 字符。还有其他方式访问 emoji 字符：

* 使用原始命令 `\char"<码位>` 或 `\Uchar"<码位>` （见 [本节](#optional-detail-luatexluahbtex-char-vs-uchar) 文章中的内容）。
* 使用包含 emoji 字符的 UTF-8 格式输入文本文件。
* 使用插入 emoji 字符的 LaTeX 命令（宏）。

#### 将 emoji 和其他非 BMP 字符粘贴到 Overleaf 中

如果你将一个 emoji 字符，例如 😀，粘贴到 Overleaf 代码编辑器中，目前它会被转换为字符 ��。

![因复制 + 粘贴非 BMP 字符到 Overleaf 编辑器而导致的错误](/files/9d1e3e84652e14d82ab4cbc2c3106eaccc9f3736)

� 字符的 Unicode 码位是 FFFD，其正式名称是 REPLACEMENT CHARACTER，用于“[替换一个未知、未识别或无法表示的字符](https://en.wikipedia.org/wiki/Specials_\(Unicode_block\))”。

### 在 LuaLaTeX 中使用 Unicode 码位（U+）

Unicode 文档使用以下记法表示码位值： `U+<十六进制值>`——例如 `U+1F600`，其中 `1F600` 是 `<十六进制值>` 是 😀 emoji 字符对应的 Unicode 码位的十六进制值。要在 LuaLaTeX 中使用这些码位值，你删除 `U+` 并写成 `\char"<十六进制值>` 或 `\Uchar"<十六进制值>`。 `"` 字符告诉 TeX 引擎所提供的数字是以十六进制指定的。例如，要使用 😀 emoji，你可以写 `\char"1F600` 或 `\Uchar"1F600`——并使用能够对其进行排版的字体。

一个使用以下命令的最小 LuaLaTeX 示例： `\char` 和 `\Uchar` 来排版 😀 emoji 字符的示例可能是：

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont[Renderer=Harfbuzz,SizeFeatures={Size=20}]{NotoColorEmoji.ttf}
%在组中使用 \emojifont 以将其影响限制在局部
{\emojifont
\Uchar"1F600
\char"1F600}
\end{document}
```

[在 Overleaf 中打开此 LuaLaTeX 示例](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Test+using+LuaLaTeX\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%5BRenderer%3DHarfbuzz%5D%7BNotoColorEmoji.ttf%7D%0A%25Use+%5Cemojifont+in+a+group+to+keep+its+effects+local%0A%7B%5Cemojifont+%0A%5CUchar%221F600%0A%5Cchar%221F600%7D%0A%5Cend%7Bdocument%7D)

**（可选细节）LuaTeX/LuaHBTeX：\char 与 \Uchar**

除了常规的 `\char<字符代码>` 命令用于排版特定的 `<字符代码>`，使用当前字体，LuaTeX、LuaHBTeX 和 XeTeX 引擎还提供 `\Uchar<字符代码>` 命令。从用户的角度看， `\char` 和 `\Uchar` 看起来一样，但这些命令的工作方式存在细微差别，下面我们会说明。

**关键区别：展开**

`\Uchar` 是一种所谓的 [可展开命令](/latex/zh-cn/shen-du-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md#expansion-a-general-term-for-a-set-of-operations) 而 `\char` 不可展开。当一个 `\char<字符代码>` 或 `\Uchar<字符代码>` 命令被“执行”时——也就是说，该命令并未作为宏或其他记号列表的一部分被存储——TeX 引擎内部会发生以下操作：

* **`\char<字符代码>`** 指示 TeX 引擎立即插入一个字符记号，表示 `<字符代码>`，到它当前正在排版的任意内容中。
* 相比之下， **`\Uchar<字符代码>`** 具有两个不同的处理步骤：

1. 该 `\Uchar<字符代码>` 命令是 *展开*，并且 `<字符代码>` 被转换为一个临时记号列表，其中包含一个单独的 [字符记号](/latex/zh-cn/shen-du-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md#tex-tokens-101-28and-notions-of-expansion29) ，它表示 `<字符代码>`.
2. 这个单字符记号列表现在已 *可供使用* 作为 TeX 引擎下一次输入的来源。实际上，TeX 引擎会“暂时转移视线”，将这个单记号列表用作其下一个输入项（记号）的位置。默认情况下，TeX 引擎只是回去读取（输入）该记号并排版相应字符，从而重现 `\char` 该命令。 **然而**，由于那个 `<字符代码>` 并未立即被排版，而是临时 *存储* （保存）为一个单独记号，原始 TeX 命令或 LaTeX 宏可以使用（吸收）该记号——它不必立即排版，而是可以按需用于后续处理。

实际上， `\char<字符代码>` 表示“现在排版这个 `<字符代码>` ”，而 `\Uchar<字符代码>` 通过创建一个存储的字符记号并将其作为下一个输入项（一个记号）提供，从而具有某种“延迟动作”。该记号既可以被 TeX 命令和宏使用（吸收），也可以被 TeX 引擎重新读取并排版。

### Unicode（编码）并不是全部故事

在 Unicode 编码文本中使用 emoji 字符的能力只是 emoji 成功故事的一部分。emoji 使用量的激增还得益于 [OpenType 字体技术的发展](https://learn.microsoft.com/en-us/typography/opentype/)——这类字体的字形数据（字符设计）可以包含 [彩色数据](https://learn.microsoft.com/en-us/typography/opentype/spec/colr)：所谓的 [OpenType 彩色字体](#opentype-color-fonts).

除了合适的字体之外，使用彩色 emoji 还需要额外的软件组件，其任务包括：

* 预处理（“[整形](#the-concept-of-text-shaping)”）Unicode 编码文本， *为其准备* 使用特定字体进行显示；
* *渲染和显示* 字体中彩色 emoji 的 *字形* 到设备屏幕上。

#### 字形 vs. 字符：它们不是同一个东西吗？

“字形”和“字符”这两个术语常常被当作可互换使用——指代同一个基本概念——但它们的含义之间存在细微但重要的差别。

Unicode [将“字符”一词定义为](http://www.unicode.org/glossary/#character) 如下：

> “具有语义价值的书面语言的最小组成部分；指的是抽象意义和/或形状，而不是某种具体形状……”

相比之下，“字形”是一个 *具体的* *形状* （设计），用于 *视觉表示* 某个特定 *@*.

当在各种软件系统/平台上查看包含大量 emoji 的文本时，比如在基于 iOS 或 Android 的手机上，或在 Windows 台式电脑上阅读同一文本，就能很容易观察到字符与字形之间的问题。无论使用哪种设备或平台，其底层文本（字符序列）都将包含相同的 Unicode 编码 *emoji* *字符*。是设备特定的能力参与了 *预处理* 该文本随后 *渲染* 和 *显示* 结果的显示，可能使用设备特定字体，从而生成不同的字形（字符设计）来表示相同的 emoji 字符。

Unicode 的 [完整 Emoji 列表](https://unicode.org/emoji/charts/full-emoji-list.html) 提供了代表每个 Unicode emoji 字符的示例图像——展示了不同技术供应商所使用的各种字形。不仅字体设计师会采用各自特定的设计（字形）来表示 emoji 字符，而且不同字体所支持的 emoji 字符数量（即包含的字形）也各不相同，并且可能包含也可能不包含 Unicode emoji 规范中所列的更高级 emoji 文本处理功能。

字符的概念及其语义与编码构成了 Unicode 世界的基础：它处理的是字符。单个字符作为字形的设计与视觉呈现，则属于字体技术和字体设计工艺的范畴。

#### Unicode emoji：远不止文本编码

Unicode 的核心作用是提供一个全球统一的编码标准，规定应使用哪个整数值，也就是一个 *码位，* 用来表示 Unicode 编码文本流中的每个字符，包括 emoji。

Unicode 对 emoji 的规范还定义了 *处理行为* 针对某些 *序列* 出现在 Unicode 编码文本流中的 emoji 字符。已定义的 emoji 字符序列可以通过一个称为 [文本整形](#the-concept-of-text-shaping) 的过程“合并”，从而生成一个单一的结果（“复合”）emoji 字形——该单个字形将由设备的操作系统用来表示文本中原始的字符序列。

Unicode 关于……的技术报告 [Unicode Emoji](https://unicode.org/reports/tr51/) 记录了希望提供符合 Unicode 的 emoji 字符处理的软件可用的丰富功能集。举例来说，Unicode 定义（编码）了称为 [emoji 修饰符](http://www.unicode.org/reports/tr51/#Emoji_Modifiers_Table) 可用于生成 *变化* “基础” emoji 字符的，例如 [基于 Fitzpatrick 量表的肤色](http://www.unicode.org/reports/tr51/#Diversity)。请注意，基础 emoji 字符集及其适用的修饰符被定义为整体 [Unicode emoji 标准的一部分](http://www.unicode.org/reports/tr51).

Unicode 页面 [Emoji 序列](http://unicode.org/emoji/charts/emoji-sequences.html) 提供了当前由 Unicode 规范给出的序列图表。将鼠标指针悬停在任何 emoji 字形图像上，会显示一个小型弹出提示，告诉你产生该字形的底层 Unicode emoji 字符序列：

![EmojiSequenceChart.png](/files/3eb3d2d6803dee592d31ac844f9e1e09a611ddb7)

例如，emoji 字形：

![HandMediumSkinTone.png](/files/0b5f0ad2e3aef166520b93d453da12f43b0cba2f)

列在 [修饰符序列部分](http://unicode.org/emoji/charts/emoji-sequences.html#modifier_sequences) 并由两个字符的序列 U+1F44B U+1F3FD 生成。其组成字符是：

U+1F44B：![UnicodeWavingHandDefault.png](/files/6801a392fc4208c4387301e263b6670de95be9a6) （挥手）

U+1F3FD：![FitzPatrick3.png](/files/6d07f19a6bfba70e45e9bf79a5a231585e50026b) （EMOJI 修饰符 Fitzpatrick 类型-4）

**在 LuaHBTeX 中使用肤色修饰符**

以下示例使用 LuaHBTeX 来演示 emoji 修饰符的使用：

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont[Renderer=HarfBuzz,SizeFeatures={Size=20}]{NotoColorEmoji.ttf}
单独的挥手：{\emojifont\Uchar"1F44B}\par
单独的修饰符：{\emojifont\Uchar"1F3FD}\par
组合结果：{\emojifont\Uchar"1F44B\Uchar"1F3FD}
\end{document}
```

[在 Overleaf 中打开此 LuaLaTeX emoji 修饰符示例](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Emoji+modifiers+using+LuaLaTeX\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%5BRenderer%3DHarfBuzz%2CSizeFeatures%3D%7BSize%3D20%7D%5D%7BNotoColorEmoji.ttf%7D%0AIsolated+waving+hand%3A+%7B%5Cemojifont%5CUchar%221F44B%7D%5Cpar%0AIsolated+modifier%3A+%7B%5Cemojifont%5CUchar%221F3FD%7D%5Cpar+%0ACombined+result%3A+%7B%5Cemojifont%5CUchar%221F44B%5CUchar%221F3FD%7D%0A%5Cend%7Bdocument%7D)

此示例生成如下输出：

![ModifiersInLuaHBTeX.png](/files/286ca2999d062002e03741d47b595a82a6c3cd45)

#### UTF-8：它在存储 Unicode 文本中的作用

你在 Overleaf 代码编辑器（或可视化编辑器）中输入或粘贴的任何文本或代码都将以 UTF-8 格式存储，因此我们将简要回顾 UTF-8 的实际含义。UTF 代表 Unicode Transformation Format，而 UTF-8 在存储或传输 Unicode 编码文本中的作用由“Transformation *格式*。”

Unicode 的码位值范围从 0 到最大 1,114,111，因此不可能使用单个 8 位字节来表示所有 Unicode 字符值，因为一个字节最多只能存储 256 个不同的值：0 到 255。不过，可以使用一个 *连续序列* 的字节大小数值来表示任何 Unicode 码位整数——这就是 UTF-8 背后的原理。

UFT-8 提供了一份“配方”来 *转换* （即“编码”或“转换”）一个 Unicode 整数码位值为 1 到 4 个连续字节大小整数的唯一序列：所需连续字节的数量取决于该码位整数的值。因此，你可能会把 UTF-8 存储 Unicode 字符的方式读作 *多字节序列* 因为一个 Unicode 字符（码位整数）在 UTF-8 中表示为 1 到 4 个连续字节的序列。

自然地，以 UTF-8 存储的文本可以转换回其原始的 Unicode 码位整数序列——这正是 XeTeX 或 LuaTeX/LuaHBTeX 在读取以 UTF-8 格式存储的 LaTeX 输入文件时必须做的事情。这些 TeX 引擎在排版文本之前需要知道输入的 Unicode 码位（字符）值。请注意，pdfTeX 没有内置的 UTF-8 解码能力，因此必须依赖 TeX 宏来处理（解码）以 UTF-8 格式输入的文本。

**一些 UTF-8 示例**

* 阿拉伯字符 ش（“sheen”）的 Unicode 码位是十六进制 0634，或十进制 1588。在 UTF-8 中，ش 由 2 个十六进制值 D8 和 B4 表示，因此字符 ش 会在 UTF-8 编码文本中存储为两个连续字节 D8B4。
* emoji 字符 😀 的 Unicode 码位是十六进制 1F600，或十进制 128512。在 UTF-8 中，😀 由 4 个十六进制值 F0、9F、98 和 80 表示，因此字符 😀 会在 UTF-8 文本文件中存储为 4 个连续字节 F09F9880。

#### Unicode 基础的 emoji 文本处理中使用的特殊字符

并非 Unicode 中编码的每个字符都用于通过字体中的字形进行视觉呈现：有些编码字符被指定为 *不可打印字符* 其用途是辅助专门的文本处理功能（在支持软件中）。不同软件应用对 Unicode 中编码的不可打印字符提供不同程度的支持，因此结果将取决于所使用的软件环境——应用程序和字体。

**两个需要了解的不可打印字符**

* **零宽连字符（ZWJ）**，码位 200D（十六进制）顾名思义，旨在触发输入字符的“连字行为”——但前提是这些输入字符 *具有* 已定义的连字行为。
* **零宽非连字符（ZWNJ）**，码位 200C（十六进制），旨在 *阻止* 阻止输入字符本可能表现出的“连字行为”。例如，你可以使用 ZWNJ 来防止连续阿拉伯字符的连字行为，因为这些字符通常会被处理（整形）为其连字形式。

Unicode 已发布一份 [推荐的 Emoji ZWJ 序列](https://unicode.org/emoji/charts/emoji-zwj-sequences.html) 其中使用 U+200D ZERO WIDTH JOINER（ZWJ）将 emoji 字符序列组合为单个复合 emoji 字形——如果所使用的字体中提供了该字形。

**零宽非连字符的示例用法**

以下最小代码片段使用 TeX Live 中包含的 Scheherazade OpenType 字体，定义一个名为 `\arabicfont` 的 LaTeX 字体，我们可以用它来排版一些阿拉伯文。该行

```latex
{\arabicfont Non-joining:\textdir TRT\Uchar"0644\Uchar"200C\Uchar"0627}
```

通过 `\Uchar"200C`，来阻止阿拉伯字母 ل（lam）和 ا（alef）的正常连字行为。请注意使用 `\textdir TRT` 将文本方向设置为从右到左：

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\arabicfont[Script=Arabic,Renderer=Harfbuzz,SizeFeatures={Size=40}]{Scheherazade}
{\arabicfont Joining:\textdir TRT\Uchar"0644\Uchar"0627}\par
{\arabicfont Non-joining:\textdir TRT\Uchar"0644\Uchar"200C\Uchar"0627}
\end{document}
```

[在 Overleaf 中打开此 LuaLaTeX 示例](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Zero+width+non-joiner+using+LuaLaTeX\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Carabicfont%5BScript%3DArabic%2CRenderer%3DHarfbuzz%2CSizeFeatures%3D%7BSize%3D40%7D%5D%7BScheherazade%7D%0A%7B%5Carabicfont+Joining%3A%5Ctextdir+TRT%5CUchar%220644%5CUchar%220627%7D%5Cpar%0A%7B%5Carabicfont+Non-joining%3A%5Ctextdir+TRT%5CUchar%220644%5CUchar%22200C%5CUchar%220627%7D%0A%5Cend%7Bdocument%7D)

此示例生成如下输出：

![NonJoiner.png](/files/37bdc1a503b196f61ab13958ca8e3b1c4af3821f)

## “文本整形”的概念

让我们从一个视觉示例开始，使用“educational”一词的乌尔都语译文。乌尔都语译文的文本可能通过键盘或触摸屏设备输入，并会被创建为一串简单线性的 Unicode 阿拉伯字符序列。然而，当该文本被排版，或在设备屏幕上以 [Nastaliq 风格](https://en.wikipedia.org/wiki/Nastaliq)显示时，结果会是一个复杂的二维字形排列。

以我们的乌尔都语示例为例，下面的图形比较了 Unicode 阿拉伯文的线性输入 *字符* 与以 Nastaliq 风格排版的输出之间的差异，后者包含一个二维排列的 *字形* 存在于（免费）字体中的 [Awami Nastaliq](https://software.sil.org/awami/download/):

![](/files/4b41ae8e3c656cef4eb80e2b3f9a6f614f99ae9b)

将输入字符“转换”为一组位置正确的输出字形的过程称为 *文本整形*，它是在显示或排版之前处理文本的一个关键组成部分。我们的示例使用乌尔都语（阿拉伯文字）文本，因为整形的结果非常明显；相比之下，使用拉丁文字的语言，如英语，其整形效果要不那么显著——例如只会产生简单的连字。

在使用诸如 [阿拉伯语](https://en.wikipedia.org/wiki/Arabic), [希伯来语](https://en.wikipedia.org/wiki/Hebrew_language), [天城文](https://en.wikipedia.org/wiki/Devanagari) 或 [马拉雅拉姆语](https://en.wikipedia.org/wiki/Malayalam)，这只是所谓的四个示例 *复杂文字*。为了确保这些文字及使用它们的语言的文本能够正确呈现，整形过程需要仔细处理特定文字和语言组合中的任何整形规则和细微差别。例如，某些语言需要多个输入字符才能生成某个特定输出字形，或者可能存在对变音符号精确定位的复杂要求，以及字形之间的重新排列，以确保各个字形相对于彼此的位置正确。

一般而言，对一段文本进行整形需要若干信息：

* 书写系统或 *文字系统* 文本所使用的语言。
* 特定的 *语言* 正在使用。单个脚本可用于多种语言，而每种脚本–语言组合都有其各自的排版微妙差异/细节。
* 书写 *方向* ，例如从右到左或从左到右。
* 一个 *字体* 它提供表示经过排版的文本所需的字形，并且可选地包含额外的“排版规则”，用于指导文本排版过程。

文本排版的要求，尤其是对于复杂脚本及其相关语言，可能极其详细而微妙，这表明需要专门的软件来应用可能非常复杂的文本排版“规则”。毫不奇怪，这类软件确实存在，被称为一个 *文本排版引擎*；我们将要讨论的称为 [HarfBuzz](https://en.wikipedia.org/wiki/HarfBuzz)，其文档值得一读——例如 [为什么我需要排版引擎？](https://harfbuzz.github.io/why-do-i-need-a-shaping-engine.html).

**关于文本排版的进一步阅读**

强烈推荐以下简短介绍：

* [什么是文本排版？](https://harfbuzz.github.io/what-is-harfbuzz.html#what-is-text-shaping)
* [为什么我需要排版引擎？](https://harfbuzz.github.io/why-do-i-need-a-shaping-engine.html)

**TeX 技术说明：多种排版技术（模型）**

HarfBuzz 文本排版引擎支持若干“排版技术”，它们在实现排版过程的方式上各不相同——每种实现都称为一个 *排版器*，包括在 `luaotfload` 文档中。本文的重点是 OpenType 排版，但另一种可免费使用的技术是 [Graphite](https://scripts.sil.org/cms/scripts/page.php?site_id=projects\&item_id=graphite_aboutOT)，由 [SIL International](https://www.sil.org/)开发。HarfBuzz 支持的另一种排版模型是 [Apple Advanced Typography（AAT）](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6AATIntro.html)——支持 AAT 的字体通常用于 Apple 技术平台。

**使用 Graphite 排版器的示例**

下面的示例使用名为 [Awami Nastaliq](https://software.sil.org/awami/download/)的字体来排版一些乌尔都语文本，该字体支持 Graphite 排版，并可在 Overleaf 上使用。Awami Nastaliq 由 [SIL International](https://www.sil.org/)创建该技术的开发组织。

下面的示例展示了基于 Graphite 的字体的高级排版能力——请注意 `luaotfload` 字体声明如何使用 `shaper=graphite2`.

```latex
\documentclass{article}
\usepackage{luaotfload}
\begin{document}

\font\urdutest={file:AwamiNastaliq-Regular.ttf:mode=harf;shaper=graphite2} at 100bp
% Technology
\pardir TRT\textdir TRT \urdutest ٹیکنالوجی

\vskip 75bp

% Educational
\pardir TRT\textdir TRT \urdutest تعلیمی
\end{document}
```

[在 Overleaf 中打开此示例。](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Typesetting+Urdu+using+the+Graphite+shaper\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bluaotfload%7D%0A%5Cbegin%7Bdocument%7D%0A%0A%5Cfont%5Curdutest%3D%7Bfile%3AAwamiNastaliq-Regular.ttf%3Amode%3Dharf%3Bshaper%3Dgraphite2%7D+at+100bp%0A%25+Technology%0A%5Cpardir+TRT%5Ctextdir+TRT+%5Curdutest+%D9%B9%DB%8C%DA%A9%D9%86%D8%A7%D9%84%D9%88%D8%AC%DB%8C%0A%0A%5Cvskip+75bp%0A%0A%25+Educational%0A%5Cpardir+TRT%5Ctextdir+TRT+%5Curdutest+%D8%AA%D8%B9%D9%84%DB%8C%D9%85%DB%8C%0A%5Cend%7Bdocument%7D)

此示例生成如下输出：

![](/files/4d7de5316ad92c67d5d0cc5e3944942ccfe9825a)

#### 表情符号与文本排版

我们已使用复杂脚本语言乌尔都语的示例介绍了文本排版。然而，令人惊讶的是，要渲染正确的表情符号字形，需要对包含表情符号字符序列的 Unicode 文本应用文本排版——[正如 HarfBuzz 的首席开发者所指出的](https://github.com/harfbuzz/harfbuzz/issues/2428#issuecomment-639108677):

> ……使用 HarfBuzz 对表情符号进行排版完全在其范围之内，而且要获得家庭表情、肤色等效果实际上是必要的。

我们将看看这方面的示例。

### 职责分工：文本排版引擎 + OpenType 字体

在实践中，文本排版是一种文本排版引擎内置的逻辑和规则与所用字体中内置的附加排版规则和数据之间的“联合操作”或分工——从现在起我们将只介绍基于 OpenType 的排版 *仅*.

要执行排版，通常会向文本排版引擎提供一些 Unicode 文本、指定的脚本和语言，可能还包括书写方向，以及最重要的，一个在排版过程中使用的 OpenType 字体——该字体将提供输出：一组字形和定位数据。如果需要，排版引擎还可以应用额外的规则（[OpenType 特性](https://en.wikipedia.org/wiki/List_of_typographic_features#OpenType_typographic_features)）这些规则包含在所用的 OpenType 字体中——要应用哪些规则通常可以由用户从字体支持的特性列表中选择。

排版过程的结果是一个 *字形列表* ，包含在 OpenType 字体中，并附带 *字形间* 定位数据。该定位数据涉及 *排版后字形的相对位置*；它不涉及排版页面或其他媒体/内容（如网页、Tweet 等）中的绝对位置。渲染软件（排版引擎、网页浏览器等）使用字形间定位信息，确保字形组装并并入最终输出后彼此之间的位置正确。

#### 什么是字形列表？

在内部，OpenType 字体中的每个字形都会被分配一个数字标识符，即称为字形索引的整数值——也称为字形标识符或 GID。完成排版任务后，文本排版引擎会将结果返回为一个 *字形标识符列表* 加上 *定位数据* ，用于这些字形。

OpenType 字体中的单个字形由字体创建者分配索引（标识符），因此这是一个高度依赖字体且任意的值——它也可能因某一特定字体的版本而异。切勿假定不同字体中“相似”的字形会使用相同的 GID 值；几乎肯定不会。如果你有排版引擎提供的字形标识符列表，你只能用它们来访问获取它们所来源的那个字体中的字形。

#### 什么是 OpenType 字体？

网上 *充斥着* 关于 OpenType 字体的解释和细节，因此我们只做简短说明。 [OpenType 规范](https://docs.microsoft.com/en-us/typography/opentype/spec/) 是一份面向开发者的复杂文档，但从本质上说，它定义了一种用于字体数据的文件格式或容器。OpenType 字体包含描述字形形状的数据，以及受支持的脚本和语言信息、字体的元数据，以及各种定义 [排版特性](https://en.wikipedia.org/wiki/List_of_typographic_features#OpenType_typographic_features) 的表。

通常可以指示文本排版引擎在排版过程中选择性地应用（使用）字体的特性，应用特定的排版效果（“规则”），从而选择字体中适当的一组字形。所选字体需要支持并提供文本排版引擎要求应用的任何特性的字形。

#### 编码和未编码的“字形”

OpenType 字体包含一个名为 [cmap](https://docs.microsoft.com/en-us/typography/opentype/spec/cmap) （字符到字形索引映射）的数据表，它将字体支持的 Unicode 字符集合映射到该字体中对应的字形索引。以下视频简要展示了名为 `lmmono10-regiular.otf` （包含在 TeX Live 中）。

{% embed url="<https://videos.ctfassets.net/nrgyaltdicpt/2537Y9gOUMWgd0t1guqt0X/482c53a9d8112ecae3d622aa7e00eef8/openType_cmap.mp4>" %}

然而，字体通常包含许多并不表示特定 Unicode 字符、也不包含在该 cmap 表中的字形。因此，OpenType 字体中的字形集合可以分为两大类：

* 表示 Unicode 字符的已编码字形；
* 不表示 Unicode 字符的未编码字形。

编码字形可以通过在文本中包含相应的 Unicode 字符来访问——但未编码字形呢，它们如何使用/访问？这些字形通常用于提供文本排版操作的输出，包括应用字体特性以产生特定的视觉/排版效果。

### OpenType 彩色字体

表情符号字符预计应以全彩显示/渲染——黑白表情符号并不能完全提供“完整的表情符号体验”。然而，在 Unicode 最初对表情符号进行编码时， [OpenType 字体规范](https://docs.microsoft.com/en-us/typography/opentype/spec/) 并没有为在 *colorful*在 OpenType 字体中嵌入字形数据提供任何合适的方案。OpenType 中的这一“空白”促使领先的技术/平台供应商寻找解决方案，随后展开的“竞赛”导致了 [扩展 OpenType 的各种提案](https://www.fontlab.com/news/color-font-format-proposals/) 以支持 OpenType 彩色字体——不仅用于显示彩色表情符号字符（字形），也用于以彩色渲染任何字形。

#### OpenType 彩色字体的四种类型

[Adobe、Microsoft、Google 和 Apple 各自提交了提案](https://www.fontlab.com/news/color-font-format-proposals/) 以扩展 OpenType 来支持全彩 OpenType 字体，最终有四项提案被采纳并纳入正式的 OpenType 规范。为方便起见，我们可以粗略地将这四种变体分为基于矢量和基于光栅两类——但正如在这个 [GitHub 仓库](https://github.com/simoncozens/test-fonts)中所示，OpenType 规范足够灵活，能够支持结合这四种基础技术的 OpenType 彩色字体文件。

* **基于矢量的 OpenType 字体：**
* **Microsoft**：字形形状使用一种分层彩色矢量形式来描述（[COLR](https://docs.microsoft.com/en-us/typography/opentype/spec/colr) 和 [CPAL](https://docs.microsoft.com/en-us/typography/opentype/spec/cpal) 表）。
* [**Adobe 和 Mozilla**](https://www.w3.org/2013/10/SVG_in_OpenType/) ([SVG 表](https://docs.microsoft.com/en-us/typography/opentype/spec/svg)）：字形形状使用 SVG 绘制，SVG 支持由矢量构成的字形 *和光栅图像*。另见 [Adobe 关于 SVG 字体的用户指南](https://helpx.adobe.com/fonts/user-guide.html/fonts/using/ot-svg-color-fonts.ug.html).
* **基于光栅的 OpenType 字体：**
* **Google**：字形由嵌入字体中的彩色 PNG 图像表示（[CBDT](https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt) 和 [CBLC](https://docs.microsoft.com/en-us/typography/opentype/spec/cblc) 表）。
* **Apple**：字形同样由嵌入字体中的彩色图像表示。除了 PNG 之外，Apple 的机制（[sbix 表](https://docs.microsoft.com/en-us/typography/opentype/spec/colr)）还支持 JPEG 和 TIFF。

因此，支持 OpenType 彩色字体的操作系统和应用软件需要应对当今混合技术并存的局面。此外，你还应当知道，单个 OpenType 彩色字体——以及 *版本* 相同字体的——会：

* 对完整集合中的 [Unicode 表情符号字符](https://unicode.org/emoji/charts/emoji-list.html)具有不同的覆盖范围——即字体为多少表情符号字符提供了字形；
* 使用不同的字形设计来表示单个表情符号字符；
* 在支持 Unicode 标准更高级用法的功能上有所差异，例如 [emoji 修饰符](https://unicode.org/reports/tr51/#Emoji_Modifiers_Table)，以及在 [Unicode 技术标准 #51：Unicode Emoji](https://unicode.org/reports/tr51/).

#### 关于 HarfBuzz 的热议

我们已经提到了对一个 *文本排版引擎*：一种软件，它接受用特定脚本和语言组合编写的输入 Unicode 文本，并使用指定字体将该文本排版成一串字形，以及可供排版原始输入文本使用的定位数据。

[HarfBuzz](https://harfbuzz.github.io/) 是这样一种文本排版引擎：它是 [一个开源代码库](https://github.com/harfbuzz/harfbuzz) ，并且是十多年研究与开发的成果——至今仍作为许多软件产品的一部分被积极开发和部署。HarfBuzz 本身不执行“排版”，而是为选择集成它的软件提供“文本排版服务”，包括 XeTeX、LuaHBTeX、 [Adobe PhotoShop 和 Adobe InDesign](https://en.wikipedia.org/wiki/HarfBuzz).

通过集成 HarfBuzz，TeX 引擎可以利用其先进的文本排版能力，提供非常复杂的多语言排版，尤其适用于阿拉伯文、希伯来文、天城文及许多其他复杂脚本。还要注意，HarfBuzz 也用于处理和排版 Unicode 表情符号文本字符，我们将更详细地探讨这一点。

下图概述了 HarfBuzz 在与 XeTeX 或 LuaHBTeX 等软件集成时，在为阿拉伯文等复杂脚本排版文本过程中所扮演的角色：

![使用 HarfBuzz 进行阿拉伯文文本排版概览](/files/e78551d2acb34cf95b750132bccd734e5dd565be)

**探索 HarfBuzz**

任何想进一步了解 HarfBuzz 以及它为 XeTeX 和 LuaHBTeX 提供的 OpenType 排版服务的人都可以 [下载 HarfBuzz 的二进制发行版](https://github.com/harfbuzz/harfbuzz/releases) ，其中包含 HarfBuzz 库（供程序员使用）以及命令行实用工具 `hb-view` 和 `hb-shape`.

**示例：如何使用 hb-view**

在你喜欢的支持 UTF-8 的文本编辑器中新建一个文件，并将以下六个表情符号 👋👋🏻👋🏼👋🏽👋🏾👋🏿 复制/粘贴到该文本文件中，然后以 UTF-8 格式保存为名为，例如 `emoji.txt`.

请注意，你的文本编辑器可能会显示这些表情符号的（回退）黑白版本，因为它无法（或未被编程为）渲染彩色字形。一旦这 6 个表情符号保存完毕，文件 `emoji.txt` 应包含以下 Unicode 表情符号序列的 UTF-8 数据——我们用逗号分隔表情符号修饰符，仅为 *便于阅读*:

* `1F44B` 生成 👋
* `1F44B`, `1F3FB` 生成 👋🏻
* `1F44B`, `1F3FC` 生成 👋🏼
* `1F44B`, `1F3FD` 生成 👋🏽
* `1F44B`, `1F3FE` 生成 👋🏾
* `1F44B`, `1F3FF` 生成 👋🏿

总共应有 **11** 个 Unicode 字符，每个会生成 4 字节的 UTF-8 数据，因此最终的 `emoji.txt` 文件长度应为 44 字节，不包括行尾表情符号所在行末使用的任何行尾标记。

该 `hb-view` 实用工具可以使用文件 `emoji.txt`，以及你选择的合适的 OpenType 彩色字体，例如 `NotoColorEmoji.ttf`，来生成 HarfBuzz 排版输出的 SVG 文件。以下命令行示例必须 **在一行中输入** 到你的终端中，将生成 SVG 文件 `emoji.svg`:

```latex
hb-view --font-size=20 --output-file="emoji.svg"
--output-format=svg --text-file=emoji.txt
--font-file=NotoColorEmoji.ttf
```

成功执行后，由 `emoji.svg`生成的文件 `hb-view`可以由 Inkscape 打开，且看起来应类似这样：

![Hbvieemoji.png](/files/9ae064d66057b3e2c3d90fb2013ea45f81d759a3)

`hb-view` 可用于探索任何合适的 Unicode 文本文件和 OpenType 字体的 HarfBuzz 排版——它当然不限于用于表情符号！输入

```latex
hb-view --help-all
```

可以查看这个强大而方便的实用工具的丰富命令行选项。祝排版愉快！

## 文本排版与 TeX 引擎

在这里，我们将回顾 XeTeX 和 LuaTeX 系列 TeX 引擎的文本排版能力。

### XeTeX

XeTeX 于 2000 年代初开发，并在基于 TeX 的排版中率先引入了若干创新，最显著的是 *内置的* 对以下内容的支持：

* 读取 UTF-8 格式的 Unicode 文本；
* 使用 OpenType 字体；
* 用于多语言排版的文本排版；
* 基于 OpenType 的数学排版。

XeTeX 能够轻松便捷地排版复杂脚本语言，得益于其内置的文本排版能力——最初基于现已弃用的 [ICU LayoutEngine](http://userguide.icu-project.org/layoutengine)。得益于 Khaled Hosny 的工作，XeTeX 切换为使用 HarfBuzz 进行文本排版，正如 [2013 年 3 月](https://tug.org/pipermail/xetex/2013-March/024118.html)的一则公告所述。对于希望排版多语言文本的人来说，XeTeX 通常被认为是首选 TeX 引擎——但现在还有另一个选择：LuaHBTeX，我们将进行探讨。

### LuaTeX 和 LuaHBTeX

LuaTeX 的开发大约始于 2005 年，但其设计理念与 XeTeX 大不相同，后者将新功能 *直接* 集成到 XeTeX 软件中。与 XeTeX 相比，LuaTeX 的开发者选择了“……提供一组最小工具而不提供解决方案。”（参见 [LuaTeX 参考手册](https://www.pragma-ade.com/general/manuals/luatex.pdf)）。LuaTeX 系列引擎并不提供一整套额外功能 *内置于* LuaTeX 系列引擎中，而是将其内部机制开放出来，使开发者和熟练用户能够利用集成的 Lua 脚本语言构建自己的解决方案。

例如，与 XeTeX 不同，LuaTeX 引擎不能 *直接* 使用 OpenType 字体；相反，OpenType 字体必须通过用 Lua 代码编写的字体加载函数来加载并“准备使用”。这些字体加载函数被称为 *回调* 函数：当请求加载字体时，LuaTeX 将调用（“执行”）的 Lua 代码。

此外，LuaTeX 引擎并不提供任何 *内置的* 文本排版能力——这些能力同样必须由外部代码提供，LuaTeX 引擎可以调用这些代码为其提供文本排版服务。这里再次与 XeTeX 引擎形成对比，后者将文本排版能力纳入了核心软件。

#### luaotfload：在 LuaTeX/LuaHBTeX 中使用 OpenType 字体的关键

LuaTeX 的字体加载回调机制提供了极大的灵活性，尽管“代价”是需要额外编程。对于 LuaLaTeX 用户来说，TeX 社区幸运地开发了一个名为 `luaotfload`的宏包，它是 [TeX Live 年度发布](https://www.tug.org/texlive/) 的一部分，当然也可供 Overleaf 用户使用。

`luaotfload` 是 [可在 CTAN 上获取](https://ctan.org/pkg/luaotfload?lang=en) 并且有一个 [GitHub 上的开发仓库](https://github.com/latex3/luaotfload) ，你可以在那里跟踪最新进展和 [新发布版本](https://github.com/latex3/luaotfload/releases).

`luaotfload` 可以直接通过

```latex
\usepackage{luaotfload}
```

请注意 `luaotfload` 是一个 LaTeX *宏包*的名称，这意味着它的文件名是 `luaotfload.sty`。如果你想在普通 TeX 中使用 `luaotfload` ，你可以通过添加以下一行来实现

```latex
\input luaotfload.sty
```

到你的 plain TeX 文档中。

通常，LuaLaTeX 的用户——即使用 LuaTeX/LuaHBTeX 排版 LaTeX 的用户——不需要直接接触 `luaotfload` 因为 [`fontspec` 宏包](https://ctan.org/pkg/fontspec) 会为你加载 `luaotfload` 宏包，并通过 `fontspec` 宏包。

### LuaHBTeX：文本排版的新选项

`luaotfload` 是一个成熟且强大的 Lua 库，提供 LuaTeX 对 OpenType 字体的处理——同时也为多种语言和脚本提供文本排版服务。最初， `luaotfload` 的文本排版函数是用纯 Lua 代码实现的，但 TeX Live 2020 的发布又带来了另一种主流文本排版选项——一个名为 LuaHBTeX 的新 LuaTeX 引擎。

LuaHBTeX 中的“HB”代表 HarfBuzz——本质上，LuaHBTeX 就是原始的 LuaTeX 引擎 *加上* 配合一个集成的 HarfBuzz 文本排版引擎。遵循 LuaTeX 的设计理念，HarfBuzz 的可用性并不会 *自动地* 保证文本会由 LuaHBTeX 进行排版：HarfBuzz 是另一种可用于构建文本排版解决方案的工具。

LuaHBTeX 对 HarfBuzz 的集成是 [可通过 Lua 代码编程](#introduction-to-the-luahbtex-harfbuzz-api)，这使得 `luaotfload`的开发者能够添加基于 HarfBuzz 的文本排版解决方案。因此， [从 2019 年 11 月 5 日发布的 3.1 版开始](https://github.com/latex3/luaotfload/releases/tag/v3.1), `luaotfload` 得到了增强，以利用 HarfBuzz——使一般用户也能轻松访问 HarfBuzz 的文本排版能力。

对 HarfBuzz 与 LuaTeX 集成技术细节感兴趣的读者可以阅读 [Khaled Hosny 的论文](https://www.tug.org/TUGboat/tb40-1/tb124hosny-harfbuzz.pdf).

### luaotfload：文本排版的两种选择（何时使用 HarfBuzz？）

LuaLaTeX 用户现在有两种文本排版选项：

* `luaotfload`的原始（基于节点的）文本排版实现，完全用 Lua 编写；
* `luaotfload`的基于 HarfBuzz 的排版——通过调用 HarfBuzz 文本排版函数的 Lua 代码来访问。

`luaotfload` 通过其“`mode`”参数提供对这两个排版系统的访问——不过大多数用户会使用等价的 `fontspec` “`Renderer`”选项，而不是直接使用底层函数的 `luaotfload`.

每一种 `luaotfload`的文本排版解决方案都有其优点和（当前的）弱点，但你应该使用哪一种，又何时使用呢？以下几点可供考虑：

* `luaotfload`的原生基于节点的处理可能会占用大量内存，尤其是对于大型 CJK OpenType 字体。使用 HarfBuzz 进行 CJK 文本排版可以提升速度并减少内存使用。
* 对于复杂脚本，请使用 HarfBuzz，因为它“……极大地改善了印度文字和阿拉伯文字的渲染，并且强烈推荐用于此类脚本。”（见 `luaotfload` 手册）。
* HarfBuzz 集成到 `luaotfload` 中仍然相对较新，且仍在进一步开发中。在撰写本文时（2021 年 7 月），建议对主文档字体使用 luaotfload 内置排版（设置 `mode=node`）。尤其当你的文档使用拉丁脚本时更是如此。请参见这条 [GitHub issue](https://github.com/latex3/luaotfload/issues/175#issue-801120377)，其中总结了相关问题和讨论。如果你想试验，可以使用 `luaotfload` 来加载字体文件并创建两种 LaTeX 字体：一种使用基于 HarfBuzz 的排版，另一种使用基于 Lua 的排版。Overleaf 创建了一个 [示例项目](#sample-project-arabic-shaping)，演示了这一点。
* 不要使用 HarfBuzz 处理数学字体。正如 tex.stackexchange 上的开发者所讨论的，HarfBuzz [不是为处理数学排版字体而设计的](https://tex.stackexchange.com/questions/544881/does-luahbtex-with-harfbuzz-renderer-completely-supports-math-formating) ，所以不要将其用于该目的。

**示例项目：阿拉伯文排版**

这里有一个 Overleaf 项目，它使用多种高质量阿拉伯字体来比较 `luaotfload`的基于节点的文本排版服务（`mode=node`）与 HarfBuzz 的服务（`mode=harf`):

* <https://www.overleaf.com/latex/examples/complex-script-shaping-using-luaotfload-and-harfbuzz/gfssprnhfddn>

该项目包含以下图片所示的输出：

![阿拉伯文排版](/files/34502c2357406eae0431953140667263d2ac553a)

### 在 fontspec 中选择“Renderer”

正如其 [文档](https://mirror.ox.ac.uk/sites/ctan.org/macros/unicodetex/latex/fontspec/fontspec.pdf), `fontspec` “……允许 XeTeX 或 LuaTeX 的用户在 LaTeX 文档中加载 OpenType 字体”。如果你使用 LuaTeX 或 LuaHBTeX 引擎， `fontspec` 会为你加载 `luaotfload` 库为你提供支持，并且还提供一套方便的用户级命令，从而减少接触 `luaotfload`的低层功能。

那么你该如何在 HarfBuzz 的排版与 `luaotfload`提供的内置排版之间选择呢？答案包含在出色的 [`fontspec` 文档](https://mirror.ox.ac.uk/sites/ctan.org/macros/unicodetex/latex/fontspec/fontspec.pdf)中，尤其是第 VI 部分：仅 LuaTeX 的字体特性。 `fontspec` 提供一个名为 `Renderer` 的设置，可以在字体通过 `fontspec`. `Renderer` 定义时进行设置，它控制字体的低层处理。值得关注的两个选项是

* `Renderer = Node`：排版 OpenType 字体的默认“mode”——它使用 `luaotfload`的文本排版函数，这些函数完全用 Lua 实现。
* `Renderer = Harfbuzz`：此“mode”将字体定义/加载为供 HarfBuzz 文本排版引擎使用。 `luaotfload` 使用 LuaHBTeX 的 API 调用 HarfBuzz 中的函数。

有关更多信息，请参见 [`fontspec` 文档](https://mirror.ox.ac.uk/sites/ctan.org/macros/unicodetex/latex/fontspec/fontspec.pdf).

## TeX 引擎、HarfBuzz 和彩色表情符号

虽然 XeTeX 和 LuaHBTeX 都集成了 HarfBuzz，但它们对 HarfBuzz 某些更高级功能的支持程度不同——最显著的是加载和使用 OpenType 彩色字体。

### XeTeX 和 OpenType 彩色字体

如前所述，基于存储字体字形所用数据格式的不同，OpenType 彩色字体分为两类：基于矢量和基于光栅。

#### XeTeX 与基于光栅的 OpenType 彩色字体

XeTeX 不能加载基于光栅的 OpenType 彩色字体——例如 Google 的 [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) ，它随 TeX Live 2020 提供。例如，如果你尝试加载 Noto Color Emoji（NotoColorEmoji.ttf），XeLaTeX 将会失败，并给出一个可能具有误导性的错误，声称找不到 Noto Color Emoji“cannot be found”。以下使用 XeLaTeX 排版的 LaTeX 代码， *无法工作*:

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont{NotoColorEmoji.ttf}
\newcommand{\smiley}{{\emojifont\char"1F600}}
\smiley
\end{document}
```

[在 Overleaf 中打开这段 XeLaTeX 代码（它 ***不会*** 能工作）。](https://www.overleaf.com/docs?engine=xelatex\&snip_name=XeTeX+failure\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%7BNotoColorEmoji.ttf%7D%0A%5Cnewcommand%7B%5Csmiley%7D%7B%7B%5Cemojifont%5Cchar%221F600%7D%7D%0A%5Csmiley%0A%5Cend%7Bdocument%7D)

它会以如下错误失败：

```
! Package fontspec Error: The font "NotoColorEmoji" cannot be found.
```

类似地，一个由 XeTeX 处理的简单 Plain TeX 示例也会失败

```latex
\font\emojifont="[NotoColorEmoji.ttf]" at 12pt
\emojifont \char"1F600
\bye
```

[在 Overleaf 中打开这个 Plain TeX（XeTeX）示例（它 ***不会*** 能工作）。](https://www.overleaf.com/docs?engine=latex_dvipdf\&snip_name\[]=main.tex\&snip\[]=%25%5Ctitle%7Bdummy+title%7D%0A%5Cfont%5Cemojifont%3D%22%5BNotoColorEmoji.ttf%5D%22+at+12pt%0A%5Cemojifont+%5Cchar%221F600%0A%5Cbye\&snip_name\[]=readme\&snip\[]=This+project+uses+a+latexmkrc+file+to+run+xetex+not+xelatex\&snip_name\[]=latexmkrc\&snip\[]=%24latex+%3D+%27xetex%25O+%25S%27%3B+%23+to+use+the+xetex+engine\&main_document=main.tex)

这个 Plain TeX 示例报告了一个类似但不同的错误信息：

```
! Font \emojifont=[NotoColorEmoji.ttf] at 12.0pt not loadable: Metric (TFM) fil
e or installed font not found.
l.1 \font\emojifont="[NotoColorEmoji.ttf]" at 12pt

我无法读取此字体的尺寸数据，
因此我将忽略字体规范。
[Wizards can fix TFM files using TFtoPL/PLtoTF.]
你也许可以尝试插入不同的字体规范；
例如，输入 `I\font<same font id>=<substitute font name>'.
```

**Plain LuaHBTeX 示例**

作为比较，这里有一个用 LuaHBTeX 编译的最小 Plain TeX 示例

```latex
\input luaotfload.sty
\font\emojifont=NotoColorEmoji.ttf:mode=harf at 12pt
\emojifont \Uchar"1F600
\bye
```

[在 Overleaf 中打开这个 Plain TeX（LuaHBTeX）示例（它编译成功）。](https://www.overleaf.com/docs?engine=latex_dvipdf\&snip_name\[]=main.tex\&snip\[]=%25%5Ctitle%7BPlain+TeX+with+LuaHBTeX%7D%0A%5Cinput+luaotfload.sty%0A%5Cfont%5Cemojifont%3DNotoColorEmoji.ttf%3Amode%3Dharf+at+12pt%0A%5Cemojifont+%5CUchar%221F600%0A%5Cbye\&snip_name\[]=readme\&snip\[]=This+project+uses+a+latexmkrc+file+to+run+luahbtex+not+lualatex\&snip_name\[]=latexmkrc\&snip\[]=%24latex+%3D+%27luahbtex+%25O+%25S%27%3B+%23+to+use+the+luahbtex+engine\&main_document=main.tex)

#### XeTeX 失败的真正原因

XeTeX 提供的错误信息部分掩盖了问题的实际原因：OpenType 彩色字体，尤其是基于光栅的变体， *不* 受 XeTeX 支持。实际上，XeTeX（Kpathsea）可以 *找到* Noto Color Emoji 字体，但 XeTeX 不能完全 *加载* 该字体，并且无法初始化用于将该字体用于排版所需的内部字体数据表。内部上，XeTeX *开始* 在加载字体的过程中会测试其“scalability”（使用的是 FreeType 对“scalability”的定义），但该测试失败了，于是 XeTeX 会发出一个标准的、可以说具有误导性的 TeX 引擎错误信息。

**技术注释**

通过编译一个调试版的 XeTeX 可执行文件，研究了 XeTeX 对 NotoColorEmoji.ttf 的处理。使用 Eclipse IDE 为 XeTeX 函数设置了断点 `creatFontFromFile(filename, index, pointsize)`，然后逐步执行代码以观察后续处理过程。

#### XeTeX 和基于矢量的 OpenType 彩色字体

XeTeX 可以 *加载* 处理基于矢量的 OpenType 彩色字体，但不会在生成的 PDF 中产生彩色 emoji——如果 XeTeX 真的能生成 PDF 的话。不同于 LuaTeX、LuaHBTeX 和 pdfTeX，XeTeX 不会 *直接* 以 PDF 格式输出排版后的文档。相反，XeTeX 输出一个中间的 `.xdv` （e**x**xtended **dv**i）文件格式，然后由一个名为 `xdvipdfmx`的工具将其转换为 PDF。就撰写本文时而言， `xdvipdfmx` 无法将合适的彩色 emoji 字形数据嵌入到 PDF 中，因此最多你只能在 PDF 中看到单色 emoji——即“fallback”结果——或者也许什么都看不到，这取决于所使用的字体。

下面是一个使用 OpenType 彩色字体 [TwemojiMozilla.ttf](https://ctan.org/tex-archive/fonts/twemoji-colr)的 XeLaTeX 示例，它可在 TeX Live 中获得。TwemojiMozilla.ttf 使用微软的 COLR/CPAL 矢量格式来存储彩色字形，并随 TeX Live 2020 提供。在这个示例中，XeTeX 能够加载该字体，生成一个 `.xdv` 和 PDF 文件，但 emoji 字形并未出现在排版后的 PDF 中：

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont{TwemojiMozilla.ttf}
\newcommand{\smiley}{{\emojifont\char"1F600}}
这里有一个笑脸： \smiley
\end{document}
```

[在 Overleaf 中打开这段 XeLaTeX 代码（它无法工作）。](https://www.overleaf.com/docs?engine=xelatex\&snip_name=XeTeX+failure\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%7BTwemojiMozilla.ttf%7D%0A%5Cnewcommand%7B%5Csmiley%7D%7B%7B%5Cemojifont%5Cchar%221F600%7D%7D%0AHere+is+a+smiley%3A+%5Csmiley%0A%5Cend%7Bdocument%7D)

相反，如果定义 `\emojifont` 使用 `fontspec` 设置 `[Renderer=HarfBuzz]`:

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont{TwemojiMozilla.ttf}[Renderer=HarfBuzz]
\newcommand{\smiley}{{\emojifont\char"1F600}}
这里有一个笑脸： \smiley
\end{document}
```

[在 Overleaf 中打开这段 LuaLaTeX 代码（它可以工作）。](https://www.overleaf.com/docs?engine=lualatex\&snip_name=LuaLaTeX+emoji+example\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%7BTwemojiMozilla.ttf%7D%5BRenderer%3DHarfBuzz%5D%0A%5Cnewcommand%7B%5Csmiley%7D%7B%7B%5Cemojifont%5Cchar%221F600%7D%7D%0AHere+is+a+smiley%3A+%5Csmiley%0A%5Cend%7Bdocument%7D)

### LuaHBTeX 和 OpenType 彩色字体

借助其集成的 HarfBuzz shaping 引擎以及 `luaoftload` 库，LuaHBTeX 支持 OpenType 彩色字体的全部四种变体。LuaLaTeX 用户可以充分利用对包含 emoji 字符文本的基于 Unicode 的处理，或者简单地使用 OpenType 彩色字体为文档增添绚丽色彩。

如前所述，OpenType 彩色字体的四种变体可分为两组：

* 包含栅格图像格式字形的，例如 PNG；
* 其他使用 SVG 或微软 COLR/CPAL 机制的矢量格式。

基于矢量的字形格式的优点是可缩放性：在任何字号下都能生成清晰的字形图形。

**在 LuaHBTeX 中使用微软 COLR/CPAL 彩色字体**

如果你想为你的 OpenType 彩色 emoji 字体使用矢量格式，请查看字体 [TwemojiMozilla.ttf](https://ctan.org/tex-archive/fonts/twemoji-colr?lang=en)，它基于微软的 COLR/CPAL 格式。TwemojiMozilla.ttf 已包含在 TeX Live 中，但你可以从其 [GitHub 仓库](https://github.com/mozilla/twemoji-colr/releases) 并将其上传到你的 Overleaf 项目中。

这里有一个小的、 `fontspec`基于 的示例，使用 `Renderer=Harfbuzz`，它会排版出一个很大的（矢量）emoji 鸭子：

```latex
\documentclass{article}
\usepackage{fontspec}
\title{Duck demo}
\begin{document}
\newfontfamily\emojifont[Renderer=Harfbuzz,SizeFeatures={Size=400}]{TwemojiMozilla.ttf}
\emojifont\Uchar"1F986
\end{document}
```

[在 Overleaf 中打开这个 LuaLaTeX 示例，以排版一个矢量鸭子。](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Typesetting+an+emoji+duck\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Ctitle%7BDuck+demo%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%5BRenderer%3DHarfbuzz%2CSizeFeatures%3D%7BSize%3D400%7D%5D%7BTwemojiMozilla.ttf%7D%0A%5Cemojifont%5CUchar%221F986%0A%5Cend%7Bdocument%7D)

下面就是上面示例生成的（矢量）鸭子：

![](/files/ff893f538ac8569cf59d106f0efd54877106c5e8)

#### 使用基于 SVG 的 OpenType 彩色字体与 LuaHBTeX

在撰写本文更新版（2023 年 7 月）时，关于在 LuaLaTeX 中使用 SVG 风格的 OpenType 彩色字体，正式文档很少。一些 [在线讨论中的评论](https://github.com/latex3/luaotfload/issues/96) 建议使用 `fontspec`的节点列表中的以下内容： `RawFeature`，如下方伪代码所示。将 `*your SVG font file name here*` 替换为一个可被你的 LaTeX 代码访问的基于 SVG 的字体文件名：

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emoji[RawFeature={+svg},SizeFeatures={Size=20}]{your SVG font file name here}
\emoji Your emoji here...
\end{document}
```

如果你省略 `fontspec` 并直接加载 `luaotfload` ，你可能需要按如下方式声明并指定字体——我们的实验表明，为了使其工作，你需要省略 `mode=harf` 选项：

```latex
\font\emoji=[your SVG font file name here]:+svg;
```

**一些注意事项**

有兴趣使用 SVG 风格 OpenType 彩色字体的读者应注意：

* 包含大量字形的 SVG 风格 OpenType 字体可能会让 [LuaLaTeX 的处理在计算上代价很高](#processing-svg-glyph-data) ，这可能导致 [Overleaf 超时](/latex/zh-cn/zhi-shi-ku/038-fixing-and-preventing-compile-timeouts.md).
* LuaLaTeX 对这些字体的支持可能 [被视为实验性](https://github.com/latex3/luaotfload/issues/96#issuecomment-530317399)：结果可能因你的项目所使用的 TeX Live 版本而异；因此，建议你进行实验并谨慎行事。

**处理 SVG 字形数据**

SVG 使设计师能够创建复杂且多彩的设计来表示字体的字形——但需遵守某些 SVG 限制 [，这些限制已在 OpenType 规范中记录](https://docs.microsoft.com/en-us/typography/opentype/spec/svg)。然而，包括 LuaHBTeX 在内的 TeX 引擎不能直接导入（使用）SVG 文件或数据——例如用于描述 SVG 风格 OpenType 彩色字体中字形形状的 SVG 数据。字形的 SVG 数据必须转换为 PDF 格式，因为 LuaHBTeX 可以使用它来排版该字形并生成最终的 PDF 文档。该 SVG 到 PDF 的转换由其中的 Lua 代码处理 `luaoftload`：每个字形的 SVG 数据都会从字体文件中提取出来，保存到一个临时的 `.svg` 文件中，并通过命令行使用 Inkscape 转换为 PDF。提取 SVG 数据并将其转换为 PDF 会带来一些处理开销，从而可能导致较长的文档编译时间——尤其是使用包含数千个 emoji 字形的大型 SVG 字体的文档。

#### 基于栅格的 OpenType 彩色字体

**在 LuaHBTeX 中使用 Google 的 CBDT/CBLC OpenType 彩色字体格式**

[Noto Color Emoji](https://fonts.google.com/noto/specimen/Noto+Color+Emoji) 是 TeX Live 中包含的一个 OpenType 彩色字体，因此在 Overleaf 项目中很容易使用。由于 Noto Color Emoji 使用 PNG 格式图像来表示 emoji 字形，我们可以用它来排版一个很大的（栅格）鸭子 emoji——如下例所示。再次注意， `fontspec` 字体声明（`\emojifont`）使用 `Renderer=Harfbuzz`.

```latex
\documentclass{article}
\usepackage{fontspec}
\title{Duck demo}
\begin{document}
\newfontfamily\emojifont[Renderer=Harfbuzz,SizeFeatures={Size=400}]{NotoColorEmoji.ttf}
\emojifont\Uchar"1F986
\end{document}
```

[在 Overleaf 中打开这个 LuaLaTeX 示例，以排版一个栅格鸭子。](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Typesetting+a+large+raster+duck\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bfontspec%7D%0A%5Ctitle%7BDuck+demo%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewfontfamily%5Cemojifont%5BRenderer%3DHarfbuzz%2CSizeFeatures%3D%7BSize%3D400%7D%5D%7BNotoColorEmoji.ttf%7D%0A%5Cemojifont%5CUchar%221F986%0A%5Cend%7Bdocument%7D)

下面就是上面示例生成的栅格鸭子：

![由 LaTeX 排版的栅格鸭子 emoji](/files/34196509411999d0ef855276a29e91b1458e19c0)

如果你尝试使用 `NotoColorEmoji.ttf` 但省略 `[Renderer=Harfbuzz]` 来自 `fontspec` 声明，LuaHBTeX 将会失败，并在尝试写出 PDF 文件时给出错误信息：

```latex
! error:  (file /usr/local/texlive/2020/texmf-dist/fonts/truetype/google/noto-em
oji/NotoColorEmoji.ttf) (ttf): loca table not found
```

此错误的原因在于 [loca table](https://docs.microsoft.com/en-us/typography/opentype/spec/loca) 是 [在 GitHub 上已有解释](https://github.com/latex3/luaotfload/issues/98#issuecomment-531610153).

**在 LuaHBTeX 中使用苹果的 sbix OpenType 彩色字体格式**

离线测试表明 LuaHBTeX 支持 `sbix` 版 OpenType 彩色字体，但截至撰写本文时，我们还没能找到一个许可合适的 `sbix`变体彩色 emoji 字体来演示排版一只鸭子。如果你 [联系我们](https://www.overleaf.com/contact) 知道这样的字体，请告诉我们，我们会很快把本文更新为使用它。

## LuaHBTeX HarfBuzz API 简介

![Db.gif](/files/37cc1f2b45a72bae51ccc2d79a160c50fd2caf79) ![Db.gif](/files/37cc1f2b45a72bae51ccc2d79a160c50fd2caf79)

文本 shaping，尤其是针对复杂脚本语言，甚至 emoji，本质上是一项困难的任务，因此，HarfBuzz 是一个相当复杂的库，使用起来可能有些棘手——除非你已经熟悉文本 shaping 操作。在最后一节中，我们将介绍 LuaHBTeX 对 HarfBuzz 的集成，以及如何通过其中的 Lua 代码访问它 `\directlua`.

我们的示例使用相当基础的代码来演示 LuaHBTeX HarfBuzz API。它有些刻意设计，不属于生产级质量，也不太实用，因为它的唯一目的在于引入一些核心概念。我们已经把 Lua 代码分成两个 `\directlua` 块：第一个加载 `luaharfbuzz` 库，并创建一些全局变量，我们会在第二个 `\directlua` 块中使用它们，在那里我们定义一个名为 `\codestoemoji`.

复制 Knuth 使用双重危险弯折标志的做法似乎很合适（图片鸣谢于 [这个网站](http://www.truetex.com/db.htm)），因为这些内容在某种程度上比较底层，会“窥视内部”——尽管我们希望它能引起更勇敢读者的兴趣。LuaHBTeX 对 HarfBuzz 的集成源自 [GitHub 上的 luaharfbuzz 项目](https://github.com/ufyTeX/luaharfbuzz/wiki#projects-using-luaharfbuzz) ，在那里你可以找到 [项目简介](https://github.com/ufyTeX/luaharfbuzz/wiki) 以及一个 [luaharfbuzz API 列表](http://ufytex.github.io/luaharfbuzz/).

### 第一步：加载 luaharfbuzz 库并找到一个字体

要使用 LuaHBTeX 的 HarfBuzz API，我们首先需要加载名为 `luaharfbuzz`的库（模块），它内置于 LuaHBTeX 中，并将返回的表保存到一个（全局）变量中，我们把它称为 `hblib`:

```latex
hblib=require("luaharfbuzz")
```

接下来，我们需要找到一个合适的 emoji OpenType 彩色字体：我们将使用 Noto Color Emoji——注意，我们这里非常偷懒，没有做任何错误检查，以防我们找不到它！为了找到它，我们将使用 `kpse` （Kpathsea）库，它也是 LuaTeX/LuaHBTeX 的一部分：

```latex
pathtofontfile=kpse.find_file("NotoColorEmoji.ttf","truetype fonts")
```

现在我们已经可以通过变量 `hblib`访问 HarfBuzz 库，以及一个合适字体的路径（`pathtofontfile`），我们可以开始使用 `hblib`。首先，我们将创建一个 HarfBuzz font 和 HarfBuzz face，供第二个 `\directlua` 代码块中定义宏时使用。

```latex
% 从 Noto Color Emoji 创建 HarfBuzz face 和 HarfBuzz font
hbface = hblib.Face.new(pathtofontfile)
hbfont = hblib.Font.new(hbface)
```

#### HarfBuzz font 和 HarfBuzz face：它们是什么？

一个 [HarfBuzz face 对象](https://harfbuzz.github.io/fonts-and-faces.html) 表示从字体文件中加载的一个字形样式，但尚未设置特定参数（例如字号）。一个 [HarfBuzz font 对象](https://harfbuzz.github.io/fonts-and-faces.html) 表示 *HarfBuzz face 的一个特定实例* ；因此，可以从单个 HarfBuzz face 派生出不同的 HarfBuzz font 对象：每个 HarfBuzz font 都可以将其属性（例如大小）设为不同的值。HarfBuzz face 是比 HarfBuzz font 更高层级的抽象。

### 使用字体字形创建 PNG 文件

我们第一个 `\directlua` 块的最后部分是一个名为 `writePNGglyph(hbfontobject, glyphID)` 的函数，我们用它来演示某些 OpenType 彩色字体（例如 Noto Color Emoji）使用 PNG 图形来表示其包含的 emoji 字形。

此函数使用 LuaHBTeX 的 HarfBuzz API 从字形中提取 PNG 数据，并将该数据写入一个 `.png` 名为 `Graphics<glyphID>.png`。该 `.png` 文件名会被返回，供 `\includegraphics` 用于在我们排版的 PDF 中嵌入 PNG 字形图像。

有了 `writePNGglyph(hbfontobject, glyphID)` ，我们的第一个 `\directlua` 代码块看起来如下：

```latex
\directlua{

% 从 LuaHBTeX 加载 luaharfbuzz 库
hblib=require("luaharfbuzz")

% 在 Overleaf 的服务器上定位 Noto Color Emoji 字体
pathtofontfile=kpse.find_file("NotoColorEmoji.ttf","truetype fonts")

% 从 Noto Color Emoji 创建 HarfBuzz face 和 HarfBuzz font
hbface = hblib.Face.new(pathtofontfile)
hbfont = hblib.Font.new(hbface)

% 此函数接受一个字体和一个字形 ID：
% 它提取字形的 PNG 数据并将其写入
% 将其输出为一个 .png 文件

function writePNGglyph(hbfontobject, glyphID)

    % 获取字形 PNG 数据
    local pngblob=hbfontobject:ot_color_glyph_get_png(glyphID)
    local pngdata=pngblob:get_data()

    % 为我们的 .png 文件构造一个文件名
    local fname="Glyph"..glyphID..".png"

    % 写出 .png 文件并返回文件名
    local output = assert(io.open(fname, "wb"))
    output:write(pngdata)
    output:close()

    % 返回供 \includegraphics 使用的文件名
    return fname
end
}
```

### 第二个 \directlua 块：创建宏 \codestoemoji

目标是定义一个宏 `\codestoemoji` ，我们可以用包含 emoji 字符代码的一段文本来调用它，让 HarfBuzz 对其进行 shaping。具体来说，我们将使用 `\Uchar<字符代码>` 来表示每个 emoji 字符；例如：

```latex
\codestoemoji{\Uchar"1F3F4\Uchar"E0067\Uchar"E0062\Uchar"E0065\Uchar"E006E\Uchar"E0067\Uchar"E007F}
```

在 `\codestoemoji` 的定义中有很多内容，我们将在下面解释，但其定义看起来像这样：

```latex
\newcommand{\codestoemoji}[1]{%
\directlua{

local str="#1"
local hbbuffer = hblib.Buffer.new()
hbbuffer:add_utf8(str)

hbbuffer:set_direction(hblib.Direction.new("ltr"))
local res = hblib.shape_full(hbfont, hbbuffer, {},{})

if (res) then
    local hbglyphs=hbbuffer:get_glyphs()
    % 字形表 hbglyphs 是从 1 开始编号的
    local i = 1
    while hbglyphs[i] \noexpand~= nil do
        local glyph = hbglyphs[i]
        i = i + 1
        local fname=writePNGglyph(hbfont, glyph.codepoint)
        % 缩小导入的 PNG 图像尺寸
        local s = 0.75
        local scal="[scale="..tostring(s).."]"
        tex.print([[\noexpand\includegraphics]]..scal..[[{]]..fname..[[}]])
     end
end
}}
```

#### 理解宏 \codestoemoji 的定义

该 `\codestoemoji` 该宏大多是包含在 `\directlua`中的 Lua 代码，所以如果你想进一步了解 *如何* `\directlua` 是如何工作的，请查看 Overleaf 文章 [理解 `\directlua`](/latex/zh-cn/shen-du-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md)。它解释了当 Lua 代码中包含 TeX/LaTeX 命令时，LuaTeX 和 LuaHBTeX 如何处理 `\directlua` ，以及尤其需要使用 `\noexpand` 和 `\unexpanded`.

**处理宏参数：“#1”**

宏以这三行代码开头：

```latex
local str="#1"
local hbbuffer = hblib.Buffer.new()
hbbuffer:add_utf8(str)
```

它们执行以下任务：

* `local str="#1"`：这会根据宏传入的输入创建一个 Lua 字符串；
* `local hbbuffer = hblib.Buffer.new()`：这会使用 HarfBuzz API 创建一个缓冲区，用于保存我们希望 HarfBuzz 进行 shaping 的文本；
* `hbbuffer:add_utf8(str)`：这会将由宏输入创建的 UTF-8 格式字符串添加到 HarfBuzz 缓冲区中。

第一行代码

```latex
local str="#1"
```

看起来相当直接，但其操作实际上相当复杂，值得更详细地探究。

如果我们考虑第三行代码

```latex
hbbuffer:add_utf8(str)
```

我们会看到它使用了我们的 `str` 变量，为 HarfBuzz 缓冲区提供一个以 UTF-8 格式编码的 Unicode 字符串。为此，变量 `str` 本身必须包含以 UTF-8 格式编码的 Unicode 文本；于是问题来了： *如何* LuaHBTeX 是否把宏参数 `"#1"`，其中包含 `\Uchar` 命令，转换成了 Lua 字符串变量 `str` ，以便为 HarfBuzz 提供 UTF-8 文本？

如果我们看看对 `\codestoemoji` 宏的预期用法：

```latex
\codestoemoji{\Uchar"1F3F4\Uchar"E0067\Uchar"E0062\Uchar"E0065\Uchar"E006E\Uchar"E0067\Uchar"E007F}
```

输入，例如 `\Uchar"1F3F4\Uchar"E0067\Uchar"E0062\Uchar"E0065...`，看起来完全不像是一个以 UTF-8 编码的 emoji 字符序列。此外，HarfBuzz 对 TeX 命令一无所知。不知怎么地，包含 `\Uchar` 命令的原始 TeX 输入会被转换为 HarfBuzz 可以使用的、以 UTF-8 编码的 Unicode 字符，但 *如何*?

答案在于 `\Uchar` 命令的行为：尝试调用 `\codestoemoji` 使用 `\char` 而不是 `\Uchar` 将会失败，但 *为什么*?

**\Uchar：\directlua 中的展开**

当 `\codestoemoji` 宏被调用时，存储在宏定义中的 `\directlua` 命令必须为发送到 LuaHBTeX 内置 Lua 解释器的 Lua 代码做准备。该代码准备过程的一部分，是展开原始 Lua 代码中出现的任何 TeX/LaTeX 命令，以及用户提供的任何宏参数。这个展开过程会生成一个标记列表，随后又被转换回文本，从而生成要传递给 Lua 解释器的 Lua 代码。为了方便起见，我们从 Overleaf 文章中重现一幅图 [理解 `\directlua`](/latex/zh-cn/shen-du-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md):

![\directlua 的机制](/files/b579751b07334a894e40a5375a2562e877a14f10)

宏 `\codestoemoji` 旨在通过 `\Uchar` 命令调用，并且， [如本文前面所述](#the-key-difference-expansion), `\Uchar` 是一个可展开命令，其展开会生成一个字符标记。在 `\directlua`的处理活动中，LuaHBTeX 会展开每一个 `\Uchar<字符代码>` 命令，并用相应的展开值替换它：一个表示 *移除* 每次 `\Uchar<字符代码>` 从输入中 *将其替换为* 的字符标记 `<字符代码>`.

在处理的最后阶段，由 `\directlua` 生成的初始标记列表会被转换 *回文本* ，以成为将要交给 Lua 解释器的 Lua 代码（见上图）。由 `\Uchar` 展开产生的所有字符标记也都会 *转换回文本*：字符标记到文本的这种转换生成了原始 `<字符代码>` 值的 UTF-8 表示。

在我们的示例中，当 Lua 代码生成并准备好交给 Lua 解释器时，"#1" 的宏输入已被转换为一个 UTF-8 文本序列：该 `str` 变量现在是一个 UTF-8 文本字符串，可以安全地添加到 HarfBuzz 缓冲区中。

**为什么 \char 不起作用？**

直接的答案是因为 `\char` 是 *不* 一个可展开的命令。不同于 `\Uchar` 命令， `\char` 命令 *不会被移除* 在输入中于 `\directlua`的初始处理过程中生成一个标记列表时，它们会“穿过”并被并入由 `\directlua`构建的标记列表中。例如，如果 `\codestoemoji` 的参数包含 `\char"1F3F4` ，LuaHBTeX 会把它转换为一串标记，并将它们作为正在生成的总标记列表的一部分存储起来。

在下一阶段的处理——将标记转换回文本——中，生成的 Lua 代码会包含 *字面字符串* `\char"1F3F4` ，位于用于定义我们变量的文本中 `str`。当 `str` 的内容被添加到 HarfBuzz 缓冲区时，它不会包含表示表情符号字符“1F3F4”的 UTF-8 编码序列，而是会包含字面字符串 `\char"1F3F4`，HarfBuzz 会尝试对其进行字形排版，而就我们的目的而言，它不会生成表情符号字形。顺便说一下，字符串 `\char"1F3F4` 如果不是以“长括号字符串”创建，也会产生 Lua 语法错误——有关该问题的背景，请参见 [什么是 Lua 转义序列](/latex/zh-cn/shen-du-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md#what-are-e2809clua-escape-sequencese2809d3f) 。

如果我们尝试使用 `\codestoemoji` 替换为一个 `\char` 命令，就像这样：

```latex
\codestoemoji{\char"1F3F4\Uchar"E0067\Uchar"E0062\Uchar"E0065\Uchar"E006E\Uchar"E0067\Uchar"E007F}
```

LuaHBTeX 会失败并报告类似这样的语法错误：

```latex
[\directlua]:1: 在 '"\c' 附近出现无效的转义序列。
\codestoemoji ...ing \includegraphics }.}]]) end }

l.75 ...r"E0065\Uchar"E006E\Uchar"E0067\Uchar"E007F}

lua 解释器遇到了问题，因此
这个 lua 代码块的其余部分将被忽略。
```

#### 调用 HarfBuzz 排版函数

**设置缓冲区参数**

HarfBuzz 有时需要关于它被要求处理的文本的额外信息。你可以通过配置你的 `<buffer variable>` 使用 *缓冲区方法*来提供这些信息，例如：

* `<buffer variable>:set_direction(*HarfBuzz direction*)`;
* `<buffer variable>:set_language(*HarfBuzz language*)`;
* `<buffer variable>:set_script(*HarfBuzz script*)`.

例如，我们需要告诉 HarfBuzz，我们的表情符号文本方向是从左到右。为此，我们在我们的 `set_direction()` 方法上使用 `<buffer variable>` （称为 `hbbuffer`）并写成：

```latex
hbbuffer:set_direction(hblib.Direction.new("ltr"))
```

其中 `hblib.Direction.new("ltr")` 创建了一个适合通过 Lua 传递给 HarfBuzz 引擎的“方向对象”。

**执行排版**

在缓冲区正确初始化后，我们可以通过函数 `shape_full()`来请求 HarfBuzz 执行实际的排版。在我们的示例中，我们写：

```latex
local res = hblib.shape_full(hbfont, hbbuffer, {},{})
```

的第 3 和第 4 个参数 `shape_full()` 函数需要是 Lua 表——我们为这两个参数都使用了空表“`{}`”。 `shape_full()` 的一般形式是：

```latex
shape_full(Harfbuzz 字体, Harfbuzz 缓冲区, {字体特性}, {"shaper"}
```

* **`{"shaper"}`**：通常不需要设置，但可选项有 `{"ot"}` 或 `{"graphite2"}`。关于“shaper”概念的更多信息可参见 [HarfBuzz 文档](https://harfbuzz.github.io/shaping-and-shape-plans.html)——请注意，这里文档说明的是底层 C API，而不是基于 Lua 的 `luaharfbuzz` 绑定（实现）。
* **`{font features}`**：这是一个列出 [OpenType 特性](https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist)——由字体支持的——你希望 HarfBuzz 在排版过程中应用的特性。

任何你想使用的字体特性都需要用一个 `luaharfbuzz` 库函数

```latex
library_instance.Feature.new(feature_string)
```

其中

* `library_instance` 是你的 `luaharfbuzz` 库实例变量（`hblib` 在我们的示例中）；
* `feature_string` 使用一种 [语法来定义特性](https://github.com/ufytex/luaharfbuzz/wiki/Feature-Strings)。其示例包括 `+smcp` 用于启用小型大写字母，或 `-kern` 用于禁用字偶距调整。

例如：

```latex
local dosmcp = hblib.Feature.new("+smcp")
local nokern = hblib.Feature.new("-kern")
% 像这样使用你的字体特性
local res = hblib.shape_full(hbfont, hbbuffer, {dosmcp,nokern},{})
```

#### 访问结果：获取字形

最后，如果排版操作成功，排版后的字形会返回到我们在代码中前面创建的缓冲区变量 `hbbuffer` 中。

我们通过缓冲区方法 `get_glyphs()` 来访问这些字形，并使用循环逐个获取每个字形。请注意，保存字形的 Lua 表 `hbglyphs` 在我们的示例中是从 1 开始索引的，而不是 0。

每个字形的 *字形标识符* （令人困惑地称为 `码位`），以及 HarfBuzz 字体（`hbfont`）会被传递给 `writePNGglyph()` 函数，该函数使用该字形在字体中的栅格图像表示来创建一个 PNG 文件。

`writePNGglyph()` 写出一个 PNG 文件并返回 PNG 文件名，然后该文件名被用于通过 `\includegraphics[scale=0.75]{<fname>}`将（缩放后的）PNG 文件导入到我们的 LaTeX 文档中。注意我们如何可以在 Lua 代码中直接使用 `\includegraphics` 。

```latex
if (res) then
    local hbglyphs=hbbuffer:get_glyphs()
    % 字形表 hbglyphs 是从 1 开始编号的
    local i = 1
    while hbglyphs[i] \noexpand~= nil do
        local glyph = hbglyphs[i]
        i = i + 1
        local fname=writePNGglyph(hbfont, glyph.codepoint)
        % 缩小导入的 PNG 图像尺寸
        local s = 0.75
        local scal="[scale="..tostring(s).."]"
        tex.print([[\noexpand\includegraphics]]..scal..[[{]]..fname..[[}]])
     end
end
```

### 你可以在 Overleaf 中打开的完整代码

```latex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\directlua{

% 从 LuaHBTeX 加载 luaharfbuzz 库
hblib=require("luaharfbuzz")

% 在 Overleaf 的服务器上定位 Noto Color Emoji 字体
pathtofontfile=kpse.find_file("NotoColorEmoji.ttf","truetype fonts")

% 从 Noto Color Emoji 创建 HarfBuzz face 和 HarfBuzz font
hbface = hblib.Face.new(pathtofontfile)
hbfont = hblib.Font.new(hbface)

% 此函数接受一个字体和一个字形 ID：
% 它提取字形的 PNG 数据并写入
% 将其输出为一个 .png 文件

function writePNGglyph(hbfontobject, glyphID)

    % 获取字形 PNG 数据
    local pngblob=hbfontobject:ot_color_glyph_get_png(glyphID)
    local pngdata=pngblob:get_data()

    % 为我们的 .png 文件构造一个文件名
    local fname="Glyph"..glyphID..".png"

    % 写出 .png 文件并返回文件名
    local output = assert(io.open(fname, "wb"))
    output:write(pngdata)
    output:close()

    % 返回供 \includegraphics 使用的文件名
    return fname
end
}

\newcommand{\codestoemoji}[1]{%
\directlua{

local str="#1"
local hbbuffer = hblib.Buffer.new()
hbbuffer:add_utf8(str)

hbbuffer:set_direction(hblib.Direction.new("ltr"))
local res = hblib.shape_full(hbfont, hbbuffer, {},{})

if (res) then
    local hbglyphs=hbbuffer:get_glyphs()
    % 字形表 hbglyphs 是从 1 开始索引的。
    local i = 1
    while hbglyphs[i] \noexpand~= nil do
        local glyph = hbglyphs[i]
        i = i + 1
        local fname=writePNGglyph(hbfont, glyph.codepoint)
        % 缩小导入的 PNG 图像尺寸
        local s = 0.75
        local scal="[scale="..tostring(s).."]"
        tex.print([[\noexpand\includegraphics]]..scal..[[{]]..fname..[[}]])
     end
end
}}

一只鸭子：\codestoemoji{\Uchar"1F986}

一面旗帜：\codestoemoji{\Uchar"1F3F4\Uchar"E0067\Uchar"E0062\Uchar"E0065\Uchar"E006E\Uchar"E0067\Uchar"E007F}
\end{document}
```

[在 Overleaf 中打开这个 luaharfbuzz API 示例。](/latex/zh-cn/shen-du-wen-zhang/10-an-overview-of-technologies-supporting-the-use-of-colour-emoji-fonts-in-latex.md)

此示例生成如下输出：

![Harfbuzzexample.png](/files/d137fa97f721cc0d7bae3debcd46aefab5614887)

## 加餐部分：有趣的表情符号数学

为了以轻松的方式收尾，Overleaf 团队的一名成员使用了 [`emoji` LaTeX 宏包](https://ctan.org/pkg/emoji?lang=en) 来创建一个有趣的示例：

```latex
\documentclass{article}
\usepackage{emoji}
\usepackage{unicode-math,fontspec}
\setmainfont{STIX}
\setmathfont{STIX Two Math}
\begin{document}
\newcommand{\emomath}[1]{\text{\emoji{#1}}}
\[
e^{\emomath{droplet} \ln\emomath{smile}}=\emomath{sweat-smile}
\]
\[
e^{\emomath{eye}\emomath{pie}}=-1
\]
\end{document}
```

[在 Overleaf 中打开这个有趣的示例](https://www.overleaf.com/docs?engine=lualatex\&snip_name=Fun+with+emoji+math\&snip=%5Cdocumentclass%7Barticle%7D%0A%5Cusepackage%7Bemoji%7D%0A%5Cusepackage%7Bunicode-math%2Cfontspec%7D%0A%5Csetmainfont%7BSTIX%7D%0A%5Csetmathfont%7BSTIX+Two+Math%7D%0A%5Cbegin%7Bdocument%7D%0A%5Cnewcommand%7B%5Cemomath%7D%5B1%5D%7B%5Ctext%7B%5Cemoji%7B%231%7D%7D%7D%0A%5C%5B%0Ae%5E%7B%5Cemomath%7Bdroplet%7D+%5Cln%5Cemomath%7Bsmile%7D%7D%3D%5Cemomath%7Bsweat-smile%7D%0A%5C%5D%0A%5C%5B%0Ae%5E%7B%5Cemomath%7Beye%7D%5Cemomath%7Bpie%7D%7D%3D-1%0A%5C%5D%0A%5Cend%7Bdocument%7D)

此示例生成如下输出：

![Emojimath2.png](/files/af3269c9208f09c5953a42a0d9225be43b1a8341)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/10-an-overview-of-technologies-supporting-the-use-of-colour-emoji-fonts-in-latex.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
