如果你曾经尝试从 PDF 中提取加粗或斜体文本,你可能首先想到的是使用正则表达式。这看起来是个显而易见的选择:在字体名称中搜索“Bold”一词,标记文本,然后继续。这种策略能奏效一小会儿,足以让你产生一种虚假的信心。然后,如果有人从不同版本的 Acrobat、LibreOffice 或通过“打印到 PDF”驱动程序导出同一份文档,你编写的所有假设都会土崩瓦解。

为什么字体名称会撒谎

PDF.js 会向你返回类似 ABCDEF+TimesNewRomanPS-BoldMT 的字符串。前六个字符是在导出过程中注入的随机前缀,每次重新生成文件时它们都会改变。把解析器寄托在那个前缀上,无异于在赌噪声。其他导出器甚至更不可靠。有些会输出像 Font12F1 这样简陋的标识符。这些标签完全不具备语义;它们只是文件写入时恰好位于附近的内部资源标签。

由于 PDF 格式的设计初衷并非为了方便下游文本提取,因此字体名称仅仅是对嵌入或子集化资源的引用。它们从未被设计为用于样式检测的稳定 API。当你编写一个查找子字符串 BoldItalic 的正则表达式时,你实际上是在抓取一个由创建应用程序随意格式化的标签。你读取的并不是实际的排版属性,而是一种文件命名约定,而约定并不等同于契约。

读取描述符,而非标签

真相存在于别处。在 PDF.js 中,每个页面对象都会暴露 commonObjs,这是一个保存渲染页面所需的真实字体描述符的映射(map)。当库解析页面时,它会用真实的字体对象填充这个映射。这些对象会暴露 bolditalic 的布尔属性。这些布尔值并非从名称字符串中推断出来的。它们源自嵌入在 PDF 内部的字体描述符,由字形度量(glyph metrics)、OS/2 表标志以及排版人员写入文档的符号描述符推导而来。

这意味着你可以停止猜测了。在遍历页面上的文本项之前,通过迭代 page.commonObjs 来创建一个 fontStyleMap。对于每个字体 ID,存储一个记录了字体对象提供的实际 .bold.italic 属性的对象。稍后,在处理每个文本项时,在你的映射中查找其字体引用,并读取预计算的标志。你会突然得到一个确定性的答案,且该答案在不同导出版本之间保持不变。

你可以保留一个回退方案。如果描述符由于某种原因缺失或不完整,可以清理字体名称——去掉前缀,丢弃随机标签,然后对剩余部分运行一个保守的正则表达式。但这应该是你的最后手段,而不是你的主要逻辑。可靠性方面的差异是巨大的。基于名称的解析在不同导出器之间会失效,而基于描述符的解析则能保持稳定,因为它直接询问文件实际包含的内容。

当字体也撒谎时:合成样式

甚至描述符也可能出错。一些 PDF 创建工具并不费力去嵌入单独的斜体字库。相反,它们直接使用正体罗马字体,并通过变换矩阵(transform matrix)将其倾斜。这在设计工具或较旧的文字处理器生成的文件中很常见,因为它们更看重文件大小而非排版纯净度。

PDF.js 中的每个文本项都携带一个 transform 数组,这是一个包含六个元素的仿射矩阵,用于将字形坐标系映射到页面坐标系。该数组的第三个元素控制水平剪切(horizontal shear)。当该值为非零时,文本正由渲染器进行机械式倾斜。如果你只信任字体描述符,你会将此文本归类为正体罗马字体。如果你检查矩阵,就能捕捉到这种合成斜体并进行正确标记。同样的逻辑也适用于通过重叠打印(overprinting)创建的合成加粗,尽管仅凭几何形状很难检测到这一点。对于倾斜文本,剪切值就是你的“铁证”。

下划线是绘制出来的,而非声明的

加粗和斜体是字体属性。下划线则不是。在 PDF 中,下划线是一条矢量路径。渲染器会发出一个绘制命令,在靠近文本基线的位置绘制一段细长的水平线段。它是一个恰好位于字形下方的图形元素,而不是存储在 cmap 中的字符属性。

This distinction matters because no amount of font descriptor reading will reveal an underline. You need to look at the raw drawing operators on the page, or at the geometry-level output, for short horizontal line segments that run parallel to the baseline at the correct proximity. When your extraction engine spots such a segment underneath a text run, you tag that run as underlined. Treating this as a separate detection layer keeps your data model honest: bold and italic are intrinsic to the font, while underline is extrinsic decoration rendered by the document.

A Working Pipeline

A clean extraction system separates concerns into distinct layers. First, a geometry worker walks the page. It queries page.commonObjs to assemble your fontStyleMap, inspects each text item’s transform array to catch synthetic italics, and scans nearby vector paths to spot underlines. Its output is a clean intermediate structure call it textMeta where every text run carries three simple booleans: bold, italic, and underline.

Next, a text rebuilder consumes that structure and emits marked-up output. Nesting order is important here. The correct hierarchy places underline outermost, then italic, then bold innermost. That means a fully styled run becomes <u><i><b>text</b></i></u>. This ordering prevents invalid HTML overlaps and keeps rendering consistent across browsers and document converters. It also mirrors the typographic logic: decoration wraps semantic emphasis, and semantic emphasis wraps structural weight.

The power of this approach is that it uses only what the PDF already knows about itself. There is no OCR involved, no cloud vision service, and no machine learning model guessing at styles from rasterized pixels. You are reading the file’s own semantic layer, exposed through the geometry and metadata that the creator application already calculated. The result is fast, deterministic, and accurate across the chaotic landscape of PDF generators.

The Real Takeaway

Regex against font names is a trap. It feels like a shortcut because it works on the one file you tested, but it collapses under the mild pressure of a second export. The real information is already inside the PDF, sitting in descriptors, matrices, and vector paths. Build your pipeline around those facts. Ask the font object whether it is bold. Check the transform matrix for shear. Look at the drawn segments for underlines. If you query the document’s own engineering instead of scraping its surface labels, you get styles that survive from one PDF generator to the next.