三个现代引擎里,只有 XeTeX 不写 PDF。 排版结束后它吐出的是一串叫 .xdv(extended DVI)的字节,再由另一个程序 xdvipdfmx 把它转成 PDF。在用户看来 xelatex document.tex 一步到位,内部却分成两段。知不知道这一点,会直接改变你在图像或字体嵌入出问题那天读日志的方式。本页讲 XeTeX 给 LaTeX 带来的正题——直接读 UTF-8、用 fontspec 按名字调用操作系统里已装好的字体——以及 .xdv 到底是什么,还有把 pdfLaTeX 的稿子搬到 XeLaTeX 时究竟哪里会坏。
XeTeX 是谁、为了什么写的
起点是 Jonathan Kew 在 SIL International 工作期间写下的程序,首次公开发布是 2004 年 4 月,且仅限 Mac OS X。SIL 从事全球少数语言的文字与正字法工作,需求从一开始就摆在那里:用 Unicode 排版,并使用为这些语言真正制作的字体。因此早期 XeTeX 建立在当时 Mac 的排版技术 AAT(Apple Advanced Typography) 之上。2006 年移植到 Linux,随后是 Windows,自 TeX Live 2007 起随各平台一同发行。在 TeX Live 2024 上,xetex --version 打印的版权行写着 SIL International, Jonathan Kew and Khaled Hosny,接着列出它链接的库:ICU 74.2、HarfBuzz 8.3.0、Graphite2 1.3.14、FreeType2 2.13.2,在 macOS 上还有 Core Text 与 Cocoa 框架。理解 XeTeX 最好的方式,是把它看作把 TeX 接到操作系统与字体工业实际产物上的那一层。
版本号里也藏着一个小故事。xetex --version 返回 XeTeX 3.141592653-2.6-0.999996 (TeX Live 2024)。开头的 3.141592653 是 Knuth 的 TeX:他每修一次 bug 就多加一位数字,使它趋近 π,并写明「在我死后要做的绝对最后一次改动」是把版本号定为 π 本身,届时所有残留的 bug 一律变成特性。pdfTeX 与 LuaTeX 都继承了这串数字,所以三个引擎共享同一个前缀。末尾的 0.999996 属于 XeTeX 自己,随附的 NEWS 显示它每年二月前进一步,从 2019 年的 0.999991 走到 2024 年 2 月的 0.999996——一路添 9,始终停在 1 的前面。不过并没有文献声明它「要收敛到 1」,所以这只当作可观察的事实来记。
按名字调用系统字体:fontspec 与 \setmainfont
载入 \usepackage{fontspec},写 \setmainfont{Helvetica Neue},用操作系统认识的名字,整个过程就到此为止。不用做 TFM,也不用往 map 文件里加行。把它交给 xelatex 编译,再用 pdffonts 一看,就能确认 HelveticaNeue 以 CID TrueType 的形式被嵌入。正文、无衬线、等宽分别是 \setmainfont、\setsansfont、\setmonofont;想为标题另立一族,用 \newfontfamily\headingfont{...}。但随 TeX Live 附带的字体是另一回事,多数人第一次卡住就卡在那里。下一节专门讲它。
\documentclass{article}
\usepackage{fontspec} % no inputenc, no fontenc needed
% An OS font is named the way the system knows it, e.g.
% \setmainfont{Helvetica Neue}
% A font that ships with TeX Live is safest named by file name:
\setmainfont{texgyretermes-regular.otf}[
BoldFont = texgyretermes-bold.otf,
ItalicFont = texgyretermes-italic.otf,
BoldItalicFont = texgyretermes-bolditalic.otf,
]
\setsansfont{texgyreheros-regular.otf}
\setmonofont{texgyrecursor-regular.otf}
\newfontfamily\headingfont{texgyreadventor-regular.otf}
\begin{document}
Unicode goes in literally: naïve, Straße, ¿cómo?, œuvre.
\textbf{Bold} and \textit{italic} come from the files named above.
\[ E = mc^2 \]
\end{document}这里的关键在于你不写什么:inputenc 与 fontenc 都不载入。UTF-8 输入与 Unicode 字体是基线,pdfLaTeX 时代那些字符编码的咒语整套消失。找不到字体时的报错是 ! Package fontspec Error: The font "..." cannot be found.——一旦出现,首先该怀疑的不是 TeX,而是操作系统这一侧的名称解析。更细的控制项——Ligatures、Numbers=OldStyle、SmallCapsFeatures、用来传递原始 OpenType 标签的 RawFeature、以及选择整形器的 Renderer(HarfBuzz / AAT / Graphite)——属于 fontspec 的话题,留给它自己的页面。若连数学也想统一到 Unicode 字体,就再配上 unicode-math。
The font "..." cannot be found. — 附带字体按名字找不到时
XeTeX 拿名字去问的是操作系统的字体数据库,而不是 TeX Live 的目录。 这正是多数人第一次卡住的地方。在装好 TeX Live 2024 的 macOS 上原样尝试,\setmainfont{Helvetica Neue} 能用,而 \setmainfont{TeX Gyre Termes}、\setmainfont{Latin Modern Roman}、\setmainfont{TeX Gyre Pagella} 全都以 ! Package fontspec Error: The font "..." cannot be found. 失败;同样这三个在 LuaLaTeX 下却都能用。差别在于查找方式:LuaTeX 的 luaotfload 会扫描 TeX 的目录树本身并建立索引,而 XeTeX 是去问操作系统的字体机制(macOS 上是 Core Text),因此发行版自带、从未注册到系统里的字体,按名字是找不到的。
解决办法很简单:附带字体一律用文件名来叫。 \setmainfont{texgyretermes-regular.otf} 可以用——而且区分大小写,所以 TeXGyreTermes-Regular.otf 不行。用文件名指定会失去粗体与斜体的自动配对,因此要显式声明 BoldFont、ItalicFont、BoldItalicFont。上面的示例就是这种写法,在 xelatex 与 lualatex 下都是零报错、零缺字。另一条路是把发行版的字体注册进系统(例如 Linux 上的 texlive-fontconfig 配置),但这因机器而异,在合作者那里不会重现,所以要写进稿件的话,文件名指定更可靠。当有人报告「XeLaTeX 找不到字体」时,第一件事就是让他改用文件名试试。
看日文字体,这条规则会从反面显现出来。操作系统认识的名字能通,只有 TeX Live 认识的名字不能通。 在这台装了 TeX Live 2024 的 macOS 上实测,\setmainfont{Hiragino Mincho ProN}、\setmainfont{Hiragino Sans}、\setmainfont{YuMincho} 在 XeLaTeX 下都能编译,因为这些正是系统字体面板里显示的名字。反过来 \setmainfont{Noto Sans JP} 若没有装到系统层面,就会报 cannot be found。所以判断很简单:看这个字体是否出现在系统的字体列表里。出现,XeTeX 就找得到;不出现,哪怕 TeX Live 里有,按名字也够不着。至于需要竖排或严格禁则的日文稿件,luatexja 或 upLaTeX 仍比 xeCJK 顺手。
.xdv 究竟是什么——以及 XeTeX 为何不自己写 PDF
.xdv 是 带扩展的 DVI。运行 xelatex -no-pdf 就能把它当作文件取出来:开头几个字节是 f7 07——DVI 的 pre 命令后面跟的格式 ID 是 7,而不是标准 DVI 的 2。往里看,在一句 XeTeX output 2026.08.13:0452 的注释之后,用到的字体是以文件路径写进去的(比如 lmroman10-regular.otf)。也就是说,.xdv 已经确定了「哪个字体文件的第几个字形放在哪个坐标」,但字体本身还没有被嵌入。剩下的活儿是打开这些字体文件、做子集化、装进 PDF——这正是 xdvipdfmx 的职责。
正常跑一次 xelatex,磁盘上并不会留下 .xdv,因为 XeTeX 是把这串字节通过管道送进驱动的标准输入的。-output-driver=CMD 允许替换驱动,所以只要指向一个仅执行 cat 的脚本,就能把流经的内容原样接住(实测 820 字节,末尾是 DVI 传统的 df df df df)。由此得出两个实务后果。其一,当 graphicx 之类的宏包询问驱动时,答案是 xetex(即 xdvipdfmx)。其二,必须分辨错误出自哪一段。你敲的是 xelatex,但图像处理或字体嵌入的失败会以 xdvipdfmx 的行浮现出来。把日志分成两半读——第一条 TeX 错误行与最后一条驱动行——最省时间。
# Stop after the first stage and keep the intermediate XDV file.
$ xelatex -no-pdf document.tex
Output written on document.xdv (1 page, 820 bytes).
# The second byte is the DVI format id: 7 for XDV, 2 for plain DVI.
$ xxd document.xdv | head -2
00000000: f707 0183 92c0 1c3b 0000 0000 03e8 1d20 .......;.......
00000010: 5865 5465 5820 6f75 7470 7574 2032 3032 XeTeX output 202
# Run the second stage by hand. xdvipdfmx has been the default
# driver on every platform since XeTeX 0.997.
$ xdvipdfmx document.xdv
document.xdv -> document.pdf
# The driver is a replaceable external command.
$ xelatex -output-driver="/path/to/save-stdin.sh" document.tex把文档从 pdfLaTeX 搬到 XeLaTeX 会坏掉什么
最危险的情况恰恰是那种不报错的。 把旧的导言区原样交给 xelatex,多半是能编译过去的。\usepackage[utf8]{inputenc} 只会带来一行警告 Package inputenc Warning: inputenc package ignored with utf8 based engines. 然后被忽略,单看这一条并无实害。麻烦的是 \usepackage[T1]{fontenc} 与 \usepackage{lmodern} 这一对。这两行留着,XeLaTeX 就会停在 8 位的 NFSS 路径上:被嵌入的是名为 ec-lmr10 的 TFM 字体,pdffonts 报作 Type 1C。这和用 fontspec 得到的 CID Type 0C 完全是两回事——也就是说,换引擎几乎白换了。
一行日志就能把两者区分开。仍载入 [T1]{fontenc} 时,写 日 会得到 Missing character: There is no 日 ("65E5) in font ec-lmr10!——码位用的是 TeX 式的十六进制 "65E5,字体名是 8 位字体。同一份文档改用 fontspec 后,则是 Missing character: There is no 日 (U+65E5) in font [lmroman10-regular]...——记法变成 U+65E5,字体名变成 OpenType 文件。 看到前一种写法,就说明已经退回 8 位路径了。组合用的变音符号也是同理(ec-lmr10 里没有 U+0301,而 OpenType 路径能正常排出)。正确的迁移做法是一次性删掉整套旧字体声明——inputenc、fontenc、lmodern、times 之类——让 fontspec 成为选字体的唯一入口。
| 旧导言区中的行 | XeLaTeX 如何处理 | 应对方式 |
|---|---|---|
\usepackage[utf8]{inputenc} | 被忽略,只留一行警告 | 删除 |
\usepackage[T1]{fontenc} | 无声地把你留在 8 位 NFSS 路径上 | 换成 fontspec |
\usepackage{lmodern} | 嵌入 ec-lmr10(Type 1C) | 改用 \setmainfont{lmroman10-regular.otf} |
microtype | 只有字符突出生效,没有字体伸缩 | 照常载入即可 |
babel | 能用,但对 RTL 与复杂文字支持较薄 | 考虑改用 polyglossia |
为什么在 XeLaTeX 下字体伸缩不起作用
因为引擎里根本没有这个功能。 microtype 的两根支柱中,XeTeX 只实现了字符突出。逐个探测原语会发现,XeTeX 有设定逐字突出量的 \lpcode 与 \rpcode,却没有设定伸缩幅度的 \efcode。连突出的开关也不是 pdfTeX 的 \pdfprotrudechars,而是另起名字的 \XeTeXprotrudechars。在 XeLaTeX 下载入 \usepackage{microtype},日志里会出现 Character protrusion enabled (level 2).,而在 pdfLaTeX 或 LuaLaTeX 下本该并列出现的 Automatic font expansion enabled 那一行则悄然消失。若明确要求,它会干脆停下:! Package microtype Error: Font expansion does not work with xetex.
复杂文字与从右向左:HarfBuzz 与 \XeTeX... 原语
XeTeX 之所以被广泛使用,最主要的原因是它能妥善处理那些字形随上下文变化的文字体系——阿拉伯文、印度语系文字等。字形整形由 HarfBuzz 负责:0.9999 版(2013 年 5 月)从旧的 ICU LayoutEngine 切换到了 HarfBuzz,TeX Live 2024 中的构建链接的是 HarfBuzz 8.3.0。引擎自身也带着一组专用原语:在文档中途切换输入编码的 \XeTeXinputencoding,给字符分类并据此改变相邻行为的 \XeTeXcharclass 与 \XeTeXinterchartoks,按语言指定断行规则的 \XeTeXlinebreaklocale,为 PDF 文本提取写入真实字符的 \XeTeXgenerateactualtext,以及读入图像的 \XeTeXpicfile 与 \XeTeXpdffile。语言切换交给 polyglossia,RTL 排版交给 bidi(请求阿拉伯语等时 polyglossia 会自动载入),中日韩则由 xeCJK 负责——不过对需要竖排与讲究禁则的日文稿件,luatexja 或 upLaTeX 更顺手。
什么时候该选 XeLaTeX,什么时候不该
- 想直接用手头的字体时。 把 OpenType 或 TrueType 的名字写进
\setmainfont就结束了,不必碰任何配置文件。 - 想直接写 Unicode 时。 UTF-8 输入是默认,整段编码相关的导言可以一并删掉。
- 多语言与 RTL 文档。
polyglossia搭配bidi在阿拉伯文与希伯来文上久经使用,字形整形交给 HarfBuzz。 - 不该选的理由之一——需要字体伸缩时。 XeTeX 没有
\efcode,microtype的伸缩根本无法生效。若把排版质量放在首位,请用 pdfLaTeX 或 LuaLaTeX。 - 不该选的理由之二——想用程序控制排版时。 XeTeX 没有内嵌脚本语言。需要低层介入就转向 LuaLaTeX。
- 不该选的理由之三——日文竖排。 这超出了
xeCJK的设计范围,早点试luatexja或 upLaTeX 更省时间。
一句话的判断标准是:只想按名字使用系统字体就选 XeLaTeX;想伸手进排版本身就选 LuaLaTeX。 三者的完整对比放在「如何选择引擎」那一页。最后提一条协作上的注意:字体名因系统而异,\setmainfont{Helvetica Neue} 在同事的 Linux 机器上基本必然失败。共享的稿件,先做到用随 TeX Live 附带、按上面那样以文件名指定的字体能正常构建,再按需要逐个替换。若只提交 PDF,请先用 pdffonts 确认所有字体都已嵌入再发出。