运行 pdftex --version,得到的是三段拼起来的数字:pdfTeX 3.141592653-2.6-1.40.26。第一段并不属于 pdfTeX,而是 Knuth 的 TeX 收敛到 π 的中途状态,XeTeX 和 LuaTeX 也都继承了这串数字。pdfTeX 就是在那个 TeX 上加了一件事:直接写出 PDF。敲 pdflatex 时真正跑起来的正是它,它至今仍是 LaTeX 的默认引擎。本页讲清楚它为何一直是默认、字体为何要经过 TFM 度量与 map 文件这一套老设计,以及 pdfTeX 真正的贡献——microtype 打开的微观排版:字符突出与字体伸缩。下面每一条结论都来自在 TeX Live 2024 上的实际运行。
pdflatex、latex 与 pdftex 的区别
三者是同一个可执行文件。 打开 TeX Live 2024 的 bin 目录会看到,pdflatex 和 latex 都是指向 pdftex 的符号链接,amstex 和 csplain 也一样。真正不同的只有启动时载入的格式。格式是一个 .fmt 文件,里面装着预先展开并冻结的一整套宏;fmtutil.cnf 里那行 pdflatex pdftex ... *pdflatex.ini 的意思是:以 pdflatex 这个名字被调用时,就用 pdftex 引擎加载 LaTeX 的宏来运行。所以「引擎(程序)」和「格式(命令体系)」是两根不同的轴,pdfLaTeX 不过是「pdfTeX 引擎 × LaTeX 格式」这个组合的名字。
$ readlink $(which pdflatex) latex amstex
pdftex
pdftex
pdftex
$ grep -E '^(latex|pdflatex|pdftex) ' $(kpsewhich fmtutil.cnf)
latex pdftex language.dat -translate-file=cp227.tcx *latex.ini
pdflatex pdftex language.dat -translate-file=cp227.tcx *pdflatex.ini
pdftex pdftex language.def -translate-file=cp227.tcx *pdfetex.ini从这张表还能顺带看出两件事。其一,latex 这条命令根本不是「旧 TeX」,而是 以 DVI 模式运行的 pdfTeX。运行它,横幅会打出 This is pdfTeX, ... (preloaded format=latex),产出的是 .dvi。其二,pdftex 格式是由 pdfetex.ini 生成的——那个 e 指 e-TeX,也就是 \protected、\unexpanded、扩容的寄存器、\middle 这一组现代 LaTeX 视为理所当然的扩展。在三个引擎上分别检查 \eTeXversion 是否已定义,pdfTeX、XeTeX、LuaTeX 全都回答「已定义」。e-TeX 早已不是可选项。相反,\pdftexversion 只存在于 pdfTeX,宏包正是靠它来判断自己跑在哪个引擎上。
直接写出 PDF——以及用 \pdfoutput=0 退回 DVI
决定输出格式的只有一个整数参数 \pdfoutput:正值表示 PDF,0 表示经典的 DVI。Knuth 最初的 TeX 只吐出 DVI(device independent),转成 PostScript 或 PDF 是另一个程序的活;pdfTeX 把那一步并进了页面构建之后,却并没有堵死 DVI 这个出口。依赖 PostScript \special 的宏包——尤其是 PSTricks——有时仍需要走 DVI → PS → PDF 这条路,\pdfoutput=0 正是为此保留的。
陷阱在于写在哪里。\pdfoutput 必须在第一页被输出之前定下来;改晚了,pdfTeX 就会停在 ! pdfTeX error (setup): \pdfoutput can only be changed before anything is written to the output.,紧接着是 ! ==> Fatal error occurred, no output PDF file produced!,这一趟既不留 PDF 也不留 DVI。由于 LaTeX 的驱动判定发生得相当早,稳妥的位置是文件最顶端,写在 \documentclass 之前。
% Force DVI output even when the file is compiled with pdflatex.
% This line must come before \documentclass.
\pdfoutput=0
\documentclass{article}
\begin{document}
This run writes a .dvi file, not a .pdf.
\end{document}microtype 到底做了什么:字符突出与字体伸缩
\usepackage{microtype} 这一行打开的是两套彼此独立的机制。打开 .log,两行并排出现:Package microtype Info: Character protrusion enabled (level 2). 和 Package microtype Info: Automatic font expansion enabled (level 2), stretch: 20, shrink: 20, step: 1。前者是字符突出(也叫边缘微调)——让行末的句点、连字符稍稍探出版心,使机械对齐的边缘在视觉上反而更平直,这是活字时代「悬挂标点」的一般化。后者是字体伸缩——字形可在水平方向伸缩至多 2%(20/1000),而这点余地会被并入断行决策本身。词间空白因此变得均匀,段落里那些纵向的白色「河流」也随之变细。
这两项都不是 TeX 的发明。源头是 hz-program——字体设计师 Hermann Zapf 自 1988 年起与 Peter Karow 等人在汉堡 URW 公司开发的排版程序,目标是「没有空洞、也没有河流的均匀灰版面」。URW 为它申请了专利(欧洲专利 EP 0466953,权利于 2010 年 7 月到期),算法最终转到 Adobe,被写进了 InDesign。可同一个想法还走了另一条路:越南人 Hàn Thế Thành 分析了 hz,并把它实现在 TeX 里,作为他在捷克马萨里克大学信息学院的博士论文《Micro-typographic extensions to the TeX typesetting system》(2000 年 10 月,导师 Jiří Zlatuška)。同一个想法,一头进了商业软件的顶端,一头成了人人可用的免费实现——\usepackage{microtype} 属于后者。
\documentclass{article}
\usepackage{microtype} % protrusion + expansion, sensible defaults
\begin{document}
With microtype loaded, pdfTeX nudges punctuation into the margin
and flexes glyph widths by a hair, so justified text looks far
more even. Nothing else in the document has to change.
\end{document}这两套也可以用原始 primitive 直接驱动。突出一侧是 \pdfprotrudechars(0 关闭,1 开启,2 连宽度计算也一并反映)配合 \lpcode(左边缘)与 \rpcode(右边缘),逐字设定突出量。伸缩一侧是 \pdfadjustspacing(=2 把伸缩并入断行)、逐字设定拉伸意愿的 \efcode,以及声明字体伸缩实例的 \pdffontexpand。实际写作中几乎用不到它们——microtype 为每种字体备有配置文件(Computer Modern 对应 mt-cmr.cfg),会替你挑好数值。不过记住这些名字仍有价值:日志里的警告因此变得可读。
| 原语 | 所属功能 | 控制什么 |
|---|---|---|
\pdfprotrudechars | protrusion | 0 关闭,1 开启,2 连宽度计算也反映 |
\lpcode / \rpcode | protrusion | 逐字设定左右边缘的突出量(千分比) |
\pdfadjustspacing | expansion | =2 时把伸缩并入断行决策 |
\efcode | expansion | 逐字的可拉伸程度;XeTeX 没有这个 |
\pdffontexpand | expansion | 声明字体的伸缩实例 |
字体由 TFM 度量与 map 文件决定
pdfTeX 从两个不同的地方分别取得度量和字形。度量来自 .tfm(TeX Font Metric):kpsewhich cmr10.tfm 返回 fonts/tfm/public/cm/cmr10.tfm,这个文件里只有每个字符的宽、高、深和一张紧排表,一条轮廓也没有。只有排版结束、要组装 PDF 的时候,才会去查 map 文件,读取其中写明的真实字体(Type 1 的 .pfb 或 TrueType)。cmr10 对应的条目只有一行:cmr10 CMR10 <cmr10.pfb——依次是 TFM 名、PostScript 名,< 表示「嵌入这个文件」。TeX Live 2024 上由 updmap 生成的 pdftex.map 长达 45,443 行;这一个文本文件就是整套发行版所有字体的对照表。
$ kpsewhich cmr10.tfm cmr10.pfb
/usr/local/texlive/2024/texmf-dist/fonts/tfm/public/cm/cmr10.tfm
/usr/local/texlive/2024/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb
$ grep -m1 '^cmr10 ' $(kpsewhich pdftex.map)
cmr10 CMR10 <cmr10.pfb
# The same one-line document, compiled by three engines:
$ pdffonts min-pdflatex.pdf
INJFRF+CMR10 Type 1 Builtin yes yes yes
$ pdffonts min-xelatex.pdf
HOLJOD+LMRoman10-Regular-Identity-H CID Type 0C Identity-H yes yes yes
$ pdffonts min-lualatex.pdf
YIRABR+LMRoman10-Regular CID Type 0C Identity-H yes yes yes把同一份单行文档用三个引擎各排一遍,再用 pdffonts 一看,设计上的差别就直接落在了输出里。pdfLaTeX 嵌入的是 Type 1 的 CMR10;XeLaTeX 和 LuaLaTeX 嵌入的是 CID Type 0C 的 LMRoman10-Regular。页面看上去同样是 Computer Modern,但走的路径和嵌进去的文件截然不同。map 文件可以在文档里用 \pdfmapfile 和 \pdfmapline 追加——有意思的是,这两条在 XeTeX 里也还留着。8 位时代的遗产活得比想象中更广。
8 位引擎的上限:! Bad character code,以及 fontspec 为何拒绝运行
一套字体最多 256 个字符。 这是 pdfTeX 最硬的天花板。写 \font\x=cmr10 \x \char"1234,它就停在 ! Bad character code (4660).。把同样的输入喂给 LuaTeX 则毫无反应——它内部的字体是「宽」的。正因为有这堵 256 格的墙,带重音的拉丁字母才必须重新装进 T1 之类的字体编码,希腊字母和各种符号才要挤到另外的字体里去。看清这个天花板,也就明白 fontenc 到底是干什么用的了。
误解多半出在输入这一侧。UTF-8 的源文件本身,在 2018 年以后的 LaTeX 里不载入 inputenc 也能通过。 但能通过的只有「LaTeX 已经声明过的字符」。正文里写 → 的 .tex 用 pdflatex 编译毫无问题;换成 日,就会停在 ! LaTeX Error: Unicode character 日 (U+65E5) not set up for use with LaTeX.。也就是说 pdfTeX 的限制不是「读不了 UTF-8」,而是「只能排已经安排好的字符」。而且它没有按名字调用操作系统 OpenType 字体的通道:一载入 fontspec,这一趟立刻死在 ! Fatal Package fontspec Error: The fontspec package requires either XeTeX or LuaTeX.。这三件事——256 格、只认已声明字符、用不了系统字体——几乎解释了所有转向 XeTeX 或 LuaTeX 的动机。
- 留下的理由:与既有模板兼容、速度,以及三十年的实战检验。以英文为主的论文,
pdflatex是意外最少的选择。 - 离开的理由:多语言 Unicode、操作系统字体、日文排版、基于 Lua 的处理。只要真需要其中任何一项,换引擎都比继续堆变通办法划算。
- 常见事故:仍在 pdfLaTeX 上却载入
fontspec。fontspec属于 XeLaTeX 与 LuaLaTeX。 - 另一种事故:把
\pdfoutput写在\documentclass之后。这一趟会死掉,既不留 PDF 也不留 DVI。
图像:PNG、JPEG、PDF 直接进,EPS 自动转换
pdfTeX 能直接读入的图像格式有三种——PNG、JPEG、PDF——底层入口是 \pdfximage,日常使用则由 graphicx 的 \includegraphics 架在上面。只有 EPS 必须先转成 PDF,而如今这一步也是自动的:在 TeX Live 默认开启的 restricted shell escape 下,\includegraphics{fig.eps} 会悄悄调用 repstopdf,在旁边生成 fig-eps-converted-to.pdf 再嵌入。陷阱在这里:若以 -no-shell-escape 运行,转换不会发生,而且既无错误也无警告。取而代之的是一个印着文件名的空框,PDF 照样正常产出。CI 里插图凭空消失,多半就是这么来的。
因为 PDF 是它自己写的,pdfTeX 也提供了一组直通 PDF 功能的原语。\pdfliteral 直接注入原始的 PDF 绘图算子;\pdfobj 创建 PDF 对象;\pdfannot 放置注释(链接、表单控件等)。hyperref 的链接与书签就建在这一层之上。文档级设置则有写入标题、作者等元数据的 \pdfinfo,决定压缩强度的 \pdfcompresslevel,以及给每页附加属性的 \pdfpageattr。
\pdfinfo{
/Title (My Report)
/Author (A. Author)
}
\pdfcompresslevel=9
% a thin rule drawn with a raw PDF operator
\pdfliteral{0 0 m 100 0 l 0.4 w S}| 原语 | 在 pdfTeX 中的作用 | 在 LuaTeX 中的写法 |
|---|---|---|
\pdfoutput | 决定输出 PDF 还是 DVI | \outputmode |
\pdfliteral | 注入原始 PDF 绘图算子 | \pdfextension literal |
\pdfobj / \pdfannot | 直接创建 PDF 对象或注释 | \pdfextension obj / annot |
\pdfinfo | 写入标题、作者等元数据 | \pdfextension info |
\pdfcompresslevel | 输出 PDF 的压缩级别(0–9) | \pdfvariable compresslevel |
\pdfximage | 读入 PNG / JPEG / PDF 图像 | \saveimageresource |
\pdfsavepos | 记录当前在页面上的位置 | \savepos |
\pdfprotrudechars | 启用字符突出 | \protrudechars |
\pdfadjustspacing | 启用字体伸缩 | \adjustspacing |
pdfTeX 的 \pdf... 在后来的引擎里留下了什么
LuaTeX 是在 pdfTeX 之上造出来的,继承了其中大部分机制,但臃肿的 \pdf... 命名空间被整理过了。 其中许多如今经由三个入口——\pdfextension、\pdfvariable、\pdffeedback——接收关键字与参数。\pdfliteral 变成 \pdfextension literal,\pdfoutput 变成 \outputmode,\pdfximage 变成 \saveimageresource,\pdfprotrudechars/\pdfadjustspacing 变成 \protrudechars/\adjustspacing。另一方面,\lpcode、\rpcode、\efcode 名字原封不动。pdfTeX 时代的知识几乎可以照搬,只是拼写会变。
XeTeX 的继承是半吊子的,而这半吊子恰恰影响实务。在 TeX Live 2024 上逐个探测原语可以看到:XeTeX 有 \lpcode 和 \rpcode,却 没有 \efcode;它的突出开关也不叫 \pdfprotrudechars,而是另起名字的 \XeTeXprotrudechars。所以在 XeLaTeX 下,字符突出有效,而字体伸缩根本无法实现;若用 \usepackage[expansion=true]{microtype} 明确要求,这一趟就会停在 ! Package microtype Error: Font expansion does not work with xetex.。不带选项载入时,日志里关于伸缩的那一行只是悄悄消失——于是常有人觉得「装了 microtype 却效果不明显」,却不知原因。
今天什么时候该选 pdflatex
如果文档以英文为主、投稿方或合作者已经定好模板、而且截稿在即,那就用 pdfLaTeX。 在这台机器上,用同一份 417 页的数学文档计时,pdfLaTeX 是三个引擎中最快的,而且最不容易被既有宏包绊倒。它对你的要求也很少:加一行 \usepackage{microtype},插图用 PNG / JPEG / PDF,若依赖 EPS 就别关掉 shell escape。反过来,为了需要中日文或系统字体的文档而在 pdfTeX 上不断堆变通办法并不划算;越过那条线,活儿就归 XeLaTeX 或 LuaLaTeX 了。pdfTeX 自身如今基本处于维护状态,新开发已转向 LuaTeX——但 pdftex.map 那 45,443 行和三十年积累的模板资产,短期内哪儿也不会去。