字体工具

投稿须知写着“所有字体必须嵌入”,而你并不知道自己的 PDF 是否合规。本该是意大利体的段落偏偏不倾斜。你在 fontspec 里写了 Numbers=OldStyle,却什么也没变。这类情形靠的不是猜,而是 检查工具。本页把 LaTeX 周边的字体检查工具一字排开,并附上真实运行得到的输出:pdffontsfc-listotfinfo\fontname\font、用来打印字形表的 nfssfont,以及把 虚拟字体(.vf 拆成可读文本的 vftovp。顺带也解开一个谜:为什么 TeX 的字体名会长成 phvbc8t 这副样子。

现在用的到底是哪个字体?\fontname 与 \showthe\font

文档中任意一点上究竟选中了什么,\fontname\font 用真实文件名回答,\showthe\font 用 NFSS 的五个坐标回答。前者返回 TeX 实际载入的度量名,如 cmr10cmbx10cmti10;后者会往日志里打印 > \OT1/cmr/m/n/10 .。两者不会互相矛盾:后者是请求,前者是结果。同时看这两样,就能一眼判断你拿到的是不是自己要的字体,还是中途被替换掉了。

log
% \typeout{fontname = \fontname\font} in a default article
fontname = cmr10
bf fontname = cmbx10
it fontname = cmti10

% \showthe\font stops the run and prints the NFSS name
> \OT1/cmr/m/n/10 .

% \showfont is NOT a LaTeX command; it belongs to ConTeXt.
% In LaTeX it fails with:
! Undefined control sequence.

这里需要更正一点。\showfont 并不是 LaTeX 的命令。 它常被当作报告当前字体的方法引用,但它既不在 LaTeX2e 内核里,也不在任何宏包里,执行会得到 ! Undefined control sequence.(同名命令存在于 ConTeXt)。若要在 LaTeX 中做同样的事,除上面两条外还可以直接读内部宏:在 \makeatletter 之内,用 \f@encoding\f@family\f@series\f@shape\f@size 就能以任意格式 \typeout 出这五个坐标。

若想追踪字体加载本身,就用 tracefnt 宏包。设定 \tracingfonts=2,每次载入实际字体时,日志都会记录“把哪个外部字体当作哪个 NFSS 名载入”。在追查替换原因时,它比警告本身信息量更大。不过输出量很大,实际做法是先把出问题的那个 \section 抽成最小文档再跑。

log
% \usepackage{tracefnt} + \tracingfonts=2, document set in T1
LaTeX Font Info:    External font `ecbx1000 at10.0pt' loaded as
(Font)              T1/cmr/bx/n/10  on input line 6.
LaTeX Font Info:    External font `ecti1000 at10.0pt' loaded as
(Font)              T1/cmr/m/it/10  on input line 6.

检查 PDF 中嵌入的字体 — pdffonts

pdffonts(随 Poppler / Xpdf 提供)会把成品 PDF 中 有哪些字体、以什么格式、是否嵌入 列成表,一行一个字体。要看的有三列——emb 表示是否嵌入,sub 表示是否子集化(只保留实际用到的字形),uni 表示是否有回到 Unicode 的映射(能否从 PDF 复制文字)。投稿前的检查只需这一条命令;只要 emb 一列全是 yes,就不必担心因“字体未嵌入”被退回。

terminal
# a document compiled with pdfLaTeX and \usepackage[T1]{fontenc}
$ pdffonts pf.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
OSZFFG+SFRM1000                      Type 1            Custom           yes yes yes      4  0
PCDLRW+SFBX1000                      Type 1            Custom           yes yes yes      5  0
IEKZWO+SFTI1000                      Type 1            Custom           yes yes yes      6  0
GFIBPT+CMMI10                        Type 1            Builtin          yes yes yes      8  0

# the same document via XeLaTeX + fontspec with an OpenType face
$ pdffonts pfx.pdf
PPMQXL+TeXGyrePagella-Regular-Identity-H CID Type 0C   Identity-H       yes yes yes      4  0
AXDTQU+TeXGyrePagella-Bold-Identity-H    CID Type 0C   Identity-H       yes yes yes      6  0

这份输出还兼作引擎指纹。pdfLaTeX 以 Custom / Builtin 编码嵌入 Type 1 字体,而 XeLaTeX 或 LuaLaTeX 通过 fontspec 使用 OpenType 时则给出 CID Type 0CIdentity-H——别人发来的 PDF 是用什么排的,据此基本能判断。名字前缀的六个字母(如 OSZFFG+)是子集标记;同一字体做两次子集化就会得到两个不同前缀。反过来,若某行的 embno,说明这个字体交给读者的机器去解决——标准十四款 PostScript 字体通常无妨,但投稿时会被拒。

查明系统里有哪些字体 — fc-list 与 otfinfo

交给 fontspec 的名字必须是 字体自身的名字,随手编一个是行不通的。要列出已安装的字体并把名字和文件对应起来,用 fc-list(fontconfig 的一部分)。不带参数会列出全部;fc-list : family 只列家族名;给出模式则可缩小范围。LuaTeX 一侧对应的是 luaotfload-tool--find 用于解析名字,--update 用于重建字体名数据库。另外,fontconfig 在 Linux 上是标配,但 macOS 默认并不带(需要通过 Homebrew 之类另行安装)。

terminal
# every installed font, one line each (this machine reports 2799 lines)
$ fc-list | wc -l
2799

# narrow to one family: file, then all its names, then the style
$ fc-list "Hiragino Mincho ProN" file family style
/System/Library/Fonts/HiraginoMinchoProN.ttc: Hiragino Mincho ProN,\
  Hiragino Mincho ProN W6:style=W6,Regular

# what would fontconfig pick for a generic request?
$ fc-match sans
Verdana.ttf: "Verdana" "Regular"

一个字体 究竟能做什么,归 otfinfo(lcdf-typetools,随 TeX Live 提供)管。-i 给出名称、版本和版权,-f 列出 OpenType 特性标签-s 给出支持的文字系统与语言,-t 列出表。其中 -f 最有实用价值:在 fontspec 里写 Numbers=OldStyleLetters=SmallCaps 之前先在这里确认有没有 onumsmcp,就能预先避开大部分“写了却没效果”的情况。因为对不具备该特性的字体写上特性名,只会被静默忽略。

terminal
$ otfinfo -i $(kpsewhich texgyrepagella-regular.otf)
Family:              TeX Gyre Pagella
Subfamily:           Regular
PostScript name:     TeXGyrePagella-Regular
Version:             Version 2.501;PS 2.501;ffdkm 0.1
Copyright:           Copyright 2006-2018 for TeX Gyre extensions by B. Jackowski,
                     J.M. Nowacki, et al. Vietnamese characters were added by Han The Thanh.

$ otfinfo -f $(kpsewhich texgyrepagella-regular.otf)
c2sc	Small Capitals From Capitals
dlig	Discretionary Ligatures
kern	Kerning
liga	Standard Ligatures
lnum	Lining Figures
onum	Oldstyle Figures
size	Optical Size
smcp	Small Capitals
ss01	Stylistic Set 1

这条 -i 输出里还藏着一层小小的渊源。为 TeX Gyre Pagella 添加越南语字符的是 Hàn Thế Thành——pdfTeX 的作者,也是用博士论文把微观排版带进 TeX 的人,正是本站 microtype 页面的主题。字体的版权声明里,往往保留着这样的来历。

把字体的全部字形打印出来 — nfssfont.tex 与 fonttable

亲眼确认 一个字体到底装了哪些字形,就把字形表打印出来。几十年来随 TeX 一起分发的交互式工具是 nfssfont.tex:启动后它会问 Input external font name, e.g., cmr10,答以 cmr10,用 \table 出表,用 \bye 结束。这里要更正一点——流传甚广的 tex nfssfont 这种启动方式 是行不通的nfssfont.tex 第 48 行是 \documentclass{article},它是 LaTeX 文档;用 plain TeX 启动会以 ! Emergency stop. 崩掉。正确的命令是 latex nfssfontpdflatex nfssfont

terminal
$ pdflatex nfssfont
Input external font name, e.g., cmr10
(or <enter> for NFSS classification of font): cmr10
Now type a test command or \help for help [ \table \bye ]: \table
...
Output written on nfssfont.pdf (1 page, 71803 bytes).

# \help lists the whole repertoire:
#   \init switches to another font;  \stop or \bye finishes the run;
#   \table prints the font layout in tabular format;
#   \text prints a sample text, assuming TeX text font conventions;
#   \sample combines \table and \text;
#   \lowers \uppers \digits \math \punct \names \bigtest ...

# Knuth's original is plain TeX and does start under tex:
$ tex testfont
Name of the font to test = cmr10
Now type a test command (\help for help): \table

若嫌交互麻烦,还有 fonttable 宏包,可以在文档内部做同样的事。\fonttable{cmr10} 接受一个字体名(严格说是 .tfm 名),排出全部字形的表——想看 Zapf Dingbats 就写 \fonttable{pzdr}\xfonttable{T1}{cmr}{m}{n} 则改用 NFSS 的四个属性来指定。用 \fontrange{<low>}{<high>} 可把表限制到以 16 个字形为一块,最多 256 个。若想看的是 用该字体排出的示例文本 而非字形网格,那就用 \fonttext{cmr10}。该宏包的大部分代码是 nfssfont.tex 的修改版,其字形测试命令则重新实现了 Knuth 的 testfont.tex——可以视作同一件工具的三张面孔。顺带一提,在 nfssfont 的首个提示处直接回车而不输入名字,就会切换到用 NFSS 五个属性 指定字体的模式。

虚拟字体(.vf):一层能造出并不存在的字形

虚拟字体(.vf 是一层翻译:把 TeX 看到的那一个逻辑字体,翻译成实际字形和低层排版操作。用 TeX FAQ 的说法,虚拟字体是“把零碎片段收集起来组成字体字形的手段”,这些片段来自 其他字体、规则线以及带位置信息的排版命令。听着抽象,打开一个实物就明白了。用 vftovp 拆开 T1 编码的 Times(ptmr8t),ff 合字那个位置写的是:放一个 f,退回 0.025 em,再放一个 f。原始 Times 里没有 ff 字形,于是虚拟字体当场把它拼了出来。

terminal
$ vftovp $(kpsewhich ptmr8t.vf) $(kpsewhich ptmr8t.tfm) ptmr8t.vpl
$ head -5 ptmr8t.vpl
(VTITLE )
(FAMILY UNSPECIFIED)
(CODINGSCHEME EXTENDED TEX FONT ENCODING - LATIN)
(DESIGNSIZE R 10.0)
(MAPFONT D 0 (FONTNAME ptmr8r) ...)

# slot O 33 (octal) is the ff ligature of T1 - assembled from two f glyphs
(CHARACTER O 33
   (CHARWD R 0.640991)
   (CHARHT R 0.677991)
   (MAP
      (SETCHAR C f)
      (MOVERIGHT R -0.025)
      (SETCHAR C f)
      )
   )

这个机制是分工。TeX 本身只需要 尺寸:宽度、高度和深度,从配套的 .tfm(TeX Font Metric) 读取。每个字形“实际怎么画”写在 .vf 里,由 DVI 驱动读取(dvips、dvipdfmx)。因此虚拟字体 总是成对:一个 .tfm 加一个 .vf。二进制文件读不了,所以每个发行版都带着两个程序,用于与人类可读的 .vpl(Virtual Property List) 互转。普通度量也有同样关系的 .pl(Property List)格式:用 tftopl 打开,就能读到 FONTDIMEN 的七个参数以及 LIGTABLE 中的连字与 kerning 表。

  • vftovp.vf + .tfm(二进制)→ .vpl(文本)。参数顺序是 虚拟字体、度量、输出拆解现有虚拟字体 用它。
  • vptovf.vpl(文本)→ .vf + .tfm(二进制)。参数顺序是 描述、虚拟字体、度量。把编辑过的描述烘回去。
  • tftopl / pltotf — 针对普通西文度量的同一组来回转换。用来读 .tfm 里的内容(FONTDIMENLIGTABLE)。
  • ppltotf / ptftopl — 上一组的日文对应版本,处理 pTeX 系的二进制 JFM。
  • 以上都可以省略扩展名,程序会自动补上 .vf / .tfm / .vpl

把生成的 .tfm 放到 texmf 树中的 .../fonts/tfm/<supplier>/,把 .vf 放到 .../fonts/vf/<supplier>/,再用 mktexlsr 刷新文件名数据库,kpsewhich ptmr8t.vf 就能找到它们。把 TFM 名与实际字体文件对应起来的那些表(字体映射)则由 updmap 管理。不过在今天的实践中几乎没人手写 .vpl——从 PostScript 或 OpenType 字体生成 TeX 度量与虚拟字体,是 fontinst 的活儿。与其说虚拟字体是从零编写的东西,不如说它是工具生成之后 打开、阅读并微调 的对象,这更符合实情。

为什么 TeX 的字体名像密码 — texdoc fontname

答案是:当年只有八个字符可用。用 texdoc fontname 打开的 Fontname 规范并不讳言原因:它明确写着,为了兼容 DOS 文件系统以及用于 CD-ROM 发行的 ISO 9660 标准,名字被限制在八个字符以内。可用字符只有小写 a–z、数字和下划线。这八个字符按 S TT W [V...] [N] [E] [DD] 划分——供应商、字面、字重、变体、编码、字宽、设计尺寸——这就是整套规范。

phvbc8t 中的部分在规范中的位置含义
p供应商(S)Adobe——supplier.map 的注记说“p 取自 PostScript”
hv字面(TT)Helvetica(typeface.map 中的 hv helvetic
b字重(W)bold(weight.map 中是 b Bold
c字宽(E)condensed——在 NFSS 一侧它会与 b 合成为 bc
8t编码(N)variant.map 中是 8t ECEncoding CorkEncoding——也就是 T1

把这张表读上一遍,.fd 文件和日志里那些名字就忽然开口说话了。ptmr8t 是 Adobe 的 Times regular,T1 编码;pcrr8t 是 Courier regular,T1;ptmb8c 是 Times bold 的 TS18c 即 Text Companion)。7t 表示“TeX text 编码”,也就是 OT18rTeX Base 1,用来承接原始 Type 1,正是 T1 虚拟字体所依托的实际字体——上一节中 ptmr8t.vfMAPFONT 里写着 ptmr8r,说的就是这件事。顺带一提,Fontname 也承认八个字符过于局促,规范本身专辟一节讨论长名字的处理。

日文的度量 — JFM 与字符类

日文排版需要西文 .tfm 提供不了的东西。汉字和假名默认位于 正方形(全角)框 中,字与字之间不是词间空格:间隙只在 标点周围(括号、句读点)张合。承载这些信息的结构就是 JFM(Japanese Font Metric)。pTeX 和 upTeX 使用类似 .tfm 的二进制 JFM;LuaTeX-ja 则用 Lua 表 表达同一角色。前者可用 ppltotf / ptftopl 与文本互转;后者本身就是文本,直接读即可。

与西文 TFM 最大的区别在于,JFM 把字符归入“字符类(character class)”来处理。按 LuaTeX-ja 手册的规定,JFM 中所有尺寸都写成 以设计尺寸为单位的浮点数,以 zw(全角宽度)和 zh(全角高度,即 height + depth)为基准;每个类各有自己的 widthheightdepthitalic(斜体校正)。类 0 必定存在,容纳不属于其他任何类的绝大多数日文字符。标点分入其他类,并用表记录 在一个类与另一个类之间插入的 glue/kern——这就是标点周围空白的生成机制。

jfm.lua
-- A LuaTeX-ja JFM (excerpt); all lengths are in design-size units
luatexja.jfont.define_jfm {
  version = 3,
  dir = 'yoko',            -- horizontal writing
  zw = 1.0, zh = 1.0,       -- full width, full height
  [0] = {                  -- class 0: most kanji and kana
    chars = { '漢' },
    width = 1.0, height = 0.88, depth = 0.12, italic = 0.0,
  },
  [1] = {                  -- a class for stops and commas
    chars = { '。', '、' },
    width = 0.5, height = 0.88, depth = 0.12, italic = 0.0,
  },
}

在 pTeX 系列中,JAglue(插入在两个日文字符之间,以及日文字符与西文之间的间距)会在排版时自动加入。类间间距由 JFM 决定;未指定类的日文字符之间使用默认的 kanjiskip,日西边界处使用 xkanjiskip。它们与禁则处理(kinsoku,即禁止某些字符出现在行首或行末的规则)一起,支撑着日文排版的体裁。LuaTeX-ja 随附面向不同用途的标准 JFM:jfm-ujis.lua 是默认,基于 upTeX 使用的度量 upnmlminr-h.tfm(多数字符为正方形);jfm-jis.lua 对应 pTeX 中广泛使用的 jis.tfmjfm-min.lua 对应 pTeX 默认的 min10.tfm。在字体定义中用 jfm= 选择,例如 \jfont\F=HaranoAjiMincho-Regular:jfm=ujis