为什么 PDF 里的文字其实并不是文字
当你阅读一份 PDF 时,你看到的是词语。但文件里并不包含词语。它包含的是“在某些坐标处绘制形状”的指令,而那些形状恰好看起来像字母。PDF 所绘制的内容与它所表达的含义之间的这道鸿沟,正是一个页面可以看上去完美无缺、却仍然复制出一堆胡言乱语的原因所在。本页讲的就是这道鸿沟,以及那张把它弥合起来的、小而单独的映射表。
为何重要
标题为“为何重要”的章节你对一份 PDF以文字方式所做的一切——选中它、复制它、搜索它、为检索而索引它、用屏幕阅读器朗读它——都依赖于恢复出那些文件从未直接存储过的字符。如果这种恢复失败了,失败本身却是不可见的。页面照常渲染。没人会察觉,直到某人把一段文字粘贴进一封电子邮件、得到一串 □□□□,或是在一份 400 页的合同里搜索一处明明白白就在那里的条款、却什么也找不到。
这是一类代价高昂的缺陷,恰恰是因为它能熬过每一次目视检查。没有任何东西可看。文档看上去权威可信,而就机器用途而言,它却是失声的。
精简版说明
标题为“精简版说明”的章节- 一份 PDF 绘制的是字形——由指向某个字体的数字码所选定的视觉形状——而不是 Unicode 字符。
- 同一个形状可以表示不同的字符,而同一个字符也可以由不同的字形来绘制。外观与含义是被刻意解耦的。
- 要把文字取回来,阅读器需要把绘制路径反着走一遍:从码到字符。字体的
/ToUnicodeCMap 就是这个反向步骤所用的查找表(Spec: ISO 32000-2, §9.10ISO 32000-2 §9.10)。 - 没有
/ToUnicode,或者有一个错误的,就意味着复制粘贴出乱码、搜索失败——而页面却仍然像素级精确。 - 这是含义问题。字形看起来对不对,则是另一个单独的外观问题,在字体:困难的那部分中讲到。NextPDF 会发出一个正确的
/ToUnicode,好让这趟往返忠实可靠。
NextPDF 的处理方式
标题为“NextPDF 的处理方式”的章节先从文字究竟是如何登上页面的说起。一个内容流并不会说“写出file这个词”。它先选定一个字体,然后把一串码交给一个文本显示操作符(Spec: ISO 32000-2, §9ISO 32000-2 §9)。每一个码都是一个索引——一个字体程序中的位置——而字体会把那个索引变成一个字形轮廓并把它画出来。码 70 不是字符 F。它是“这个字体里的第 70 号槽位”,而第 70 号槽位恰好装着一条 F 形的曲线。换一个字体,第 70 号槽位就可能是一片雪花。
所以,一个码只有相对于它所属字体的编码才有意义。对于一个简单字体,那个编码大致是每个字形一个字节。而对于那些需要数以千计字形的文种——CJK,或者任何全 Unicode 文档——这点空间是不够的,于是 PDF 便求助于一个复合(Type 0)字体(Spec: ISO 32000-2, §9.7ISO 32000-2 §9.7)。一个复合字体通过一张 CMap 读取多字节的码,把它们变成 CID(字符标识符),再把 CID 映射到字形。这是一种优雅的间接机制,让一个字体得以寻址一个庞大的字形集。但它也是又一处“从‘所显示的’到‘所表达的’”的线索可能断掉的地方。
现在把它反过来走。要提取文字,阅读器拿到它在内容流中找到的那些码,必须从中恢复出字符(Spec: ISO 32000-2, §9.10ISO 32000-2 §9.10)。当初绘制字形的那个编码走的是字符到字形;而提取需要的是字形码到字符,可这个方向并不保证是可逆的。一个子集字体可能已经把它的字形重新编了号。一个复合字体的各个 CID 可能是私有的。页面上的形状本身并不携带任何固有的 Unicode 含义。
解决之道,是把这张反向映射表与字体一同发运出去。那张表就是 /ToUnicode CMap:对于文档所用的每一个码,它都记录下那个码所代表的 Unicode 值(一个或多个)。有了它,提取就是一次干净利落的查找。没有它,阅读器就只能凭字体编码和各种启发式去猜——而猜测正是 fi 变成一个问号的地方。
- Your charactersThe Unicode text you set, for example the word 'file'.
- Encoding → codesThe font's encoding turns characters into numeric codes; a composite font routes them through a CMap to CIDs.
- Codes → glyphsEach code selects a glyph outline, which is painted to the page. This is the part you see.
- Extraction reverses itA reader reads the codes back and looks each one up in /ToUnicode to recover Unicode.
- Text againWith a correct /ToUnicode, copy, search, indexing, and screen readers all get the original characters back.
NextPDF 把写出这张反向映射表当作理所当然的常规操作。当它嵌入一个复合字体时,它会发出 CIDFontType2 后代字体、Type0 父字体,以及一个 /ToUnicode CMap,好让那些码能解析回 Unicode——这正是字体:困难的那部分中所描述的编码工作。这里值得单独点明的是为什么:/ToUnicode 流并不是为了让页面看起来正确。即使没有它,字形也已经看起来正确了。它是那个让文字保持可提取的唯一构件。
实务范例
标题为“实务范例”的章节这一点最出名的现身之处,是 fi 连字。许多字体把 f 和 i 画成一个合并的字形,因为 i 上的那个点会与 f 的钩相撞。在页面上它是一个形状,由一个码绘制。问题在于:当你复制它时会发生什么。
% A /ToUnicode entry that maps the single ligature code to TWO characters,% so selecting the 'fi' glyph copies out as 'f' then 'i' — not one mystery box.1 beginbfchar<0085> <00660069> % code 0x85 -> U+0066 'f' U+0069 'i'endbfchar就是那一条条目,决定了搜索“file”是能命中,还是会悄无声息地漏掉每一处以连字渲染的出现。把连字码映射到由两个字符 f + i 构成的序列,这个词便既可搜索又可复制。让它保持未映射,页面依旧把file完美地显示出来,而文字却悄悄地说出了机器读不出的虚无。
这就是为什么“fi”既可能提取成一个字符、也可能提取成两个,而这个区别并不随机——它就是 /ToUnicode 映射表被告知要说出的内容。一张正确的映射表会把连字分解回它的各个字符。NextPDF 发出的正是这一类条目,于是页面上的一个连字,到了剪贴板上就是两个普通的字母。
常见误解
标题为“常见误解”的章节那个陷阱,点名道姓地说:**“文字渲染出来了,所以文字没问题。”**渲染与提取是读取不同数据的不同机制。渲染遵循码到字形的路径,是你的眼睛在核查的那一个。提取遵循的是穿过 /ToUnicode 的码到字符路径,是每一个机器消费方在核查的那一个。一份文档可以在第一项上拿满分,却在第二项上彻底失败,因为从构造上讲,根本没有任何视觉迹象能揭示它。
第二个更隐微的陷阱:假定一个字形“显然”知道自己是哪个字符。它并不知道。一个字形是一个带索引的形状。回到字符的那个映射,是生产者必须提供的额外信息。如果生产者从未写下它,那么任何阅读器都无法忠实地把它恢复出来——它只能去猜。
限制与边界
标题为“限制与边界”的章节| Edition | Availability |
|---|---|
| Core | NextPDF emits a correct /ToUnicode CMap for the fonts it embeds, including the ligature and composite-font cases, so the documents it produces are searchable and copyable. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
一张 /ToUnicode 映射表只能携带源字体确实暴露出来的信息。当一个字形没有可确定的字符时——一个纯装饰性的标记、一个没有 Unicode 分配的私用区符号——没有任何映射表能凭空发明出那本就不存在的含义。NextPDF 发出它能推导出的、如实的映射;它不会为填补一处空缺而捏造字符。
本页讲的是 NextPDF 所写出的文档。它不是一个针对任意外来 PDF 的修复工具——那些 PDF 的 /ToUnicode 早已缺失或出错;从它们当中恢复文字,是一个单独的、启发式的问题。而且,忠实的提取对于无障碍是必要的,却不是它的全部——阅读顺序、标签以及替代文本,都在是什么让 PDF 具备无障碍性中讲到。
简短 FAQ
标题为“简短 FAQ”的章节为什么同一份 PDF 在一个阅读器里能正常复制,在另一个阅读器里却复制得很糟?
当 /ToUnicode 缺失时,不同的阅读器会以不同的方式回退。有些会从字体的内建编码去猜,在常见的拉丁文字上碰巧蒙对;另一些则不会。一个正确的 /ToUnicode 消除了这种碰运气——每一个符合规范的阅读器都会得到相同的字符。
这和字体没有被嵌入是一回事吗?
不是。嵌入关乎的是外观——在没有安装字体的情况下,字形是否还能画出来。/ToUnicode 关乎的是含义——码是否能映射回一个字符。一个字体可以被完美地嵌入,却依然没有一个可用的 /ToUnicode,而这正是那种“漂亮却不可搜索”的失败。
提取是否曾需要每个码对应不止一个字符?
会——连字的情形恰恰就是如此:一个码映射到两个字符。一条 /ToUnicode 条目可以把单个码映射到一个短序列,连字以及某些组合形式,正是借此以其各个构成字母的形式回来的。
相关文档
标题为“相关文档”的章节- 字体:困难的那部分 —— 外观那一面的姊妹篇:嵌入、子集化,以及编码是如何构建出来的。本页是同一枚硬币的含义那一面。
- 是什么让 PDF 具备无障碍性 —— 忠实的文字只是其中一味配料;阅读顺序、标签与替代文本则是其余的部分。
- PDF 究竟是什么 —— 字体、编码以及
/ToUnicode流所栖身的对象模型。
词汇表
标题为“词汇表”的章节- 字形(Glyph) —— 一个字体中的视觉形状(一条轮廓)。即一份 PDF 实际所绘制的东西。一个字形有索引,却没有固有的字符含义。
- 字符(Character) —— 书面语言的一个单位,由一个 Unicode 码点来标识。即你所表达的,以及提取所要恢复的。
- 码(Code) —— 内容流中那个从当前字体里选定一个字形的数字值。它不是一个 Unicode 字符;只有相对于该字体才有意义。
- CID —— 一个复合字体用来在一个庞大集合内寻址某个字形的字符标识符;是介于码与字形之间的一个中间步骤。
- 复合(Type 0)字体 —— 一个通过一张 CMap 读取多字节码以抵达 CID 与字形的字体,在一个字体必须寻址数以千计字形时使用。
/ToUnicode—— 那个把文档所用的码映射回 Unicode 值的 CMap 流;是让 PDF 文字可搜索、可复制、可无障碍访问的东西。- 连字(Ligature) —— 一个把两个或更多字母作为一个形状来绘制的单一字形(例如
fi);它的/ToUnicode条目会把它分解回它的各个字符。