DVI / PS / PDF 工作流

把同一个单页 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。夹在中间的 pushdown4right3 负责挪动当前点。也就是说,DVI 是一份「在哪里、用哪个字体、放第几号字符」的指令清单,而这些字符究竟长什么样,它一无所知。

terminal
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,能读出来的只有正文里的单词,加上一串字体名:cmr10cmmi7cmsy10cmex10 等等。因此拿到 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,它只是搬运了一串字符。

terminal
# 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)platexdvipdfmx;手里攒着 PSTricks 或成堆旧 EPS 就走 latexdvipsps2pdf实务上的分叉就这么多。但「同一份文档就会得到同一个 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 → PDFPSTricks、成套 EPS、以 PostScript 为前提的交稿
terminal
# 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 you

XeLaTeX 真的跳过了 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.」,实际上 dvipdfmxextractbb 指向的正是同一个可执行文件。所谓「直接输出 PDF」,意思是把 DVI 那一步藏了起来,而不是取消了它。

terminal
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 的文件头。

terminal
# ! 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)platexdvipdfmx,有 PSTricks 就走 PostScript,其余直接出 PDF。再统一图像格式:DVI 路线上用 PDF 或 EPS,或者用经过 extractbb 处理的 PNG/JPEG。最后显式写明驱动名——在 graphicxhyperref 上都写 [dvipdfmx],日后有人换一条路线来构建这份文档,就不会掉进同一个坑。而 DVI 那一侧,始终不知道自己搬运的是什么颜色、什么图形。