字体系统 (NFSS / 编码)

LaTeX 警告里那串 OT1/cmr/m/n/10 不是什么密码,而是一个 地址。LaTeX 从不直接点名某个字体,而是用五个坐标去指认它:编码、族、系列、形状、尺寸。这套机制叫 NFSS。本页顺着这五个坐标一路追到源码和日志:为什么 \bfseries 要的是 bx 而不是 b,在 \fontseries{c} 之后再加粗是否真会得到 bc,以及 Font shape ... undefined 这条替换警告到底在说什么。

NFSS:字体由五个坐标决定

LaTeX2e 的字体选择由 NFSS(New Font Selection Scheme,新字体选择方案) 驱动,其核心主张只有一条:任何文本字体都完全由五个属性决定——编码(encoding)、族(family)、系列(series)、形状(shape)、尺寸(size)。NFSS 由 Frank Mittelbach 和 Rainer Schöpf 设计,内核源文件 ltfssbas.dtx 的版权行可以追溯到 1989 年。在此之前的 LaTeX 2.09 里,你甚至无法同时用上 \bf\it——因为还没有人把字重和字形拆成彼此独立的轴。

  • 编码 — 字形在字体内部的排列顺序(每个输入字符对应哪个槽位)。正文中主要是 OT1(TeX text)和 T1(TeX extended text,即 Cork)。下文详述。
  • — 共享字形骨架的一组字体。Computer Modern Roman 是 cmr,其无衬线是 cmss,等宽是 cmtt;商业字体中 Times 是 ptm,Helvetica 是 phv,Courier 是 pcr
  • 系列 — 把字重和字宽 合并成一条 的轴:m(medium)、b(bold)、bx(bold extended)、c(condensed)、bc(bold condensed)、sb(semibold)等。
  • 形状 — 字母的形态:n(直立)、it(意大利体)、sl(倾斜)、sc(小型大写)。少见的还有 ui(不倾斜的意大利体)。
  • 尺寸 — 设计尺寸,例如 10pt;省略单位时视为 pt

唯一容易绊住人的地方,是 series 把字重和字宽合成了一个值。标准表示中字重从 ulub(ultra light 到 ultra bold),字宽从 ucux(ultra condensed 到 ultra expanded);两者连接成一个值,其中 m 会被省略——只有当字重和字宽都是 medium 时才保留单个 m。于是“bold、正常宽度”是 b,“bold extended”是 bx,“正常字重、condensed”是 c,“bold condensed”是 bc。把这五个值写成 encoding/family/series/shape/size,就是 LaTeX 内部的字体正式名称;用 \showthe\font 可以打印出当前正在使用的那一个。

log
% \showthe\font in a default article, and what the log answers:
> \OT1/cmr/m/n/10 .

% the same five values head every overfull-box report:
Overfull \hbox (121.83368pt too wide) detected at line 3
\OT1/cmr/m/n/10 Supercalifragilisticexpialidocious

% and the first four of them name a shape that had to be substituted:
LaTeX Font Warning: Font shape `T1/cmss/bx/sc' undefined
(Font)              using `T1/cmss/bx/n' instead on input line 4.

为什么 \bfseries 要的是 bx 而不是 b

答案是:\bfseries 根本没有去读 \bfdefault。内核里的 \bfdefault 确实是 b,但 \bfseries 会先检查当前族是否等于 \rmdefault\sfdefault\ttdefault;只要相等,它就改用 专用的默认值 \bfseries@rm / \bfseries@sf / \bfseries@tt。而 latex.ltx 把这三个都初始化成了 bx。只有当前族不属于这三者时才会查 \bfdefault——也就是说,只要你在正文字体里敲 \bfseries\bfdefault 就会被跳过。

latex.ltx
% TeX Live 2024, latex.ltx (the LaTeX2e kernel), abridged
\def\bfseries@rm{bx}
\def\bfseries@sf{bx}
\def\bfseries@tt{bx}

\DeclareRobustCommand\bfseries{%
  \not@math@alphabet\bfseries\mathbf
  \expand@font@defaults
  \maybe@update@bfseries@defaults
  \ifx\f@family\rmdef@ult      \fontseries\bfseries@rm
  \else\ifx\f@family\sfdef@ult \fontseries\bfseries@sf
  \else\ifx\f@family\ttdef@ult \fontseries\bfseries@tt
  \else                        \fontseries\bfdefault
  \fi\fi\fi
  \UseHook{bfseries}%
  \selectfont
}

那么,为什么默认是 bx?原因在于 Knuth 实际刻了多少套字。数一数 TeX Live 2024 里的 Computer Modern 度量文件就会发现,纯粹的粗体 只有 cmb10.tfm 这一个。而 bold extended 则有 cmbx5cmbx6cmbx7cmbx8cmbx9cmbx10cmbx12 共七个设计尺寸。能一路支撑到脚注和下标的粗体只有 cmbx,所以 NFSS 对 Computer Modern 要的是 bx

到这里为止都还只是 Computer Modern 的特殊情况,但内核会替自己收尾。在 \begin{document} 时运行的 \init@series@setup 会检查:只要 \rmdefault 不属于 cmr, cmss, cmtt, lcmss, lcmtt, lmr, lmss, lmtt,它就把 \bfseries@rm 降为 b。换句话说,bx 只是 Computer Modern 和 Latin Modern 的遗产;一旦换成 Times 或 TeX Gyre,\bfseries 就会悄悄改要 b。实测也正是如此:加上 \renewcommand\rmdefault{ptm} 后,\bfseries@rm 和最终得到的系列都是 b

文档中的设置\bfseries 要求的系列原因
article (default)bx\rmdefaultcmr\bfseries@rm 的初值原样保留
\usepackage{lmodern}bxlmr 同样在内核的 Computer Modern 名单里
\renewcommand\rmdefault{ptm}b\init@series@setup 判定其不在名单内,遂下调 \bfseries@rm
\fontseries{b}\selectfontb在低层直接点名的值不经过默认值宏
\DeclareFontSeriesDefault[rm]{bf}{sb}sb只改写某一元族粗体的正式做法

实用结论很清楚:若要改变“粗体”的含义,请优先使用 \DeclareFontSeriesDefault[rm]{bf}{b},而不是 \renewcommand{\bfdefault}{b}。后者也能生效(改写 \bfdefault 会让 \maybe@update@bfseries@defaults 覆盖那三个专用默认值),但无法只针对罗马体或只针对无衬线。而当你遇到经典症状“明明加粗了却没变粗”时,首先要怀疑的就是这里的匹配问题——也许你的字体根本没有 bx

系列会被合成:c 加 b 得到 bc

从 2020 年的 LaTeX2e 版本起(latexrelease.sty 中的 \IncludeInRelease{2020/02/02}),系列的指定变成了 合成而非覆盖。在窄体(c)状态下要求加粗,得到的不是 b,而是 bc(bold condensed)。构建这张合成表的是 \DeclareFontSeriesChangeRule,内核里排着两百多行这样的规则。{c}{b}{bc}{} 读作“当前是 c 而请求 b 时,转为 bc;无备选”。

latex.ltx
% the rule table (latex.ltx, from ltfssaxes.dtx), a handful of the 200-odd entries
\def\DeclareFontSeriesChangeRule#1#2#3#4{%
  \@namedef{series@#1@#2}{{#3}{#4}}}
\DeclareFontSeriesChangeRule {c}{b}{bc}{}     % condensed + bold      -> bc
\DeclareFontSeriesChangeRule {b}{c}{bc}{b}    % bold + condensed      -> bc, else b
\DeclareFontSeriesChangeRule {bx}{c}{bc}{bx}  % bold extended + cond. -> bc, else bx
\DeclareFontSeriesChangeRule {m}{b}{b}{bx}    % medium + bold         -> b,  else bx

不过规则并非无条件触发。\selectfont 会先在不做替换的前提下合成,再检查得到的 encoding/family/series/shape 是否真的存在;若不存在,就恢复原值并以替换模式重做一次合成。Computer Modern 根本没有窄体,所以 \fontseries{c}\selectfont 会发出警告并退回 m,随后再加粗也只能到 b——人们由此断定“合成根本没发生”,原因就在这里。换一个真正带窄体的字族来试,规则就会照常工作。Helvetica(phv)在 T1 下备齐了 m / c / b / bc,下面的例子可以看到实物。

document.tex
\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{helvet}          % sets \sfdefault to phv
\begin{document}\makeatletter
\sffamily                    % -> phv/m   (Helvetica)
\fontseries{c}\selectfont    % -> phv/c   font file phvr8tn (Helvetica Narrow)
\bfseries                    % -> phv/bc  font file phvb8tn (Helvetica Bold Narrow)
\typeout{[series] \f@series\space -> \fontname\font}
\makeatother\end{document}

% [series] bc -> phvb8tn
%
% The same document with Computer Modern instead reports:
%   LaTeX Font Warning: Font shape `OT1/cmr/c/n' undefined
%   [series] m  (the condensed request was substituted away before bold arrived)

低层选择器与 \selectfont

低层选择器逐个设置属性:\fontencoding{T1}\fontfamily{ptm}\fontseries{b}\fontshape{it}\fontsize{12}{14}。规则只有一条——只设置不会生效,必须调用 \selectfont——上一节也已经说明了原因。\fontseries 并不会立刻写入值,而是把它压入名为 \delayed@f@adjustment 的待办列表;真正的合成、存在性检查和替换都由 \selectfont 一次性完成。所以,如果在设置和文字之间忘了 \selectfont,那段文字就会用之前的字体排出来。

document.tex
\documentclass{article}
\usepackage[T1]{fontenc}
\begin{document}
% Correct: set the attributes, commit with \selectfont, then the text
{\fontfamily{ptm}\fontseries{b}\selectfont Bold Times}

% Wrong: the words before \selectfont are still in the old font
{\fontfamily{ptm} Some \fontseries{b}\selectfont text.}

% \usefont gives four attributes at once and commits on the spot
{\usefont{T1}{ptm}{b}{it} Bold italic Times}

% Change only the size; reuse the current baselineskip
\makeatletter
{\fontsize{14}{\f@baselineskip}\selectfont Fourteen point}
\makeatother
\end{document}

一次传入五个属性中四个的捷径是 \usefont{encoding}{family}{series}{shape}:尺寸沿用当前值,并且当场生效。用分组 { } 包住即可把效果限制在该范围内。把内部宏 \f@baselineskip 作为 \fontsize 的第二个参数传入,是只改字号而不动行距的惯用法(切勿改写内部宏,但读取并传入是可以的)。不过——你很少需要亲手写这些\textbf 只是调用 \fontseries\sffamily 只是调用 \fontfamily;只有在点名某个特定字体,或把新字体接入类/宏包时,才需要下到低层。

默认值宏:\rmdefault 及其同伴

\textrm\rmfamily 选择哪个族 并不是写死的,而是保存在宏里。共三个:\rmdefault(罗马)、\sfdefault(无衬线)、\ttdefault(等宽),在 article 类中默认分别是 cmrcmsscmtt。因此要更换正文字体时,改的是这些宏而不是命令。而且正如上一节所见,这三个值还决定了粗体会要求什么,所以把它们看作 文档的字体方针 比看作别名更准确。

preamble.tex
% Re-point the three meta-families for the whole document
\renewcommand{\rmdefault}{ptm}  % roman   -> Times
\renewcommand{\sfdefault}{phv}  % sans    -> Helvetica
\renewcommand{\ttdefault}{pcr}  % mono    -> Courier

% The body font itself is these four (defaults: OT1, \rmdefault, m, n)
%   \encodingdefault \familydefault \seriesdefault \shapedefault
% and \familydefault points at \rmdefault, so the line above changes the body.

% Retarget bold and medium per meta-family (LaTeX2e 2020-02-02 and later)
\DeclareFontSeriesDefault[rm]{bf}{b}
\DeclareFontSeriesDefault[sf]{md}{l}

.fd 文件与 \DeclareFontShape:坐标如何对上真实文件

把这五个坐标与真实字体文件连接起来的,是 字体定义文件(.fd。文件名是编码的小写形式后接族名——OT1cmrot1cmr.fdT1phvt1phv.fd——LaTeX 在需要时会自动载入它。用 kpsewhich ot1cmr.fd 就能知道它在本机的位置。文件内容是一行 \DeclareFontFamily,随后是每个系列/形状组合各一条的 \DeclareFontShape

ot1cmr.fd
% TeX Live 2024, tex/latex/base/ot1cmr.fd (excerpt)
\DeclareFontFamily{OT1}{cmr}{\hyphenchar\font45 }
\DeclareFontShape{OT1}{cmr}{m}{n}%
     {<5><6><7><8><9><10><12>gen*cmr%
      <10.95>cmr10%
      <14.4>cmr12%
      <17.28><20.74><24.88>cmr17}{}
%%%%%%% bold series
\DeclareFontShape{OT1}{cmr}{b}{n}
     {%
      <5><6><7><8><9><10><10.95><12>%
      <14.4><17.28><20.74><24.88>cmb10%
      }{}
%%%%%%%% bold extended series
\DeclareFontShape{OT1}{cmr}{bx}{n}
   {%
      <5><6><7><8><9>gen*cmbx%
      <10><10.95>cmbx10%
      <12><14.4><17.28><20.74><24.88>cmbx12%
      }{}

读法只是跟着尖括号里的尺寸区间走。<5><6><7><8><9>gen*cmbx 表示“5 到 9 点时,使用词干加尺寸构成的实际字体(cmbx5cmbx6……)”;<12><14.4>...cmbx12 表示“12 点以上缩放 cmbx12”。上面那段 b 正好展示了前文说的“只有一套”的处境:从 5 点到 24.88 点全都是把 cmb10 拉伸而来。此外还有 ssub(静默替换)和 sub(带警告的替换);例如 t1phv.fd 中有一行 \DeclareFontShape{T1}{phv}{bx}{n}{<->ssub * phv/b/n}{}——Helvetica 没有 bx,于是悄悄改给 b

读懂 Font shape ... undefined 这条替换警告

这条警告是在报告:.fd 里没有这样的组合,于是它自作主张挑了最接近的一个。它总是两行——第一行是 你请求的四个坐标,第二行是 实际使用的四个坐标 加上输入行号。它不是错误,所以排版会跑到最后;只是得到的 PDF 并不是你要的那个。上面的例子里,Computer Modern 没有窄体,于是 c 变成了 mT1/cmss 没有小型大写,于是改用直立的 n

log
% a missing shape inside a family that does exist
LaTeX Font Warning: Font shape `T1/cmss/bx/sc' undefined
(Font)              using `T1/cmss/bx/n' instead on input line 4.

% a family that does not exist at all: \DeclareFontSubstitution kicks in
LaTeX Font Warning: Font shape `T1/nosuchfont/m/n' undefined
(Font)              using `T1/cmr/m/n' instead on input line 4.

% and at the end of the run, the summary line that is easy to miss
LaTeX Font Warning: Some font shapes were not available, defaults substituted.

原因只有三种。(1) 这个字体本来就没有那个字面——比如 Helvetica 没有意大利体,于是用 sl 顶替。没有的东西变不出来,只能换字体或接受替代。(2) 族名拼错了——如果第二行出现了 cmr,那就是 \DeclareFontSubstitution 的最后兜底被触发,通常意味着拼写错误或忘了载入某个宏包。(3) 用错了编码——例如对一个只有 OT1 .fd 的族在 T1 下发起请求。此外,日志末尾那一行汇总 Some font shapes were not available, defaults substituted. 很容易被一扫而过——但只要它出现,文中某处就用上了你并未选择的字体。

字体编码:OT1、T1、TU 的区别

字体编码 是关于 每个输入字符对应字体中哪个槽位(字形) 的约定。它是五个属性中的第一个,也是唯一一个没有 \textbf 那样面向作者命令的属性——切换编码是 宏包的活儿,由 fontenc 负责。OT1 是 Knuth 原始的 7 位“TeX text”(默认)。它只能容纳 128 个字符,因此 éñ 是通过 基字母与重音的组合\accent)生成的;正是这种组合导致 带重音的词无法断词,从 PDF 复制出来也会乱码。T1TeX extended text,因 1990 年在 Cork 的 TUG 会议也称 Cork 编码)是 8 位、256 字形的编码,把每个重音字母作为 单独字形 保存,于是这两个问题同时消失。

要说明“编码只是一份关于槽位顺序的约定”,没有比 OT1 里的英镑符号更好的例子了。OT1 没有英镑的槽位,于是 ot1enc.def 中的 \textsterling 去取 意大利体字体的美元符号槽位(Computer Modern 的那个槽位里正是 £)。而且若当前字体没有倾斜,它会先切到 \fontshape{ui}——否则只有 £ 会斜着冒出来。这也正是 ot1cmr.fdui 的声明上带着一条警告注释、说明该定义供 LaTeX 的 \pounds 使用、不得删除的原因。

TUTeX Unicode)用于直接以 Unicode 码点使用系统安装的 OpenType 字体,是 XeLaTeX 和 LuaLaTeX 的默认编码。它取代了较旧的 EU1(XeTeX 用)和 EU2(LuaTeX 用):按 fontspec 的更新日志,TU 出现在 v2.5(2016 年 1 月 30 日),升为默认是在 v2.5c(2017 年 1 月 20 日)。在 TeX Live 2024 的 fontspec.sty 中,EU1EU2 这两个字符串已经一处都不剩。此外还有 T2A(西里尔)、LGR(希腊)和 TS1Text Companion,货币、版权等正文用符号);数学方面分配了 OML(数学斜体)、OMS(数学符号)和 OMX(大型数学符号)。另外,TS1 的符号命令已于 2020 年并入内核,如今的 textcomp 宏包一个符号也不定义——详见符号页面。

编码覆盖内容位宽 / 备注
OT1Knuth 原始的 TeX text(默认)7 位;重音由组合生成 → 无法断词
T1TeX extended text(Cork);西欧语言8 位 256 字形;重音为单独字形,可断词
TUTeX Unicode;直接寻址 OpenType 字体Xe/LuaLaTeX 的默认;取代了 EU1 / EU2
T2A西里尔文字8 位(T2B / T2C 属同族)
LGR希腊文字当前希腊语通行的 256 字形编码
TS1Text Companion(正文符号)货币、版权等;相关命令已于 2020 年移入内核
OML / OMS / OMX数学斜体 / 数学符号 / 大型数学符号仅用于数学,不用于正文

inputenc 与 fontenc 的区别:入口与出口

这里把两个容易混淆的概念分开。输入编码 决定 .tex 源文件的 字节序列按什么字符读取(UTF-8 之类),历史上由 inputenc 宏包负责。字体编码 决定读入的字符 流入输出字体的哪个字形,由 fontenc 负责。记成 入口(inputenc)与出口(fontenc),就不会弄混。现代 pdfLaTeX 默认把源文件按 UTF-8 读取,因此几乎不再需要显式载入 inputenc。字体编码则仍然值得设置。fontenc面向 pdfLaTeX 的宏包;XeLaTeX 和 LuaLaTeX 改用 fontspec,而且那里默认就是 TU,这个问题基本不会出现。

用 pdfLaTeX 就写 \usepackage[T1]{fontenc}

如果用 pdfLaTeX 排版,惯例是在导言区写 \usepackage[T1]{fontenc}。它把文档的字体编码切到 T1,一举扫除 OT1 默认带来的弊端:带重音的词无法断词、从 PDF 复制会乱码。fontenc 的文档也是这么说的:它支持法语、德语、意大利语、波兰语等广泛的西欧语言,有了它,含重音字母的词 LaTeX 也能断词,输出也可以复制粘贴。当选项中列出多个编码时,最后列出的会成为默认值\encodingdefault 被设为它)——所以混用希腊语的文档写成 \usepackage[LGR,T1]{fontenc},正文保持 T1,只在需要处切到 LGR

document.tex
\documentclass{article}
\usepackage[T1]{fontenc}   % font encoding -> T1 (Cork)
% Input is UTF-8 by default on pdfLaTeX, so inputenc is usually unneeded.
\begin{document}
% With T1 these words hyphenate and survive copy-and-paste out of the PDF.
Na\"ive r\"esum\"e, \"uberfl\"ussig, Stra\ss{}e.
\end{document}