只有一行的 LaTeX 文档——\documentclass{article}\begin{document}Hi\end{document}——交给 pdflatex,得到的是 11,529 字节的 PDF;把一模一样的源码交给 latex,出来的 DVI 文件只有 248 字节。相差四十六倍。两者描述的是同一页、同一个「Hi」,差距究竟去了哪里?这个问题的答案,正好把 TeX 的整条处理流程讲清楚。本页跟着实测数字,走一遍从源码经引擎到输出的路径,看看 .dvi 里到底装了什么,以及为什么经由 DVI 的老路线至今没有消失。
为什么不是所见即所得:批处理这个选择
TeX 之所以不随打随刷屏,是因为它把断行放在整段的尺度上做优化。它不是一行行往下填,而是通盘评估整个段落——哪一组断点看起来最不难看——然后选出总代价最小的方案。后果之一是:在段末加一个字符,可能会改变这一段第一行的折行位置。为每次按键重算全部段落,代价太高,不划算。
于是 TeX 选择了批处理:把稿件读到底,再一次性排完全篇。放弃的是边打字边看结果的舒适,换来的是全局最优的排版、长文档中始终不走样的体例,以及纯文本带来的自动化——用脚本生成源码、在 Git 里看差异、在服务器上无人值守地构建。工作节奏于是变成「写 → 编译 → 看 PDF」的循环。下面讲的几乎全部内容,都是为了让这个循环转得更快。
从源码到输出的两条路线:直出 PDF 与经由 DVI
引擎要么直接写出 PDF,要么写出一个叫 DVI 的中间文件。 直出 PDF 的是 pdflatex、xelatex、lualatex(依次对应 pdfTeX、XeTeX、LuaTeX 引擎);写 DVI 的是 latex,以及日文的 platex、uplatex。写出 DVI 并不算完,还要把它交给另一个叫 dvi 驱动 的程序,转换成你真正想要的格式——dvipdfmx 转 PDF,dvips 转 PostScript,dvisvgm 转 SVG。把它画到屏幕上的驱动,则叫 dvi 阅读器。
# Direct to PDF, one step
lualatex document.tex # -> document.pdf
pdflatex document.tex # -> document.pdf
# Via DVI, two steps (the Japanese route)
uplatex document.tex # -> document.dvi
dvipdfmx document.dvi # -> document.pdf
# Other dvi drivers, same input
dvips document.dvi # -> document.ps
dvisvgm document.dvi # -> document.svg这里最容易混淆的一点是:「要不要经过 DVI」和「能不能用 Unicode 与系统字体」是两条互不相干的轴。 pdflatex 不经 DVI 直接输出 PDF,但它对输入的处理方式仍是老一套,没法直接指定装在你机器上的 OpenType 字体。反过来,xelatex 和 lualatex 原样读取 Unicode,可以用 fontspec 直接按名字指定字体。把这两条轴分开看,一堆名字立刻就理顺了。
| 命令 | 输出 | 输入与字体 | 可用图片格式 |
|---|---|---|---|
latex | DVI(需要 dvi 驱动) | 以 ASCII 为主;TeX 自有的字体格式 | EPS、PS |
pdflatex | PDF(直出) | Unicode 支持有限;用不了系统字体 | PNG、JPEG、PDF(EPS 自动转换) |
uplatex | DVI(交给 dvipdfmx) | 支持 Unicode 的日文引擎;和文字体是否嵌入可配置 | EPS、PDF、PNG、JPEG |
xelatex | PDF(内部经由 .xdv) | Unicode;用 fontspec 指定系统 OpenType 字体 | PNG、JPEG、PDF、EPS |
lualatex | PDF(直出) | Unicode;系统字体;内部可用 Lua 扩展 | PNG、JPEG、PDF、EPS |
.dvi 里到底装了什么:打开那 248 字节
DVI 是 device independent(与设备无关)的缩写,它的内容只是一串「把哪个字符放在哪个位置」的指令。用 TeX Live 自带的 dvitype 可以把它展开成人能读的形式。下面就是从开头那个一行文档生成的 248 字节 DVI 里,实际装着的东西。
$ dvitype -output-level=4 one.dvi
numerator/denominator=25400000/473628672
magnification=1000
' TeX output 2026.08.13:1233'
maxv=41484288, maxh=26673152, maxstackdepth=3, totalpages=1
Font 27: cmr10---loaded at size 655360 DVI units
42: beginning of page 1
117: down4 41484288 v:=0+41484288=41484288
140: right3 5046272 h:=0+5046272=5046272
144: fntdef1 27: cmr10
165: fntnum27 current font is cmr10
166: setchar72 h:=5046272+491521=5537793
167: setchar105 h:=5537793+182045=5719838
[Hi]
181: setchar49 h:=15204352+327681=15532033
[ 1]
185: eop这就是全部内容。setchar72 的意思是「把字符码 72(H)放在这里」,setchar105 是「放 105(i)」,setchar49 则是页脚那个页码 1。right3 和 down4 用来移动当前位置,而单位——一个 DVI unit 就是 1 sp,即 1/65536 pt——所以 cmr10 的「655360 DVI units」正好是 10 pt。最关键的一行是 fntdef1 27: cmr10:DVI 只是按名字点了这个字体。 至于 cmr10 长什么样,文件里任何地方都没有写。
PDF 恰好相反。翻开 pdflatex 产出的那个 11,529 字节的 PDF,字体是嵌进去的,名字叫 UNYBJV+CMR10。开头六个字母是子集化的标记,证明只抽出了真正用到的字形。而装着这份字体的对象写着 /Length1 1394 /Length2 8300 /Length3 0 /Length 9259——压缩之后仍有 9,259 字节,占整个 11,529 字节文件的八成。四十六倍的差距,基本上就落在这里。 DVI 只需写「cmr10 的 H」,PDF 却必须随身带着那个 H 的轮廓本身。这是 PDF 承诺「拿到哪里看都一样」所付的代价。
不过这里有一点要留意。「DVI 小是因为它不带字体」这句没错;但「PDF 有 11,529 字节是因为它带了字体」这句还不足以解释全部。 把同一个 248 字节的 DVI 交给 dvipdfmx,出来的 PDF 是 1,950 字节。它同样嵌入了字体,只是转换成了压缩率高得多的 /Subtype/Type1C 形式,字体本身只占 546 字节。也就是说,同一页、同一个词,仅仅因为描述方式不同,就在 248、1,950、11,529 字节之间变化。当文件大小要紧时,走哪条路线是会真的体现出来的。
DVI 路线为什么至今还活着:日文与 .xdv
DVI 之所以留存,最有力的实务理由是:它正是日文排版的标准路线。 platex 和 uplatex 输出 DVI,最后由 dvipdfmx 转成 PDF。这个程序脱胎自 Mark A. Wicks 的 dvipdfm,为满足日文需求而扩展;TeX Live 2024 里带的是 2024 年 3 月版。它的活儿是把 DVI 的定位指令翻译成 PDF 的绘制操作,并找出 DVI 引用的字体加以嵌入。实际用 upLaTeX 跑一份只有一行日文的文档,396 字节的 DVI 变成 5,998 字节的 PDF,里面嵌着作为 CID 字体的和文字体 HaranoAjiMincho。
而且这套机制比想象的更「在役」。在 TeX Live 2024 里查一下 dvipdfmx 到底是什么,会发现它是一个指向 xdvipdfmx 的符号链接——那正是用来读取 XeTeX 所产出 .xdv 文件的程序。换句话说,XeTeX 内部是先输出一种略作扩展的 DVI,再交给驱动转成 PDF:这就是不折不扣的 DVI 路线。 用 xelatex -no-pdf 就能证明:它会在转换之前停下,把 .xdv 留在那里。那份一行文档的 .xdv 是 436 字节;交给 xdvipdfmx 得到 2,329 字节的 PDF,与对同一份源码直接运行 xelatex 的输出大小完全一致。手动把工序拆成两步,只是把 xelatex 内部本来就在做的事显露出来而已。
$ readlink $(which dvipdfmx)
xdvipdfmx
$ xelatex -no-pdf one.tex # stop before the driver stage
$ ls -l one.xdv
-rw-r--r-- 1 user staff 436 one.xdv
$ xdvipdfmx one.xdv # run the driver by hand
$ ls -l one.pdf
-rw-r--r-- 1 user staff 2329 one.pdf实务上的取舍很简单。如果是新开一份英文或其他拉丁文字的稿子,就走直出 PDF 的路线(pdflatex 或 lualatex):少一道工序,就少一处出岔子的地方。如果写日文,而且手上有既有资料或实验室的既定做法,那么 DVI 路线(uplatex 加 dvipdfmx)依然稳妥,它在竖排和日文避头尾规则上积累了长期的实绩。完整的权衡留给「如何选择引擎」那一页,但至少不必把 DVI 当成「因为老所以要躲开」的东西。
辅助文件与运行次数:把 BibTeX 和 makeindex 也算进来
「引擎跑一次就完事」,只对既无引用、又无目录、也无参考文献和索引的文档成立。 引擎在一次运行中会写出 .aux(编号与页码的记录)和 .toc(目录),下一次运行再把它们读回来。反复到收敛为止是基本形态,而只要出现一个 \ref,就至少需要两遍。加入参考文献和索引之后,循环里还会插进引擎以外的程序——BibTeX 读 .aux 写出 .bbl(或者 biber 读 .bcf),makeindex 读 .idx 写出 .ind。这些产物又要被引擎读入,于是执行顺序就成了下面这样。
# A document with cross-references, a bibliography and an index
pdflatex thesis # writes .aux, .idx; references still print as ??
bibtex thesis # reads .aux -> writes .bbl
makeindex thesis # reads .idx -> writes .ind
pdflatex thesis # pulls .bbl and .ind in; numbering shifts again
pdflatex thesis # everything settles
# Or simply
latexmk -pdf thesis # figures out the order and the count on its own别再数次数了:latexmk、编辑器与 SyncTeX
latexmk 会盯着辅助文件的变化,按文档实际需要自动重跑相应次数。 当 .aux 之类的内容与上一次一致时,它判定构建已经收敛并停下;中途需要 BibTeX/biber 或 makeindex 就顺手调用。默认最多重复五次($max_repeat = 5),再不稳定就当成死循环中断。真实文档基本碰不到这个上限。想走 DVI 路线也一样:把路线写进 latexmk 的配置文件,仍旧一条命令搞定。
latexmk -pdf document.tex # pdfLaTeX, as many passes as needed
latexmk -lualatex document.tex # LuaLaTeX
latexmk -pv document.tex # build, then open the viewer
latexmk -c # remove .aux, .log, .toc and friends
latexmk -C # the same, and remove the PDF tooVS Code 的 LaTeX Workshop、TeXShop、TeXstudio、Overleaf 之类的环境,通常都在背后调用 latexmk,所以按下「编译」时,次数问题其实已经替你解决了。另一件值得打开的是 SyncTeX:它会把 PDF 上每个位置来自源码第几行记录进 .synctex.gz。启用之后,在 PDF 上点一下就能跳到源码对应行,反过来也能从源码跳到 PDF 的对应位置。文档越长,它越值。
构建停下时,该怀疑哪一个阶段
报错来自哪个阶段,就决定了该去哪里找。 .log 里出现引擎的错误,说明问题在稿件这一侧;dvi 驱动在抱怨,那就指向图片或字体;BibTeX/biber 的输出,则指向 .bib。最容易搞混的症状是「图片出不来」,而这正是路线差异的直接体现:DVI 路线顺手处理 EPS,直出 PDF 的路线顺手处理 PDF、PNG、JPEG,所以想在用 latex 编译的文档里插入 PNG 而失败,是非常常见的入口。日志本身怎么读,由「排查错误」那一页详细讲解。
- 只有交叉引用不对时,别急着删辅助文件:交给
latexmk,让它按需要重跑几遍。 - 只有图片出不来时,先确认当前走的是直出 PDF 还是 DVI,再把文件转成该路线能接受的格式。
- 症状反复出现又说不清原因时,才删掉
.aux、.toc、.out重新构建(latexmk -c)。如果祸首是过期的辅助文件,这样就好了。 - 日文 PDF 里字体变了样时,怀疑
dvipdfmx的嵌入配置。这是驱动的事,不是引擎的事。