> For the complete documentation index, see [llms.txt](https://overleaf-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://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-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月修订，以更新关于 [在 LuaHBTeX 中使用基于 SVG 的 OpenType 彩色字体](#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. 使用标准 LaTeX 图形工具，如 TikZ、MetaPost 或 Asymptote 来绘制 emoji。
2. 插入存储在外部文件中的预先准备好的 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) 以及 [Wikipedia 条目](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，它们于 20 世纪 90 年代末在日本演变而来。令人不意外的是， [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) 或文章 [我也赞同那个 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 才被正式视为 *字符* 独立的字符 [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"<code point>` 或 `\Uchar"<code point>` （见 [本文的这一节](#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`——使用一个能够对其进行排版的字体。

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

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emojifont[Renderer=Harfbuzz]{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**

EmojiSequenceChart.png `\char<character code>` 命令，用于使用当前字体排版特定的 `<character code>`之外，LuaTeX、LuaHBTeX 和 XeTeX 引擎还提供 `\Uchar<character code>` 命令。从用户角度看，二者的结果 `\char` 和 `\Uchar` 看起来相同，但这些命令的工作方式有细微差别，下面我们会说明。

**关键区别：展开**

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

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

1. 该 `\Uchar<character code>` 命令是 *展开*，而 `<character code>` 被转换为一个临时记号列表，其中包含一个单独的 [。通过计算这个会传递给 TeX 内部处理的记号值，该特定](/latex/zh-cn/shen-ru-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md#tex-tokens-101-28and-notions-of-expansion29) 它表示 `<character code>`.
2. 这个单字符记号列表现在 *可供使用* ，作为 TeX 引擎下一条输入的来源。实际上，TeX 引擎会“暂时移开视线”，把这个单记号列表当作下一条输入项（记号）所在的位置。默认情况下，TeX 引擎只是回去读取（输入）那个记号并排版相应字符，从而再现 `\char` 命令。 **然而**，因为那个 `<character code>` 并没有立即被排版，而是临时 *被存储* （保存）为一个单个记号，原始 TeX 命令或 LaTeX 宏可以利用（吸收）这个记号——它不必立刻被排版，而可以在后续处理时按需使用。

实际上， `\char<character code>` 说的是“立即排版这个 `<character code>` ”，而 `\Uchar<character code>` 则具有一种“延迟动作”，它创建并存储一个字符记号，并将其作为下一条输入项（一个记号）提供。该记号既可以被 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 还需要额外的软件组件，其任务包括：

* 对 Unicode 编码文本进行预处理（“[整形](#the-concept-of-text-shaping)”）， *为* 使用特定字体进行显示做好准备；
* *将字体彩色 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 字符序列可以通过一种称为 *字符* 的过程“合并”，生成一个单一的结果（“复合”）emoji 字形——该单个字形会被设备的操作系统用来表示文本中原始的字符序列。 [文本整形](#the-concept-of-text-shaping) 连字

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 Sequences](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) （表情修饰符 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 *Format*。”

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

UTF-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 零宽连接符（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 风格排版，或显示在设备屏幕上时，结果是一个复杂的二维字形排列。 [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 技术。

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

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

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

\vskip 75bp

% 教育
\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 字体中，连同 *字形间* 定位数据。该定位数据与 *塑形后字形的相对位置*有关；它并不指代排版页面或其他媒介/内容中的绝对位置，例如网页、推文等。渲染软件（排版引擎、网页浏览器等）使用字形间定位信息，以确保字形在组合并纳入最终输出后彼此之间的位置正确。

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

在内部，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 中）的字体内的 cmap 表。

{% 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 绘制，支持由矢量构成的字形 *以及光栅图像*。另见 [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 中描述的其他表情符号文本处理能力 [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 的引擎中，而是开放 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 文档导言区：

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

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

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

到你的纯 TeX 文档中。

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

### LuaHBTeX：文本塑形的新选项

`luaotfload` 是一个成熟而强大的 Lua 库，它提供 LuaTeX 对 OpenType 字体的处理，以及针对一系列语言和脚本的文本塑形服务。最初， `luaotfload` 的文本塑形功能是用纯 Lua 代码实现的，但 TeX Live 2020 的发布带来了另一个主流文本塑形选项——一个新的基于 LuaTeX 的引擎，称为 LuaHBTeX。

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` 通过其“`模式`”参数提供对这两种塑形系统的访问——不过大多数用户会使用等效的 `fontspec` “`Renderer`”选项，而不是直接使用 `luaotfload`.

的低级函数。 `luaotfload`的每种文本塑形解决方案都有其优点和（当前的）弱点，但你应该使用哪一种，以及何时使用？这里有几点可供考虑：

* `luaotfload`的原生基于节点的处理可能占用大量内存，尤其是对于大型 CJK OpenType 字体。对 CJK 文本使用 HarfBuzz 进行塑形可以提高速度并减少内存使用。
* 对于复杂脚本应使用 HarfBuzz，因为它“……极大改善了印地语和阿拉伯语脚本的渲染，并强烈推荐用于此类脚本。”（见 `luaotfload` 手册）。
* HarfBuzz 被集成到 `luaotfload` 中仍然相对较新，并且仍在进一步开发中。在写作时（2021 年 7 月），建议主文档字体使用 luaotfload 内置的塑形（设置 `mode=node`），尤其是如果你的文档使用拉丁脚本。参见这个 [GitHub 问题](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 字体的默认“模式”——这使用 `luaotfload`用纯 Lua 实现的文本塑形函数。
* `Renderer = Harfbuzz`：此“模式”将字体定义/加载为供 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。“以下 LaTeX 代码使用 XeLaTeX 排版， *不起作用*:

```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

我无法读取此字体的尺寸数据，
因此我将忽略该字体规范。
[向导可以使用 TFtoPL/PLtoTF 修复 TFM 文件。]
你可以尝试插入不同的字体规范；
例如，输入 `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 内部 *开始* 对字体加载过程进行检查，并测试其“可缩放性”（使用 FreeType 对“可缩放性”的定义），但该测试失败，XeTeX 因此发出一条标准的、但可以说有些误导性的 TeX 引擎错误信息。

**TeX 技术注**

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

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

XeTeX 可以 *加载* 处理基于矢量的 OpenType 彩色字体，但不会在生成的 PDF 中产生彩色表情符号——如果 XeTeX 最终能生成 PDF 的话。与 LuaTeX、LuaHBTeX 和 pdfTeX 不同，XeTeX 并不 *直接* 以 PDF 格式输出排版文档。相反，XeTeX 输出一个中间的 `.xdv` （e**x**延 **dv**i）文件格式，该格式由一个名为 `xdvipdfmx`的工具转换为 PDF。在撰写本文时， `xdvipdfmx` 无法将适当的彩色表情符号字形数据嵌入到 PDF 中，因此，最多只能在 PDF 中看到单色表情符号——“回退”结果——或者在某些情况下根本看不到任何内容，这取决于所使用的字体。

下面是一个使用 OpenType 彩色字体 [TwemojiMozilla.ttf](https://ctan.org/tex-archive/fonts/twemoji-colr) 的 XeLaTeX 示例，它可在 TeX Live 中使用。TwemojiMozilla.ttf 使用微软的 COLR/CPAL 矢量格式来存储彩色字形，并随 TeX Live 2020 一起提供。在这个示例中，XeTeX 能够加载字体，生成一个 `.xdv` 以及 PDF 文件，但排版后的 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 排版引擎以及 `luaoftload` 库，LuaHBTeX 支持四种 OpenType 彩色字体的全部变体。LuaLaTeX 用户可以充分利用对包含表情符号字符的文本进行基于 Unicode 的处理，或者只是使用 OpenType 彩色字体为文档增添丰富多彩的效果。

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

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

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

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

如果你想为你的 OpenType 彩色表情符号字体使用矢量格式，可以看看字体 [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` 的示例，它会排版出一只大型（矢量）表情鸭：

```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)

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

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

```latex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
\newfontfamily\emoji[RawFeature={+svg},SizeFeatures={Size=20}]{your SVG font file name here}
\emoji 你的表情符号在这里...
\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 会带来一定的处理开销，可能导致文档编译时间较长——尤其是那些使用包含数千个表情符号字形的大型 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 格式图形来表示表情符号字形，我们可以用它来排版一个大型（光栅）鸭子表情符号——如下例所示。再次注意， `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 排版的光栅鸭子表情符号](/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 表](https://docs.microsoft.com/en-us/typography/opentype/spec/loca) 是 [中的原因已在 GitHub 上说明](https://github.com/latex3/luaotfload/issues/98#issuecomment-531610153).

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

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

## LuaHBTeX HarfBuzz API 简介

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

文本排版，尤其是复杂文字脚本语言的排版，甚至表情符号的排版，本质上都是一项困难的任务，因此，HarfBuzz 作为一个复杂的库，使用起来并不轻松——除非你已经熟悉文本排版操作。在最后这一节中，我们会看看 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")
```

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

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

现在我们已经通过变量 `hblib`访问到了 HarfBuzz 库，以及一个合适字体的路径（`pathtofontfile`），我们就可以开始使用 `hblib`。首先，我们将创建一个 HarfBuzz face 和 HarfBuzz font，用于第二个 `\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 图形来表示其包含的表情符号字形。

该函数使用 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` ，我们可以用它传入一段包含我们希望 HarfBuzz 进行排版的表情符号字符代码的文本。具体来说，我们将使用 `\Uchar<character code>` 来表示每个表情符号字符；例如：

```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-ru-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md)。它解释了当 Lua 代码中包含 TeX/LaTeX 命令时，LuaTeX 和 LuaHBTeX 如何进行处理，尤其是为何需要使用 `\directlua` 处理宏参数："#1" `\noexpand` 和 `\unexpanded`.

**这个宏以以下三行开头：**

它们执行以下任务：

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

：这会根据宏传入的输入创建一个 Lua 字符串；

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

第一行代码

```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 编码的表情符号字符。此外，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-ru-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<character code>` 命令，并将 *从当前输入中移除* 每次 `\Uchar<character code>` 从输入中移除，并 *将其替换为* 其替换为相应的展开值：一个表示 `<character code>`.

的字符令牌 `\directlua` 在处理的最后阶段，由 *生成的最初令牌列表会被* 转换回文本 `\Uchar` ，以成为将传给 Lua 解释器的 Lua 代码（见上图）。通过展开 *而产生的所有字符令牌也都会被*转换回文本 `<character code>` ：这种将字符令牌转换为文本的过程会生成原始

值的 UTF-8 表示。 `str` 在我们的示例中，当 Lua 代码生成并准备好交给 Lua 解释器时，"#1" 的宏输入已经被转换为一串 UTF-8 文本：此时

**变量现在是一个 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-ru-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 font, Harfbuzz buffer, {font features}, {"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。

每个字形的 *字形标识符* （令人困惑地称为 `codepoint`），以及 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-ru-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://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-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.
