fontspec (Xe/Lua)

同样三行导言区,在 LuaLaTeX 下能编译,在 XeLaTeX 下却以 ! Package fontspec Error: The font "TeX Gyre Termes" cannot be found. 中止——这不是环境坏了,而是 fontspec 的设计使然。fontspec 是让 LaTeX 按书体名 调用字体的宏包,一行 \setmainfont 就能换掉正文字体。但「拿这个名字去问谁」在不同引擎下并不一样,而这正是真正会弄坏文档的地方。本页把名称解析的分歧放在中心,再依次讲透 \setmainfont\newfontfamily、按文件指定以及 OpenType 功能键,每一处都附实测。

fontspec 能在哪些引擎上运行,以及为什么 pdfLaTeX 不行

fontspec 只能在 XeLaTeX 和 LuaLaTeX 这两个引擎 上运行;在 pdfLaTeX 下,\usepackage{fontspec} 这一行本身就会报错。原因是历史性的。1980 年代的 TeX 只认识自家的 TFM 度量格式和 Type 1 / METAFONT 字体。OpenType 以及系统级字体数据库,都是 TeX 诞生之后世界才准备出来的东西。XeTeX 和 LuaTeX 是为迎合这个新世界而从外部重建的引擎,原生支持 Unicode 输入与 OpenType 字体。fontspec 只是把这些新能力翻译成 LaTeX 词汇的一层,而 pdfTeX 根本没有可供翻译的能力。

载入只需要一行 \usepackage{fontspec}。面向 XeLaTeX 和 LuaLaTeX 的文档类与宏包(bxjsarticleunicode-mathluatexja-fontspec 等)通常已经先行载入,之后再显式写一遍也不会重复载入。从 pdfLaTeX 文档移植过来时,别忘了删掉 \usepackage[T1]{fontenc}\usepackage[utf8]{inputenc}:前者多余,因为 fontspec 会切换到 TU 编码;后者在 Unicode 引擎上毫无意义。

\setmainfont / \setsansfont / \setmonofont 更换正文字体

文档的三根支柱是 \setmainfont{…}(罗马体,即正文默认字体)、\setsansfont{…}\textsf\sffamily 选用的字体)和 \setmonofont{…}\texttt\ttfamily 背后的等宽字体)。参数是书体名或字体文件名。fontspec 会据此自动找出正体、粗体、斜体、粗斜体这一整套四件,并接到 \textbf\textit\emph 上,因此正文的写法完全不变。自动配齐这套四件正是 fontspec 的核心工作;后面会看到,只有当这套自动化失灵时,你才需要额外写配置。

document.tex
% compile with lualatex (or xelatex — see the next section)
\documentclass{article}
\usepackage{fontspec}
\setmainfont{TeX Gyre Termes}   % roman / body
\setsansfont{TeX Gyre Heros}    % sans serif
\setmonofont{TeX Gyre Cursor}   % monospaced
\begin{document}
Body text, \textbf{bold}, \textit{italic}, \textsf{sans}, \texttt{mono}.
\end{document}

原则上这些命令应写在 导言区,理由只有一个:LaTeX 在 \begin{document} 时刻固定数学字体。在导言区执行 \setmainfont,同一书体也会用到数学中的罗马体(如 \mathrm);写在正文中,则只有正文会变,数学的罗马体被留在原地。数学本身的字体不归 fontspec 管,而由 unicode-math\setmathfont 负责——请记住正文和数学是两条不同的命令。

The font "…" cannot be found.:XeLaTeX 与 LuaLaTeX 的查找范围不同

如果 ! Package fontspec Error: The font "…" cannot be found. 只在 XeLaTeX 下出现,那不是名字拼错,而是「问谁」不同。XeTeX 向操作系统的字体数据库查询,因此只能看见系统认为已安装的书体。LuaTeX 通过 luaotfload 自建索引,而这个索引同时覆盖 TeX 目录树。 TeX Live 随附的字体并未注册到系统中,这个差距就直接变成了结果的差距。在本机的 TeX Live 2024 上,同一个文件在 LuaLaTeX 下编译通过,在 XeLaTeX 下中止。

terminal
$ xelatex t1.tex
! Package fontspec Error: The font "TeX Gyre Termes" cannot be found.

$ lualatex t1.tex
Output written on t1.pdf (1 page, 3697 bytes).

$ pdffonts t1.pdf
name                            type          encoding    emb sub uni
NNOCLB+TeXGyreTermes-Regular    CID Type 0C   Identity-H  yes yes yes

这条界线画在「随 TeX 分发的字体」与「系统字体」之间。实测显示,安装在系统一侧的书体——Hiragino Mincho ProNHiragino SansYuMincho——在 XeLaTeX 下确实能解析。相反,TeX Live 分发在 texmf-dist/fonts/opentype/public/ 下的字体,XeLaTeX 按名字看不见;在 TeX Live 2024 中,该目录含有 125 个宏包(实际 .otf 文件 1,499 个)。引擎本身的设计思路属于 /learn/engines/xetex/learn/engines/luatex。这里需要的是 让坏掉的文档立刻重新编译的办法

修法有两种。(1)改用 lualatex 编译。 一行源码都不用改,多数情况下这是最短路径。(2)写文件名而不是书体名。 \setmainfont{texgyretermes-regular.otf} 在 XeLaTeX 下确实可用——但有代价:按文件命名后,fontspec 无法推断粗体和斜体成员。一用 \textbf 就会得到 LaTeX Font Warning: Font shape 'TU/texgyretermes-regular.otf(0)/b/n' undefined,本想加粗的段落会以细体输出。这是典型的静默失败:只是警告而非错误,容易被忽略。文件名还区分大小写——写成 TeXGyreTermes-Regular.otf,同一个字体又会回到 cannot be found

按文件指定:Path= / Extension= / BoldFont=

按文件指定的完整写法,是把四种样式逐一手工分配。用 Path= 给出目录,用 Extension= 给出扩展名(.otf.ttf),再把各文件对应到 UprightFont=BoldFont=ItalicFont=BoldItalicFont=。这些值中的星号 * 是占位符,会被 第一个参数给出的共同基名替换。上一节里消失的粗体和斜体,只要写全这四项就会回来——实测中,写全四项的版本 Font shape … undefined 警告归零,pdffonts 里并排列出了 Regular、Bold、Italic 和 BoldItalic。

document.tex
\usepackage{fontspec}
% XeLaTeX-safe: name the files, and name all four styles
\setmainfont{texgyretermes}[
  Extension      = .otf ,
  UprightFont    = *-regular ,
  BoldFont       = *-bold ,
  ItalicFont     = *-italic ,
  BoldItalicFont = *-bolditalic ,
]

% fonts shipped inside the project directory
\setsansfont{LibreBaskerville}[
  Path        = ./fonts/ ,
  Extension   = .otf ,
  UprightFont = *-Regular ,
  BoldFont    = *-Bold ,
]

选哪种写法,实际上取决于 这份文档由谁来排。只给自己看的草稿,写书体名更快。若同一份 PDF 必须在别人的机器上也能产出——合作者、投稿、CI、Docker——那么 把字体随项目一起放入,并用 Path= 指向它们,几乎是唯一可靠的办法。给定 Path= 后,fontspec 会直接查看该相对路径而不是 TeX 的搜索路径,于是整个仓库可以一起搬走。

方法写法适用场景在 XeLaTeX 下能取到随附字体吗
By name\setmainfont{TeX Gyre Termes}草稿与个人文档;粗体斜体自动配齐不能,只能取到系统已注册的书体
By file, upright only\setmainfont{texgyretermes-regular.otf}只想先编译通过时;会失去粗体能,但 \textbf 会静默保持细体
By file, all fourPath / Extension / UprightFont / BoldFont / ItalicFont / BoldItalicFont协作、投稿、CI、自制字体;可复现性优先时能,且不会出现 Font shape … undefined 警告

\newfontfamily 增加第四、第五种书体

装不进罗马体/无衬线/等宽三个位置的书体,用 \newfontfamily 造一个自己的切换命令来补。第一个参数是想创建的命令名,第二个是字体名,并可直接接受与 \setmainfont 相同的选项。造出的命令与 \rmfamily 一样是 声明式的:它会作用于其后的一切。要限定范围,就用花括号包住,写成 {\titlefont …}

document.tex
\usepackage{fontspec}
\newfontfamily\titlefont{TeX Gyre Bonum}
\newfontfamily\quotefont{TeX Gyre Schola}[Scale = MatchLowercase]
% ...
\titlefont A heading in Bonum

{\quotefont A quoted passage in Schola.}

只用一次的书体,可以不定义,直接把 \fontspec{Font Name} 作为现场声明使用。但 \fontspec 每次调用都会重新载入字体,因此用它来写章标题这类反复出现的书体,只会让编译时间悄悄变长。用到两次以上就该用 \newfontfamily 定义。另外,若已存在同名命令,\newfontfamily 会失败——请避开 LaTeX 本来就有的短名(如 \sc\it),改用 \titlefont 这类能说明用途的名字。

用键调用 OpenType 功能:Ligatures / Numbers / Scale

OpenType 字体本身就带有替代字形——合字、数字变体、小型大写——fontspec 通过 key=value 选项 把它们取出来。这些效果测量比肉眼判断更快,下面给出用 TeX Gyre Pagella 10pt 实测的数值。加上 Numbers=OldStyle 后,0123456789 的深度从 0.19998pt 增至 2.37999pt——旧式数字让 39 带上下伸部这一事实,直接体现在数字里。

  • Ligatures= — 合字与输入转换。Ligatures=TeX 启用 TeX 式的写法-- 变成 en dash,--- 变成 em dash,`'' 变成弯引号。(实测:--- 从「三个连字符」的 9.99pt 变成正好 10.0pt,即一个 em 宽的真正 em dash。)其他值有 Common(标准的 fi、fl 等)、NoCommon(关闭它们;实测 fi 由 6.05pt 变宽至 6.35pt)、Rare/DiscretionaryHistoric`。
  • Numbers= — 数字样式。把 OldStyle(融入正文的旧式数字)或 Lining(高度一致的现代数字)与 ProportionalMonospaced 组合。表格中的数字设为 Lining,Monospaced 后就能纵向对齐。
  • Letters=SmallCaps — 真正的小型大写。在 Libertinus Serif 上实测,Abc 的高度由 6.98pt 降至 6.58pt,宽度由 16.26pt 增至 16.94pt。注意:若字体没有 smcp 表,fontspec 既不动作也不警告——在 TeX Gyre Pagella 上宽度始终是 17.6pt。也有 Letters=Uppercase 等值。
  • SmallCapsFeatures= — 只在小型大写时生效的一组功能,例如 SmallCapsFeatures={Numbers=OldStyle}。同样的写法还有 UprightFeatures=BoldFeatures=ItalicFeatures=,可以按样式分别设置。
  • Scale= — 缩放比例:可以是数值,也可以是 Scale=MatchLowercase(与正文字体的 x-height 对齐)或 Scale=MatchUppercase(与大写高度对齐)。实测中,TeX Gyre Heros 的 x-height 5.24pt 在 Pagella 正文下降到 4.68999pt,正好等于 Pagella 的 4.69pt。
  • StylisticSet= — 按编号选择字体提供的替代字形集合。RawFeature= 是没有专用键时的后门,可直接传入 OpenType 标签;RawFeature={+onum}Numbers=OldStyle 结果相同(实测深度同为 2.37999pt)。
  • Script= / Language= — 选择文字体系和语言(如 Script=ArabicLanguage=Turkish)。Renderer= 指定造形引擎;在 XeTeX 下可选 AATOpenTypeGraphite。在 LuaTeX 下由 luaotfload 处理,因此这个键基本用不上。
document.tex
\usepackage{fontspec}
\setmainfont{TeX Gyre Pagella}[
  Ligatures = TeX ,
  Numbers   = OldStyle ,
  SmallCapsFeatures = {Numbers = Lining} ,
]
% match the sans to the body x-height
\setsansfont{TeX Gyre Heros}[Scale = MatchLowercase]

若只想给某一处而非整个文档添加功能,使用 \addfontfeature{…}(多个用 \addfontfeatures{…})。它会用你指定的功能 局部覆盖当前字体,只在包围的花括号内生效。典型场景正是:正文用旧式数字排版,但某个表格里的数字需要对齐。

latex
Set in old-style figures, but this column
{\addfontfeature{Numbers={Lining,Monospaced}} 01234 56789}
lines up digit by digit.

fontspec 不负责的部分:数学与日文

fontspec 只负责 正文的拉丁字体;数学和日文各有自己的路线。数学要载入 unicode-math,把 OpenType 数学字体(Latin Modern Math、STIX Two Math、NewCM Math 等)交给 \setmathfont{…}。改动 \setmainfont 完全不会影响数学,所以正文与公式脸孔不一致并不是 pdfLaTeX 独有的问题。选哪个数学字体属于数学字体页面的范围。

日文也是同样的结构。\setmainfont 只更换拉丁字体,汉字和假名纹丝不动。在 LuaLaTeX 中要载入相当于日文版 fontspecluatexja-fontspec,并使用带 “j” 的命令:\setmainjfont{…}(Mincho,即日文正文)、\setsansjfont{…}(Gothic)、\newjfontfamily。选项写法基本一致,所以关于 Scale=Path= 的知识可以直接搬过来。也就是说,在 Unicode 引擎上排日文,就是 让两条线并行。有哪些日文字体可选,见日文字体页面。