DVI 转换工具

敲了 latex 却没有出现 PDF——这不是出故障。LaTeX 最老的那条命令写出的是 DVI 而不是 PDF,之后的事情归 DVI 转换器 管:dvipdfmxdvipsdvisvgm。这个格式的历史相当久远:按高德纳 DVItype 的源码所记,第一个 DVItype 程序由 David Fuchs 于 1979 年设计,比 PDF 的诞生早了十几年。即便今天 pdflatex 能直接产出 PDF,这条两段式路线依然没有消失,而且理由很具体。本页讲清 TeX Live 2024 里每个转换器吃什么、吐什么,日文的 uplatexdvipdfmx 路线为何至今仍是定式,以及转换阶段报错该怎么读。

既然有 pdflatex,为什么还要经过 DVI

DVI(device independent)是一种中间格式,里面只有指令:在第几页的这个坐标上,放这个字体的这个字符。它不嵌入字体文件,不懂颜色,也不懂图像。这份轻装正是关键。TeX 本体只管排版,把「纸上如何落墨」这类设备相关的事全部交给转换器——1979 年的这个设计决定,正是今天通往 PostScript、PDF 和 SVG 的出口。所以 DVI 留存下来并非只为兼容。pdflatex 只能写 PDF,而一份 DVI 可以分岔三条路。此外,PSTricks 这一系宏包依赖以 PostScript 解释器为前提的 \special 指令,因此链条中的某处必须有一个解释器;latexdvips 就是直接提供它的路。至于日文的 pTeX 系引擎,根本就没有直接输出 PDF 的实现。

dvipdfmxxdvipdfmxextractbb 是同一个程序

翻开 TeX Live 2024 的 bin 目录会发现,dvipdfmxdvipdfmextractbbebb 四者全都是指向 xdvipdfmx 的符号链接。也就是说,这是五个名字下的同一个可执行文件,程序会先看自己是以哪个名字被调用的,再决定如何行事。dvipdfmx --version 自报「This is dvipdfmx Version 20240305」,xdvipdfmx --version 则以同一日期报出 xdvipdfmx 之名——内容完全相同。明白了这一点,XeTeX 那边的事情也就顺了:xelatex -no-pdf 写出的不是 PDF,而是 .xdv(扩展 DVI),再由 xdvipdfmx 转成 PDF;既然是同一个二进制,敲 dvipdfmx sample.xdv 同样能跑。它的祖先 dvipdfm 由 Mark A. Wicks 编写;加入 CJK 支持、字体嵌入与对象流并扩展成 dvipdfmx 的,是 Jin-Hwan Cho、平田俊作等人。

terminal
ls -l $(dirname $(which dvipdfmx))/dvipdfmx
# dvipdfmx -> xdvipdfmx

dvipdfmx sample.dvi            # sample.dvi -> sample.pdf
dvipdfmx -p a4 -o out.pdf sample.dvi
xelatex -no-pdf paper.tex      # writes paper.xdv, not paper.pdf
xdvipdfmx paper.xdv            # paper.xdv -> paper.pdf
选项作用(TeX Live 2024 的默认值)
-o FILE输出文件名;- 表示标准输出。默认取输入名并换成 .pdf
-p a4按名称指定纸张大小;默认为 a4。用 dvipdfmx --showpaper 查看清单
-V 5PDF 版本;默认 5 即 PDF 1.5。若投稿方要求 1.4,则用 -V 4
-s 1-3,5限定要处理的物理页范围;可用逗号分隔指定多段
-l横向:交换纸张的宽与高
-z 9zlib 压缩级别 0 到 9;默认 9。调试时想直接读 PDF 内容可用 -z 0
-f FILE.map额外读入一个字体映射文件;用于替换要嵌入的字体

看到 ! LaTeX Error: Cannot determine size of graphic 时——先怀疑驱动选项

在 DVI 路线里插图,多数人最先撞上的就是这条报错:! LaTeX Error: Cannot determine size of graphic in fig.pdf (no BoundingBox). 原因不在图片文件,而在于 graphicx 是按哪个驱动配置的。TeX Live 2024 的 graphics.cfg 对任何写 DVI 的运行默认选用 dvips.def,而 dvips 只能嵌入 PostScript,因此无从读取 PDF 或 PNG 的尺寸。在导言区写明 \usepackage[dvipdfmx]{graphicx},在本机上 PDF 与 PNG 都无需额外文件即可直接通过。 实测中,latex 没有生成任何 .xbb、也没有用 shell escape 就读入了插图,dvipdfmx 也顺利嵌入。若遇到仍读不出尺寸的老图,就跑 extractbb fig.pdf;生成的 .xbb 只是几行文本,例如 %%BoundingBox: 0 0 612 792——按上面的符号链接可知,干这件事的正是 dvipdfmx 本体。

latex
% name the driver explicitly on the DVI route
\documentclass{article}
\usepackage[dvipdfmx]{graphicx}
\begin{document}
\includegraphics[width=3cm]{fig.pdf}
\end{document}

dvips — 为只有 PostScript 才能做的事而保留的路线

dvips -o paper.ps paper.dvi 把 DVI 变成 PostScript。它出自 Tomas Rokicki 之手,但 man 页的作者栏写着「extended to virtual fonts by Don Knuth」——虚拟字体支持是高德纳本人加的,路径查找与配置部分则由 Karl Berry 完成,是一件名副其实的合作品。如今用 dvips 的理由只剩三条:依赖 PostScript 运算的图形(首推 PSTricks)、印刷厂要 .ps 的入稿,以及用 dvips -E 抠出单张插图——它输出的 EPS 的边界框正好贴合内容(本机这次是 %%BoundingBox: 148 88 326 668)。最后若要 PDF,惯常做法是交给随 Ghostscript 提供的 ps2pdf

terminal
dvips -t a4 -o paper.ps paper.dvi   # DVI -> PostScript
ps2pdf paper.ps                     # PostScript -> PDF (Ghostscript)
dvips -E -o figure.eps figure.dvi   # one tightly cropped EPS
选项作用
-o FILE要写出的 PostScript 文件名;不给它,dvips 可能会试图直接打印
-t a4纸张类型:a4letterlegal,也可用 landscape
-E把单页写成 EPS;仅适用于单页文件,且插入的图形不计入边界框
-P NAME读取打印机配置 config.NAME,据此切换分辨率与字体处理方式
-p 3 -l 7只输出第 3 页到第 7 页

这里有一个不知道就会白丢一整天的坑:不要把位图送进 dvips 路线。 驱动仍是 dvips 时,\includegraphics{dot.png} 会停在上面那条 no BoundingBox;若强行放行——比如手工补一个 .xbb——那么 LaTeX 与 dvips 都不再吭声,.ps 照样写出来。真正的破损在更后面。把那个 .ps 交给 ps2pdf,本机上 Ghostscript 打出 Error: /syntaxerror in (binary token, type=137),随后以 GPL Ghostscript 10.03.0: Unrecoverable error, exit code 1 崩掉。PostScript 本来就不认识 PNG,这不奇怪;难受的是直到最后一步都没有人报警。所以:用 dvips,就把插图统一为 EPS;想混用 PNG、JPEG 和 PDF,就走 dvipdfmx 路线。判断只需这一句。

dvisvgm — 把公式变成 SVG,字体格式决定文件大小

把数学放到网页上,常见的选择要么是位图(一放大就糊),要么是 MathML(各环境渲染不一)。dvisvgm(Martin Gieseking,自 2005 年开发)直接把 DVI 转成 SVG,于是 TeX 的排版结果以矢量形式保留下来。本机 TeX Live 2024 里的版本是 dvisvgm 3.2.2,它还能用 --eps 接受 EPS、用 --pdf 接受 PDF。最要紧的选项是 --font-format。把同一页分别转换并实测大小:默认的 SVG 内嵌字体为 13,156 字节,--font-format=woff2 为 8,361 字节,而 --no-fonts(把每个字形都转成路径)反而涨到 23,563 字节。轮廓化后的文件里连一个 <text> 元素都没有,因此既不能搜索也不能复制。「轮廓化更小」在这里并不成立。

设置同一页的实测大小与性质
(default)13,156 字节;内嵌 SVG 字体,保留 14 个 <text> 元素
--font-format=woff28,361 字节;最小,且文本仍在。网页发布的首选
--no-fonts23,563 字节;全部转为路径,<text> 为 0;无法搜索与复制
-z5,383 字节;gzip 压缩后的 .svgz,只要服务器支持就是最小的
terminal
latex equation.tex                        # -> equation.dvi
dvisvgm --font-format=woff2 equation.dvi  # -> equation.svg
dvisvgm --pdf figure.pdf                  # PDF input
dvisvgm --libgs=/path/to/libgs.dylib f.dvi  # only if PS specials are used

还有一条消息,报告从未间断:processing of PostScript specials is disabled (Ghostscript not found)——而机器上明明装了 Ghostscript。本机正是如此:gs 就在 /usr/local/bin/gs,dvisvgm 依然这么说。原因在于它找的根本不是那个东西。dvisvgm 不去运行 gs 可执行文件,而是在运行时加载名为 libgs 的共享库。 MacTeX 附带的 Ghostscript 只放了可执行文件,于是库无从寻觅。若要转换用到 \special 的图形(PSTricks、部分 TikZ 输出),就装一个带共享库的 Ghostscript,或用 --libgs= 指出它的位置。反之,若 DVI 里只有公式和 \includegraphics,这条警告可以放心忽略。

日文的 uplatexdvipdfmx 路线与 ptex2pdf

日文排版中 DVI 路线得以存续,不是怀旧而是必然。pTeX 系引擎(ptexeptexuptexuplatex)没有直接写出 PDF 的能力。 竖排、禁则、和欧文之间的间距这些日文特有的处理由 pTeX 承担,生成 PDF 与嵌入日文字体则由 dvipdfmx 接手——四十年的积累就分布在这条分工线的两侧。因此步骤必定是两段:uplatex paper.tex 生成 .dvidvipdfmx paper.dvi 生成 PDF。在文档里同样写明驱动,例如 \documentclass[uplatex,dvipdfmx]{jsarticle}graphicxcolor 才会写出正确的 \special

把这两步合成一条命令的就是 ptex2pdf。TeX Live 2024 里的版本是 20200520.0,出自 texjporg(日文 TeX 开发社区)与 Norbert Preining 之手,是一个 texlua 脚本;命令本身是指向 texmf-dist/scripts/ptex2pdf/ptex2pdf.lua 的符号链接,引擎跑完后它会自动调用 dvipdfmx。引擎通过组合 flag 选择,日文论文常用的 upLaTeX 是 -l -u。细致的选项用 -ot 传给 TeX 一侧,用 -od 传给 dvipdfmx 一侧。不过 ptex2pdf 不会替你数编译次数;对含交叉引用或参考文献的文档,交给会数次数的 latexmk 更稳妥。

Flag调用的引擎
(none)ptex — plain TeX 的 pTeX 版
-lplatex — pLaTeX,处理 JIS X 0208 范围的传统引擎
-uuptex — 内部采用 Unicode 的 pTeX
-l -uuplatex — upLaTeX,当前日文论文的默认选择
terminal
# the two steps, written out
uplatex paper.tex          # -> paper.dvi
dvipdfmx paper.dvi         # -> paper.pdf

# the same thing in one command
ptex2pdf -l -u -ot '-kanji=utf8 -synctex=1' paper.tex
ptex2pdf -l -u -s paper.tex   # stop at the DVI

# what to check when the converter fails
dvipdfmx nosuch.dvi
# dvipdfmx:fatal: Could not open specified DVI (or XDV) file: nosuch.dvi
# No output PDF file written.

转换阶段的报错及其含义

消息实际发生了什么
dvipdfmx:fatal: Could not open specified DVI没有 .dvi——最常见的原因是文档是用 pdflatex 编译的。退出码为 1
dvips: DVI file can't be opened同样是找不到输入;dvips 不会猜文件名,请连扩展名一起核对
dvips: ! Bad DVI file: id byte not 2把 XeTeX 写的 .xdv 交给了 dvips;能读 XDV 的只有 xdvipdfmx
no BoundingBoxgraphicx 是按 dvips 配置的;改用 \usepackage[dvipdfmx]{graphicx} 试试
Ghostscript not founddvisvgm 找不到 libgs;有 gs 可执行文件是另一回事

提交前的检查也随路线而异。若要交 PDF,就在 dvipdfmx 之后用读者会用的那个阅读器打开它,确认字体嵌入、纸张尺寸、书签与链接。若要送印刷厂,就比较 dvips 生成的 .psps2pdf 前后的状态,看 EPS 与 PostScript 的 \special 是否幸存。若你依赖编辑器的正向与反向搜索,latex -synctex=1 在 DVI 路线上同样会生成 .synctex.gz——本机上是一个 860 字节的文件,跑完 dvipdfmx 之后仍在。.dvi.pdf.synctex.gz 共用 job name,因此若输出到另一个目录,请让构建工具统一指定该路径。

话虽如此,真正需要手动按顺序敲这些命令的场合并不多。latexmk 只要改一下 $pdf_mode,就能照管 latexdvipdfmx(u)platexdvipdfmx 的路线,还会自行判断 .dvi 是否已过时。需要手敲的时候,是你开始怀疑路线本身的时候——也就是本页派上用场的时候。反过来说,把转换器的名字和参数记牢一次,你也就读得懂构建工具在做什么了。