只写了一个 “Hello” 的 LaTeX 文档,作为 DVI 只有 252 字节。同样这一页用 pdflatex 生成 PDF 后是 11,287 字节。多出来的这十一千字节几乎都不是正文,而是字体本身:一个只裁下实际用到的 5 个字形的 Type 1 程序,就占去 9,002 字节。LaTeX 生成 PDF,说到底就是一连串「文件里要带什么」的决定。本页顺着这条线索,讲通往 PDF 的两条路线、PDF 版本与压缩的设定、纸张尺寸究竟是怎样被记录的,以及如何用 pdffonts 检查字体嵌入——全部附上实测数据。
通往 PDF 的两条路线:直接输出与经由 DVI
写出 PDF 的,要么是引擎自己,要么是把 DVI 转成 PDF 的另一个程序。在直接路线上,pdflatex(pdfTeX)与 lualatex(LuaTeX)一边排版一边直接吐出 PDF 运算符。在 DVI 路线上,latex 或日文的 (u)platex 先写出 DVI,再由 dvipdfmx 翻译成 PDF。有意思的是 xelatex:它看上去只有一条命令,实际上属于后一类——XeTeX 写出一种扩展 DVI(.xdv),交给 xdvipdfmx。看一眼 TeX Live 2024 的实际安装就一目了然:dvipdfmx 是 指向 xdvipdfmx 的符号链接,日文的 DVI 路线和 XeTeX 的 .xdv 路线其实由同一个二进制程序处理。证据留在输出里——用 xelatex 生成的 PDF,其 Producer 字段是 xdvipdfmx (20240305)。
# TeX Live 2024: one binary serves both DVI routes
$ ls -l $(which dvipdfmx)
lrwxr-xr-x 1 root wheel 9 May 4 2024 .../dvipdfmx -> xdvipdfmx
# direct
$ pdflatex paper.tex
# via DVI
$ latex paper.tex && dvipdfmx paper.dvi
# via DVI, the Japanese way
$ uplatex paper.tex && dvipdfmx paper.dvi把同一份稿件走五条不同的路线,结果都是「一页 A4」,可文件内部并不相同。Producer 字符串不同是自然的,真正值得注意的是默认 PDF 版本和 MediaBox 的数值。A4 的宽度 210 mm 精确说是 595.2755905… 点,各个转换器的取舍方式不一样:pdfTeX 与 LuaTeX 按 \pdfdecimaldigits=3 写成 595.276,dvipdfmx 写 595.28,而 Ghostscript(经由 ps2pdf)干脆取整为 595。这点差别在实务中几乎不会造成问题,但「同样是 A4 却比对不一致」的原因正在于此。
| 命令 | 写出 PDF 的程序 | 默认 PDF 版本 | A4 的 MediaBox 宽度 |
|---|---|---|---|
pdflatex | pdfTeX 本身 | 1.5 | 595.276 |
lualatex | LuaTeX 本身 | 1.5 | 595.276 |
xelatex | 经 .xdv 交给 xdvipdfmx | 1.5 | 595.28 |
latex + dvipdfmx | dvipdfmx | 1.5 | 595.28 |
uplatex + dvipdfmx | dvipdfmx;日文的常规路线 | 1.5 | 595.28 |
latex + dvips + ps2pdf | Ghostscript | 1.4 | 595 |
「pdflatex 不能用 EPS」已经过时了
在 TeX Live 2024 上,写 \usepackage{graphicx} 加 \includegraphics{fig.eps} 然后跑 pdflatex,图就进去了,不需要额外设置。这并不是 pdfTeX 学会了读 EPS。是驱动文件 pdftex.def 在满足条件时自动加载 epstopdf-base:LaTeX 正在运行、shell escape 已启用(TeX Live 默认的 restricted 就够)、且没有定义 \DoNotLoadEpstopdf。真跑一次就会看到源文件旁边多出一个 fig-eps-converted-to.pdf,被插入的正是它。你不必再自己写 \usepackage{epstopdf}。反过来,加上 -no-shell-escape 后转换不会发生,图就这样消失,既无报错也无警告。这种无声的失败才最麻烦。
这种自动转换也有陷阱,而且 pdftex.def 的注释本身就警告过:「这可能是错的!」如果 fig.pdf 与 fig.eps 同时存在,而真正的原图是那个 PDF,它就会被从 EPS 重新生成的版本覆盖。此时应在 \documentclass 之前写上 \newcommand{\DoNotLoadEpstopdf}{}。另外,DVI 路线(dvipdfmx)一直都能处理 EPS——它在后台调用 Ghostscript 转成 PDF 再插入,而且不会留下中间文件。这正是 EPS 资产众多的旧工作流至今仍适合 DVI 路线的原因。
% keep a hand-made fig.pdf from being overwritten by fig.eps
\newcommand{\DoNotLoadEpstopdf}{}
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics{fig} % extension omitted: pdf, png, jpg are tried first
\end{document}输出驱动真的会自动判定吗
对 graphicx 与 color 而言,答案是肯定的。两者都需要知道输出驱动(pdftex、luatex、xetex、dvipdfmx、dvips、dvisvgm)才能发出正确的底层指令,而配置文件 graphics.cfg 会通过探测 \pdfoutput、\XeTeXversion、\luatexversion 判断引擎,再决定读入 pdftex.def、luatex.def、xetex.def 还是 dvips.def。所以不要手写驱动选项——写死的指定,在你改变编译方式的那一刻就成了谎话。
但唯独 hyperref 要特殊对待。用 (u)platex 生成 DVI 时只写 \usepackage{hyperref},日志会报 Package hyperref Info: Driver (default): hdvips.——它写出的是面向 dvips 的 \special。把这样的 DVI 交给 dvipdfmx,就会刷出一串 dvipdfmx:warning: Unknown token "SDict",得到一份既无链接也无书签的 PDF。这条路线上请明确写成 \usepackage[dvipdfmx]{hyperref}。「不要指定驱动」说的是 graphicx,并不适用于 DVI 路线上的 hyperref(详见「书签与元数据」)。
指定 PDF 版本:\pdfminorversion 在哪个引擎上有效
\pdfminorversion 只属于 pdfTeX。在 LuaTeX 里写它会得到 ! Undefined control sequence.,XeTeX 也是同样的错误。LuaTeX 把 primitive 重新收进了命名空间,所以写法是 \pdfvariable minorversion=4。XeTeX 根本没有对应的 primitive——真正写 PDF 的是 xdvipdfmx——所以要传给驱动:-output-driver="xdvipdfmx -V 4"。走 DVI 路线最简单:dvipdfmx -V 4 paper.dvi。默认值的来源也能追到:TeX Live 的 pdftexconfig.tex 里写着 \pdfminorversion = 5,这就是「默认是 PDF 1.5」的全部原因。
不过到了 2024 年,已经不太需要分别记住这三种写法了。把 LaTeX 内核较新的入口 \DocumentMetadata{pdfversion=2.0} 放在 \documentclass 之前,pdflatex、lualatex、xelatex 都会输出 PDF 2.0(实测三种引擎均报 PDF version: 2.0)。\pdfminorversion 只动次版本号,而 pdfversion 连主版本号一起指定——\pdfminorversion=0 只会得到 PDF 1.0,永远到不了 2.0。
| 引擎/路线 | 指定 PDF 版本的方法 | 备注 |
|---|---|---|
\pdfminorversion=5 | pdfTeX(pdflatex) | 在 LuaTeX/XeTeX 上会得到 ! Undefined control sequence. |
\pdfvariable minorversion=5 | LuaTeX(lualatex) | 同样写法也可设定 compresslevel 等 |
dvipdfmx -V 4 | DVI 路线与 XeTeX | XeTeX 用 -output-driver="xdvipdfmx -V 4" |
\DocumentMetadata{pdfversion=2.0} | 三种引擎通用 | 也能指定主版本号;须放在 \documentclass 之前 |
PDF 压缩:\pdfcompresslevel 与 \pdfobjcompresslevel 的实测
把压缩级别提到 9,相比 6 只省下 2 个字节。用 \lipsum[1-40] 的八页文档实测:\pdfcompresslevel=0 是 79,508 字节,=1 是 45,730,=6 是 43,323,=9 是 43,321。真正有效的只有 0 → 1 这一步,再往上都是误差。另一个旋钮 \pdfobjcompresslevel 走的是完全不同的机制:它压缩的不是页面内容,而是 PDF 自身的对象定义,把它们打包成对象流。同一文档因此从 43,321 降到 41,019 字节,约 5%。
把这两者串起来的陷阱就在这里。对象流是 PDF 1.5 引入的功能,所以为了迁就旧查看器而降到 \pdfminorversion=4 时,\pdfobjcompresslevel 会被悄悄禁用。严格说也不算悄悄——日志里会写:pdfTeX warning (Object streams): \pdfobjcompresslevel > 0 requires PDF-1.5 or greater. Object streams disabled now. 实测也吻合:\pdfminorversion=4 的输出与 \pdfobjcompresslevel=0 的输出连字节数都相同(都是 43,321 字节)。「降了版本文件反而变大」几乎都是这个原因。dvipdfmx 那边关系一样,加上 -V 4 会让 8,310 字节的 PDF 变成 10,055 字节。它的压缩强度用 -z 0 到 -z 9 指定,而 -z 6 与 -z 9 的差距只有 6 个字节。
% pdfTeX defaults, as set by TeX Live in pdftexconfig.tex
\pdfminorversion = 5
\pdfcompresslevel = 9 % 0 = off; 1 already captures most of the gain
\pdfobjcompresslevel = 2 % object streams; needs PDF 1.5 or later
% LuaTeX spells the same knobs differently
\pdfvariable minorversion = 5
\pdfvariable compresslevel = 9
\pdfvariable objcompresslevel = 2想用眼睛读一读生成出来的 PDF 时,最快的办法是把压缩全部关掉。不必按引擎分别写 primitive,一行就够:\DocumentMetadata{uncompress}——实测让 52,622 字节的 PDF 变成 91,347 字节、可以用文本编辑器打开的 PDF。需要查看这次运行产生了哪些运算符,或者追踪是哪个宏包插入了哪个对象时,就用它。
明明写了 [letterpaper] 却出来 A4——纸张尺寸到底由谁决定
类选项并不决定 PDF 的纸张尺寸。在 TeX Live 2024 上,把 \documentclass[letterpaper]{article} 交给 pdflatex,pdfinfo 报的是 Page size: 595.276 x 841.89 pts (A4)。原因很朴素:写入 PDF MediaBox 的是 \pdfpagewidth 与 \pdfpageheight,而 letterpaper 根本不碰它们,只设定版心(\textwidth 等)。与此同时,TeX Live 的 pdftexconfig.tex 在启动时就已设好 \pdfpageheight = 297 true mm 和 \pdfpagewidth = 210 true mm。于是文档类就在一张 A4 上排出了一个 letter 尺寸的版心。
有三种可靠的做法。加载 geometry(\usepackage[letterpaper]{geometry} 会连 \pdfpagewidth 一起处理);自己写 primitive(\pdfpagewidth=8.5in,XeTeX 也接受);或者走 DVI 路线时在转换阶段指定(dvipdfmx -p letter paper.dvi)。实测这三种都得到 612 x 792 pts (letter)。还有第四个、也是最现代的答案:加上 \DocumentMetadata{} 之后,MediaBox 改为从 \paperwidth 与 \paperheight 写出,而不再看 \pdfpagewidth——于是类选项终于说了算。同一份源文件,仅凭这一行的有无就分出 A4 与 letter。迁移时若没注意到,后果可不只是页码位置挪了一点。
# TeX Live 2024, same source, four ways of asking for US letter
$ pdflatex letter.tex && pdfinfo letter.pdf | grep "Page size"
Page size: 595.276 x 841.89 pts (A4) # [letterpaper] alone: ignored
$ pdflatex geom.tex && pdfinfo geom.pdf | grep "Page size"
Page size: 612 x 792 pts (letter) # \usepackage[letterpaper]{geometry}
$ latex letter.tex && dvipdfmx -p letter letter.dvi
Page size: 612 x 792 pts (letter) # decided by the converter
$ pdflatex dm.tex && pdfinfo dm.pdf | grep "Page size"
Page size: 612 x 792 pts (letter) # \DocumentMetadata{} present字体嵌入了吗——怎么读 pdffonts
如果 pdffonts paper.pdf 的 emb 列全是 yes,字体就嵌入了。名字里还藏着另一个线索:嵌入的字体会叫 OREBYP+CMR10 这样,带一个 六位大写字母加 + 的前缀。这标示它是子集化过的——只裁出文档真正用到的字形;反过来,如果看到光秃秃的 CMR10 而没有前缀,那这个字体很可能根本没嵌入。翻开开头那份「Hello」的 PDF,嵌入字体的描述符里写着 /CharSet (/H/e/l/o/one):H、e、l、o,加上页码里的数字 1。子集化就是这么回事。
$ pdffonts paper.pdf
name type encoding emb sub uni object ID
----------------------------- ---------- --------- --- --- --- ---------
OREBYP+CMR10 Type 1 Builtin yes yes yes 4 0
PTKKKD+CMTI10 Type 1 Builtin yes yes yes 5 0
# the same source through latex + dvipdfmx: Type 1C instead of Type 1
$ pdffonts paper-dvipdfmx.pdf
FUYUFD+CMR10 Type 1C Builtin yes yes yes 4 0
# a font that was NOT embedded: bare name, emb = no
Helvetica Type 1 Standard no no no 7 0type 列同样有信息。比较同一页「Hello」,pdflatex 的输出写 Type 1,dvipdfmx 的输出写 Type 1C。两者是完全相同的 Computer Modern、完全相同的 5 字形子集,可嵌入的字体程序大小却是 9,002 字节对 720 字节——差了十二倍以上。原因是 dvipdfmx 会先把 Type 1 重新编码为 CFF(Compact Font Format,在 PDF 里叫 Type 1C)再嵌入;开头提到的总大小差距——DVI 版 2,145 字节对 pdflatex 版 11,287 字节——基本也就是由这一点造成的。LuaTeX 与 XeTeX 默认使用 Latin Modern 的 OpenType 版本,因此以 CID Type 0C 的形式进入。这些都是货真价实的嵌入,印刷和投稿都没有问题。
送印刷厂或投稿之前要检查什么
两条命令就能查完。用 pdffonts 确认所有字体都已嵌入,用 pdfinfo 看页面尺寸和 PDF 版本,就这些。出事的花样也很固定:emb 列里混进了 no(插入外部生成的 PDF 图最容易带进来)、以为是 A4 结果是 letter、PDF 版本比投稿规定要求的更新。另外,pdfinfo 的 Page size 并不会逐页检查,所以还可以指定范围——pdfinfo -f 1 -l 99 paper.pdf——确认中途尺寸没有变化。
pdffonts paper.pdf——emb列是否全为yes?名字是否都带ABCDEF+前缀?pdfinfo paper.pdf——Page size是否符合预期?PDF version是否在投稿规定允许的范围内?- 以英文为主、想快 → 直接路线(
pdflatex)。默认值不用多想。 - 要用系统字体、以 Unicode 为主 →
lualatex或xelatex(直接路线)。 - 用
(u)platex写日文 → DVI 路线(dvipdfmx);纸张用-p,PDF 版本用-V。 - 积累了大量 EPS → DVI 路线可以直接读入且不留中间文件;直接路线虽有自动转换,但会在目录里撒下一堆
*-eps-converted-to.pdf。