把同一个单页 LaTeX 文档编译两次:DVI 是 956 字节,而用 dvips 从它生成的 PostScript 是 169,769 字节——相差 178 倍。多出来的部分没有一个字节是新信息。唯一的区别在于,DVI 只是按名字点到九个字体,而 PostScript 把这九个整个装了进去。这一点几乎解释了 DVI 是什么、PostScript 为何在二十年间一直是中转站,以及今天通往 PDF 的三条路线有何不同。
DVI 文件里到底有什么:盒子与字体名
DVI 里只有位置和字体引用,一个字形也没有。用 TeX Live 自带的 dvitype 读一遍,这一点看得清清楚楚。fntdef1 27: cmr10 只是一句声明:27 号字体是 cmr10;setchar49 也只是一条指令:把当前字体的第 49 号字符放在这里。49 是字符码,也就是 1。夹在中间的 push、down4、right3 负责挪动当前点。也就是说,DVI 是一份「在哪里、用哪个字体、放第几号字符」的指令清单,而这些字符究竟长什么样,它一无所知。
dvitype -output-level=4 -page-start='*' doc.dvi
# numerator/denominator=25400000/473628672
# magnification=1000; 0.00006334 pixels per DVI unit
# Postamble starts at byte 723.
# Font 27: cmr10---loaded at size 655360 DVI units
# Font 37: cmbx12 scaled 1200---loaded at size 943718 DVI units
# (this font is magnified 120%)
# 145: fntdef1 37: cmbx12
# 167: fntnum37 current font is cmbx12
# 168: setchar49 h:=4063232+530841=4594073, hh:=291这既是「DVI 为什么小」的原因,也是「DVI 为什么单独看不了」的原因。对那 956 字节运行 strings,能读出来的只有正文里的单词,加上一串字体名:cmr10、cmmi7、cmsy10、cmex10 等等。因此拿到 DVI 的人,如果在自己机器上找不到同名字体,就无法还原这一页。这种分工正是 1980 年代的用意所在——排版结果只停在一个与设备无关(device-independent)的中间格式,至于如何适配真实的纸张或屏幕,交给后面的驱动程序去做。DVI 这个名字本身就是设计方针。
| DVI 有/没有 | 含义 |
|---|---|
setchar / put | 指定字符编号;不含字形本身 |
fntdef / fntnum | 仅按名称、设计尺寸和校验和引用字体 |
push / pop / down / right | 盒子的定位操作,也就是排版结果本身 |
xxx (\special) | DVI 无法表达之物的逃生口,内容交由驱动程序解释 |
glyph outlines | 没有。所以缺少字体就无法显示 |
color, paper size | 不属于 DVI 本身;两者都通过 \special 传递 |
DVI 为什么用 1/65536 磅来度量一切
答案就印在 dvitype 输出的开头。numerator/denominator=25400000/473628672 中的分母 473628672 等于 65536 × 7227,而 7227 磅正好是 100 英寸。于是 一个 DVI 单位就是 1/65536 磅,也就是 TeX 内部使用的整数单位「缩放点(sp)」。所以上面那句 cmr10---loaded at size 655360 DVI units 不过是 10 × 65536,即 10pt;而 cmbx12 scaled 1200 报出的 943718,是 12pt × 1.2 = 14.4pt 再乘以 65536。TeX 完全不用浮点数,只靠这些整数来排版——同一份源文件在任何机器上都能得到精确到 1 sp 的相同结果,原因就在这里。
设计这一格式的是高德纳的学生 David R. Fuchs,时在 1979 年;规范以「The format of TeX’s DVI files」为题,发表于 1982 年 10 月的 TUGboat(第 3 卷第 2 期)。四十多年过去,doc.dvi 的首字节依然是 247(pre 操作码),第二个字节依然是 2,即 DVI 的识别号。兼容性之所以没有断,是因为这一格式对设备不作任何假设——它当年面向的输出机器早已消失,而唯独那个什么都不假设的部分留了下来。
PostScript 为何长期充当中转站:看看 \special 里面
DVI 与设备无关,代价是它表达不了颜色、图像和旋转。于是 Fuchs 只留了一个逃生口:\special{...}——内容一概不作解释,原样交给驱动程序。而最先填满这个口子的,正是 PostScript。加载 \usepackage{graphicx},写下 \rotatebox{30}{rotated},用 latex 生成 DVI 再打开看:里面就躺着未经加工的 PostScript。DVI 甚至不知道那是 PostScript,它只是搬运了一串字符。
# what latex actually wrote into the DVI for \rotatebox and \textcolor
dvitype -output-level=4 -page-start='*' spec.dvi | grep xxx
# 88: xxx 'header=l3backend-dvips.pro'
# 116: xxx 'papersize=614.295pt,794.96999pt'
# 247: xxx 'color push rgb 1 0 0'
# 347: xxx 'ps: gsave currentpoint currentpoint translate
# 30 neg rotate neg exch neg exch translate'
# 449: xxx 'ps: currentpoint grestore moveto'此后就成了一条单行道。既然 DVI 的逃生口塞满了 PostScript,能把这份 DVI 解释到底的就只有懂 PostScript 的设备。1980 年代中期,搭载 Adobe PostScript 的激光打印机成为事实标准,由 dvips 把 DVI 译成 PostScript 的路线也就固定下来。翻译时 dvips 会把字体嵌进去:生成的 .ps 开头有一行 %%DocumentFonts: CMBX12 CMR10 CMEX10 CMSY7 CMR7 CMMI10 CMMI7 CMR5 CMSY10,正文中随后是这九个字体各自的 %%BeginFont: 区块。本页开头 956 字节与 169,769 字节之差,正是这九个字体。
PostScript 已退居幕后,指纹却处处可见。直接把 DVI 变成 PDF 的 dvipdfmx 可执行文件里,带着一个用于执行内联 PostScript 的小解释器,连诊断信息 Stack not empty after execution of inline PostScript code. 都齐备。所以上面那条 ps: 特殊命令在这条路线上依然被当作真正的旋转来处理。它的 --help 里还列着 -D template PS->PDF conversion command line template [none],那是把自己搞不定的 PostScript 交给外部程序(如 Ghostscript)的接口。再者,记录图像尺寸的 .xbb 文件内容是 %%BoundingBox: 0 0 8 8——正是 PostScript 的注释写法,至今仍在服役。
今天的三条路线,各自的代价
西文就用 pdflatex 之类直接出 PDF;中日文用 (u)platex → dvipdfmx;手里攒着 PSTricks 或成堆旧 EPS 就走 latex → dvips → ps2pdf。实务上的分叉就这么多。但「同一份文档就会得到同一个 PDF」并不成立。把本页开头那份文档沿三条路线各出一次,得到 direct.pdf 85,509 字节、viadvi.pdf 14,693 字节、viaps.pdf 17,851 字节。用 pdffonts 一看便知:pdfTeX 按 Type 1 原样嵌入,而 dvipdfmx 与 Ghostscript 会先转成压缩的 Type 1C 再嵌入。页面看上去一模一样,体积却差了六倍。
| 路线 | 经过的格式 | 适用场景 |
|---|---|---|
pdflatex / lualatex | .tex → PDF | 一步到位;西文的默认选择 |
xelatex | .tex → XDV → PDF | 系统字体;XDV 那一步只是被隐藏了 |
dvipdfmx | .tex → DVI → PDF | 日文((u)pLaTeX)的定番;可直接放置 PNG、JPEG |
dvips + ps2pdf | .tex → DVI → PS → PDF | PSTricks、成套 EPS、以 PostScript 为前提的交稿 |
# 1. straight to PDF
pdflatex doc.tex
# 2. via DVI (the Japanese route)
uplatex doc.tex && dvipdfmx doc
# doc.dvi -> doc.pdf
# [1]
# 14692 bytes written
# 3. via PostScript
latex doc.tex && dvips doc -o doc.ps && ps2pdf doc.ps
# in practice latexmk drives all three for youXeLaTeX 真的跳过了 DVI 吗
并没有。执行 xelatex -no-pdf doc.tex,会出现 Output written on doc.xdv (1 page, 2476 bytes).,并留下 doc.xdv。看它的首字节,是 247——与 doc.dvi 完全相同的 pre 操作码。不同的只有下一个字节:DVI 是 2,XDV 是 7。也就是说,XeTeX 写出的是扩展 DVI(XDV),而把它变成 PDF 的是 xdvipdfmx。TeX Live 附带的 README 也写着「In the installation, dvipdfmx is a symlink to xdvipdfmx.」,实际上 dvipdfmx 与 extractbb 指向的正是同一个可执行文件。所谓「直接输出 PDF」,意思是把 DVI 那一步藏了起来,而不是取消了它。
xelatex -no-pdf doc.tex
# Output written on doc.xdv (1 page, 2476 bytes).
# same container, different id byte:
# doc.dvi byte 0 = 247 (pre) byte 1 = 2
# doc.xdv byte 0 = 247 (pre) byte 1 = 7
xdvipdfmx doc.xdv # this is what xelatex runs for you遇到 Cannot determine size of graphic ... (no BoundingBox) 时
在 DVI 路线上插入 PNG 或 JPEG 的人,最先撞上的就是它。完整信息是 ! LaTeX Error: Cannot determine size of graphic in sample.png (no BoundingBox).,原因既不在图片,也不在 dvipdfmx,而在于 graphicx 的驱动选项。latex 不生成 PDF,因此没有自行读取图像尺寸的能力;默认的 dvips 驱动只会读 PostScript 的 %%BoundingBox 行,而 PNG 的文件头不是。解决办法有两个:显式写明驱动 \usepackage[dvipdfmx]{graphicx},或先运行 extractbb sample.png,生成写着 %%BoundingBox: 0 0 8 8 的 .xbb 文件备用。pdflatex 从不报这个错,只是因为 pdfTeX 自己会读 PNG 的文件头。
# ! LaTeX Error: Cannot determine size of graphic in sample.png (no BoundingBox).
# fix 1 - name the driver in the preamble:
# \usepackage[dvipdfmx]{graphicx}
# fix 2 - write the bounding box out first:
extractbb sample.png
cat sample.xbb
# %%Title: sample.png
# %%Creator: extractbb 20240305
# %%BoundingBox: 0 0 8 8
# %%HiResBoundingBox: 0.000000 0.000000 8.000000 8.000000于是判断顺序是这样的。先按语言和宏包决定,而不是按输出格式:日文就走 (u)platex → dvipdfmx,有 PSTricks 就走 PostScript,其余直接出 PDF。再统一图像格式:DVI 路线上用 PDF 或 EPS,或者用经过 extractbb 处理的 PNG/JPEG。最后显式写明驱动名——在 graphicx 和 hyperref 上都写 [dvipdfmx],日后有人换一条路线来构建这份文档,就不会掉进同一个坑。而 DVI 那一侧,始终不知道自己搬运的是什么颜色、什么图形。