PDF 文件结构【编辑中】

最近更新于 2025-06-07 22:09

前言

2025.6.3
上个月在某个网站下载了某盗版国标PDF文件,文件中添加了很多链接,随便点击一处都会跳转,很烦人。我就较劲研究了一下,找到了链接的存在形式,写了一个小工具来自动移除链接,并把这个项目开源了。只是这个项目适用性有点窄,不久就有网友联系说不适用他遇到的情况。
file

上周末,我简单的研究了下,Acrobat 都识别不到里面嵌入的链接对象,后面对方告知用 WPS PDF 时可见,手动删除链接可以。我反复折腾还是没有有效的方法通过编程识别到链接并实现移除,我对 PDF 文件结构也不了解,搞得很懵逼了。现在准备熟悉一下 PDF 文件结构,这样或许有望解决遇到的问题。


参考:

测试环境

Python 3.13.1
pikepdf 9.8.1

PikePDF 是一个底层 PDF 操作库,基于 C++ 的 QPDF 库。

基本理解

PDF 本身是一个结构化的文本文件,就像 json、xml、yaml 这些一样,除了文本部分,还可以存储一些二进制数据在里面,只是一般阅读 PDF,是看的阅读器渲染好的图像,而不是去看一团“代码”一样的原始文档。

下面写一段代码创建一个简单的 PDF,只显示 hello world

Python

from pikepdf import Pdf, Dictionary, Name, Stream

# 创建一个新的 PDF 文档
pdf = Pdf.new()

# 添加一个空白页面
page = pdf.add_blank_page(page_size=(595, 842))  # A4 纸大小

# 设置页面的内容
# BT 开始文本对象
# Tf 设置字体和字号,字体用 F1,字号 18
# Td 将文本矩阵的当前位置移动到指定的水平和垂直偏移量。240 水平偏移量,700 垂直偏移量
# Tj 显示文本。文本内容:Hello World
# ET 结束文本对象
page.Contents = Stream(pdf, b"""
BT
/F1 18 Tf
240 700 Td
(Hello World) Tj
ET
""")

# 添加字体资源
# 字体定义为 F1
# 字体子类型为 Type1
# 字体基础名称 Helvetica
page.Resources = Dictionary(
    Font=Dictionary(
        F1=Dictionary(
            Type=Name("/Font"),
            Subtype=Name("/Type1"),
            BaseFont=Name("/Helvetica")
        )
    )
)

# 保存 PDF 文件
pdf.save('hello_world.pdf')

创建的文件用 Edge 浏览
file

以纯文本的方式打开 PDF 文件就能看到原貌,有部分是明码,还有部分乱码,其实就是二进制数据

%PDF-1.3
%亏
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /Contents 4 0 R /MediaBox [ 0 0 595 842 ] /Parent 2 0 R /Resources << /Font << /F1 << /BaseFont /Helvetica /Subtype /Type1 /Type /Font >> >> >> /Type /Page >>
endobj
4 0 obj
<< /Length 53 /Filter /FlateDecode >>
stream
x溿r
嵋w3T0 I?21P070I嵋餒蜕 ?蔍 赦r
?滠
?endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
0000000300 00000 n
trailer << /Root 1 0 R /Size 5 /ID [<ee5d509761ba5ef60d74840dbaff2a03><ee5d509761ba5ef60d74840dbaff2a03>] >>
startxref
424
%%EOF

这个 PDF 文件由文件头、对象定义、交叉引用表、文件尾组成

文件头

%PDF-1.3
%亏

文件头表明了 PDF 遵循的版本,比如这里是 1.3 版。第 2 行以 % 开头的是注释行,创建 PDF 的软件添加的信息,不影响 PDF 显示内容。

对象定义

每一个对象定义都是由下面格式组成

对象编号 世代编号 obj
对象内容
endobj

每个对象有一个编号,世代编号相当于这个对象的版本,对象编号和世代编号可以确定文件中唯一的对象

这个 PDF 有 4 个对象:
①第一个:

  • << >> 表面这是一个字典,用于存储键值对
  • /Pages 2 0 R:表示这个对象引用了对象编号为 2 ,世代编号为 0 的对象,R表示引用
  • /Type /Catalog:表示这是一个目录对象,目录对象是PDF文件的顶级对象,它包含了对页面树的引用以及其他一些文档级别的信息
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj

②第二个:

  • /Type /Pages:页面树节点对象
  • /Kids [ 3 0 R ]:页面树节点的子节点对象。[ ] 是一个列表,包含一个对象引用,引用了对象编号为 3,世代编号为 0 的对象。
  • /Count 1:从这个页面树节点开始,所有子节点包含的页面总数为 1
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj

③第三个:

  • /Contents 4 0 R:表示这个页面的内容流存储在编号为 4,世代号为 0 的对象中
  • /MediaBox [ 0 0 595 842 ]:页面尺寸。左下角坐标(0,0),右上角坐标(595,842)。单位通常是点(1点=1/72英寸)
  • /Parent 2 0 R:表示这个页面对象的父节点是编号为2的页面树节点
  • /Resources:定义了页面使用的资源。/Font:定义了页面使用的字体。/F1:定义了一个字体对象,命名为F1。/BaseFont /Helvetica:表示这个字体是Helvetica字体。/Subtype /Type1:表示字体的子类型是Type1字体。/Type /Font:表示这是一个字体对象。
  • /Type /Page:表示这是一个页面对象。
3 0 obj
<< /Contents 4 0 R /MediaBox [ 0 0 595 842 ] /Parent 2 0 R /Resources << /Font << /F1 << /BaseFont /Helvetica /Subtype /Type1 /Type /Font >> >> >> /Type /Page >>
endobj

④第四个:
这是一个流对象

  • /Filter /FlateDecode:使用了 FlateDecode 过滤器进行压缩
  • /Length 53:未压缩的长度为 53
  • stream 和 endstream 之间的就是压缩后的流数据(二进制数据)
4 0 obj
<< /Length 53 /Filter /FlateDecode >>
stream
x溿r
嵋w3T0 I?21P070I嵋餒蜕 ?蔍 赦r
?滠
?endstream
endobj

交叉引用表

xref 开头就是交叉引用表,可以用来快速定位对象

  • 0 5:表示对象从编号 0 开始共 5 个对象
  • 0000000000 65535 f:0000000000 表示对象编号 0 对象偏移量为 0,65535 是一个特殊标记,f 表示为自由对象(已经被删除或不再使用的对象),对象 0 应该是早期的历史遗留,为了保持兼容还是从 0 开始。
  • 0000000015 00000 n:0000000015 表示对象编号 1 对象偏移量为 15,n 表示已使用对象,00000 是对象的世代号
    ……
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
0000000300 00000 n

文件尾

trailer 表示文件尾开始

  • /Root 1 0 R:表示文件的目录对象是编号为1的对象
  • /Size 5:表示文件中共有5个对象
  • /ID:表示文件的唯一标识符,这里有两个相同的标识符,用于标识文件的版本
  • startxref:表示交叉引用表的起始位置
  • 424:交叉引用表的起始位置是文件的第424字节
  • %%EOF:表示文件的结束
trailer << /Root 1 0 R /Size 5 /ID [<ee5d509761ba5ef60d74840dbaff2a03><ee5d509761ba5ef60d74840dbaff2a03>] >>
startxref
424
%%EOF

PDF 中对象类型

整数和整数

整数写为一个或多个十进制数字,符号可选

0 +5 -5 5

实数被写为一个或多个十进制数字,可选地前面带有加号或减号, 并且可选的有一个小数点

0.0 0. .0 -0.002 123.4

字符串

字符串用小括号(英文符号,下同)括起来

(我是一个字符串)

转义字符使用反斜杠,要表示反斜杠自身或小括号,前面需要加反斜杠转义。
一些转移字符:

字符序列 含义
\n 换行
\r 回车
\t 水平制表符
\b 退格
\f 换页符
\ddd 三个八进制数字的字符代码

十六进制字符串

使用<>括十六进制数字序列,如 < 0A2B34FF> 表示 0x0A、0x2B、 0x34、0xFF 四个字节,即俩俩算一个字节

名称

名称以斜杠开头,作为字典的键和定义各种多值对象

/Age
/Name

可以使用#加两位十六进制数字,引入 ASCII 字符

/My#20/Name

十六进制数 0x20 对应的字符是空格,即实际这个名称是

/My Name

布尔值

值可以为 true 和 false,常用于字典中作为标志

数组

数组用中括号括起来,有序集合,数组中子元素可以不是同一类型
比如下面的数组包含了三个元素:/red、/blue 、[123 4.5],第三个元素又是一个数组,这个数组包含 123 和 4.5 两个元素。

[/red /blue [123 4.5]]

字典

表示键值对的无序集合,用两层尖括号括起来<<>>
下面的例子将 /one 映射到 1,/two 映射到 2,/three 映射到 3

<</one 1 /two 2 /three 3>>

引用

引用格式为

对象编号 世代编号 R

流和过滤器

流用于存储二进制数据。它们由字典和一大块二进制数据组成。 字典根据流所放置的特定用途列出数据的长度,以及可选的其他参数。
从语法上讲,流由字典组成,后跟stream关键字, 换行符( ),零个或多个字节的数据, 另一个换行符,最后是endstream关键字。
压缩方法:

方法名称 描述
/ASCIIHexDecode 为压缩数据中的每对十六进制数字生成一个字节的未压缩数据。>表示数据结束。空格被忽略。这个过滤器和/ASCII85Decode旨在将数据减少到7位
——/ASCII85Decode更复杂,但更紧凑
/ASCII85Decode 这种7位编码格式使用可打印的字符从 !uZ。(译者注:即通过五个ASCII字符来表示四个字节的二进制数据。)~>标识数据结束
/LZWDecode 实现Lempel-Ziv-Welch压缩,如TIFF图像格式所使用
/FlateDecode Flate压缩,由开源zlib库使用。在RFC 1950中定义。/LZWDecode和/FlateDecode都可以在流字典中具有预测变量,它们定义数据的后处理以反转在压缩时完成的预处理
/RunLengthDecode 一个简单的基于字节的游程压缩器
/CCITTFaxDecode 实现传真机使用的第3组和第4组编码。适用于单色(位深度为1)图像,不适用于一般数据
/JBIG2Decode 一种更现代,更好的压缩机制,适用于与/CCITTFaxDecode一起使用的各种数据,但也适用于灰度和彩色图像和一般数据。实现JBIG2压缩方法
/DCTDecode JPEG有损压缩。整个JPEG文件可以放在这里,包括JPEG文件头
/JPXDecode JPEG2000有损和无损压缩。仅限于JPX基准功能集,但有一些例外
PDF 文件结构【编辑中】
Scroll to top
打开目录