PDF 到底是什么,以及为什么有时候复制不出文字
PDF 是一份绘制指令的清单。它说的是:把这个字形放在这个坐标上,用这种字体、这个字号,然后移到这里,画下一个。它没有说这是标题、这是段落、这一栏在那一栏之前。阅读器看上去对页面结构所知的一切,都是事后通过测量各个笔画之间的间隙重建出来的,这解释了 PDF 令人恼火之处的大部分。
PDF 实际存储的是什么
页面上的文本住在内容流里,那是一小串操作符:
BT
/F1 11 Tf
72 708 Td
[(In) -18 (v) 22 (oice)] TJ
180 0 Td
(2026-08-25) Tj
ET
Tf 选定字体和字号,Td 移动文本光标,TJ 画出字形并在它们之间加上字偶距偏移。请注意缺了什么:任何地方都没有空格字符。日期前面的那段间隔是一次 180 个单位的跳跃,而提取器必须自行判断每一次跳跃到底是一个空格、一个制表符、一个分栏边界,还是什么都不是。阈值稍微偏一点,你得到的就是 Invoice2026-08-25 或者 I n v o i c e。
阅读顺序也有同样的问题:字形是按它们被画出来的顺序吐出来的。双栏排版通常先把左栏从上到下画完再画右栏,这种情况下复制是好用的;但如果生成器是横穿整页逐行绘制的,复制出来就会把两栏交错成一堆废话。
分辨文本 PDF 和扫描件
扫描出来的文档里根本没有文本:它是一张页面的照片,被包在了 PDF 里。光学字符识别会在图片之上加一个不可见的文本层,这让文件可以被搜索,但那一层是猜出来的,所以它的错误会以搜不到和复制出乱码的形式浮现。
| 检查方式 | 文本 PDF | 扫描 PDF |
|---|---|---|
| 选中一个词 | 逐字符高亮 | 什么都选不中,或者整页一起选中 |
| 放大到 400% | 字母依旧锐利 | 字母发虚或发块 |
| 每页大小 | 通常 20 到 100 KB | 通常 300 KB 到几 MB |
| 用 PDF 图片提取工具 | 返回真正的插图,或者什么都没有 | 每页返回一张整页图片 |
字体、嵌入与子集化
字体要么是嵌入的,也就是字体程序就在文件内部;要么是按名称和度量信息引用的,于是阅读器会在显示它的机器上找一个相似的来用。引用字体正是一份文档在制作它的地方看着没问题、换个地方就不对劲的原因:替代字体的字符宽度不同,于是行重新折断、表格溢出,一封一页的信变成了两页。
嵌入的字体几乎总是做了子集化。只有被用到的字形才会被包含进去,而且它们经常被重新编号,所以字母 A 的字形可能坐在编号 3 的位置上。映射回真实字符的关系保存在一张单独的 ToUnicode 表里,当这张表缺失或有误时,页面渲染得完美无缺,复制出来的文本却是乱码,因为交到你手上的是内部字形编码而不是字符。重新保存一遍修不好它;出路要么是源文档,要么是对渲染后的页面做识别。
页面尺寸、各种框与旋转
坐标以点为单位,每英寸 72 点,而每一页都带着若干个矩形。MediaBox 是整张纸;CropBox 是实际显示出来的那块区域,它可以更小。
| 尺寸 | 点 | 毫米 |
|---|---|---|
| A4 | 595 x 842 | 210 x 297 |
| Letter | 612 x 792 | 216 x 279 |
| Legal | 612 x 1008 | 216 x 356 |
A4 和 Letter 在屏幕上看着一模一样,在纸上却不同,所以把一份欧洲的报告和一份美国的报告合并,得到的页面尺寸会来回交替。
每一页还带着一个 0、90、180 或 270 度的旋转标志,好让横着存储的内容能正着显示。PDF 旋转工具设置的正是这个标志,而不是重画任何东西,所以它瞬间完成且无损,但底下的坐标没有变:一个忽略这个标志的工具会横着提取文本,而把标志混杂的页面合并起来,得到的文档看上去整齐一致,其内容却分处好几个方向。
合并、拆分,以及会坏掉的那些部分
页面住在一棵页面树里。PDF 合并工具把页面对象以及它们引用的一切复制进一个文件,再重建那棵树;PDF 拆分工具则取走它的一根枝。页面本身能完好地过来,挂在文档上的东西却未必。
- 书签是一份单独的大纲,指向各个页面对象。很多合并工具会把它整个丢掉,而拆分会留下一堆指向已经不在的页面的条目。
- 表单字段是在文档级而不是页面级命名的,所以把同一份表单的两份副本合并,会得到两个都叫
name的字段,而很多阅读器把它们当成一个共享取值的字段:填了一个,另一个也跟着填上。请先把它们扁平化或者改名。 - 注释,比如链接和批注,会随着它们所在的页面一起过来,但一个内部链接如果它的目标页在拆分时被留下了,就无处可去。
合并出来的文件常常比它的各个组成部分加起来还大,因为共享资源被复制了好几份:四个各自嵌入了同一款字体子集的来源,会给出四份子集,外加四份颜色描述文件和四个元数据块。它也可能变得更小,因为重写会丢弃未使用的对象和保存历史:一份被保存过三十次的文档,把此前每一个修订版都追加在了自己身上。
密码、权限与标签
有两种密码,而且它们不可相提并论。用户密码是打开文档所必需的:内容确实是用一个由它派生出来的密钥加密的,所以没有它就没有任何东西可读。较新的文件使用 AES-256,很老的文件使用 40 位 RC4,那是弱的。
所有者密码设置的是权限标志,比如不许打印、不许复制、不许编辑。文件仍然是加密的,但密钥是任何阅读器都能自行推导出来的,因为用户密码是空的。那些标志只是一个请求,有的阅读器遵守,有的忽略:如果一个文件不提示密码就打开了,那么无论属性对话框上写着什么,它的限制都只是建议性的。
打标签是另一个层次:一棵可选的结构树,标记出标题、列表、表格单元格和插图描述,并确定一个阅读顺序。屏幕阅读器面对一份打了标签的 PDF 是在读这份文档;面对一份没打标签的,它只能从坐标去猜,得到和复制粘贴一样杂乱的结果。扫描件按定义就是没有标签的,识别只添加文本而不添加结构,而图片转 PDF 或 PDF 转图片的输出两者都没有。
体积在哪里,以及浏览器能撑住多少
文件体积几乎总是图片带来的。一张 A4 页面在 150 dpi 下大约需要 1240 乘 1754 像素,在 300 dpi 下需要 2480 乘 3508 像素,所以一张按 1200 万像素放进去的照片,携带的数据远超打印能用到的量,而降采样是唯一能可靠缩小文档体积的改动。重新保存一份纯文本 PDF 收益极小,因为它的内容流本来就已经压缩过了;当某一份出乎意料地大时,去看嵌入字体、保存历史或者附件。
这一切都在浏览器里本地完成,所以什么都不会被上传,限制来自内存而不是文件大小。渲染才是昂贵的那一步:一张 A4 页面按 4 倍缩放约为 2380 乘 3368 像素,单页大约是 32 MB 未压缩位图,所以五十页按这个缩放比例,早在为难到机器之前就先把浏览器标签页耗尽了。特别大的文档请先拆开,再分批处理。