文章目录
- PDF结构的清晰图示
-
- 一、PDF文件全景结构图
- [二、文件头 (Header) 结构](#二、文件头 (Header) 结构)
- [三、传统 XRef 表 (V4) 结构图](#三、传统 XRef 表 (V4) 结构图)
-
- [3.1 XRef 表整体结构](#3.1 XRef 表整体结构)
- [3.2 单个条目的 20 字节布局](#3.2 单个条目的 20 字节布局)
- [四、XRef 流 (V5) 结构图](#四、XRef 流 (V5) 结构图)
-
- [4.1 XRef 流对象结构](#4.1 XRef 流对象结构)
- [4.2 W 数组决定条目布局](#4.2 W 数组决定条目布局)
- [4.3 Index 数组的作用](#4.3 Index 数组的作用)
- [五、Trailer 字典与 Prev 链](#五、Trailer 字典与 Prev 链)
-
- [5.1 Trailer 结构](#5.1 Trailer 结构)
- [5.2 Prev 链(增量更新)](#5.2 Prev 链(增量更新))
- 六、代码与结构的完整对应流程
-
- [步骤1:寻找 startxref](#步骤1:寻找 startxref)
- [步骤2:根据偏移加载 XRef](#步骤2:根据偏移加载 XRef)
- [步骤3:判断 XRef 类型](#步骤3:判断 XRef 类型)
- [步骤4:解析 V4 表](#步骤4:解析 V4 表)
- [步骤5:解析 V5 流](#步骤5:解析 V5 流)
- 步骤6:填充对象信息
- 七、总结图:解析器内部数据结构
PDF结构的清晰图示
一、PDF文件全景结构图
┌─────────────────────────────────────────────────────────────────────┐
│ PDF 文件物理布局 (从上到下) │
├─────────────────────────────────────────────────────────────────────┤
│ [Header] "%PDF-1.4" + 二进制标记 │
├─────────────────────────────────────────────────────────────────────┤
│ [Body] 间接对象序列: │
│ 1 0 obj <<...>> endobj │
│ 2 0 obj <<...>> stream ... endstream endobj│
│ ... │
├─────────────────────────────────────────────────────────────────────┤
│ [XRef Table] "xref" + 子节 + 条目 │
│ (或者 XRef Stream) │
├─────────────────────────────────────────────────────────────────────┤
│ [Trailer] "trailer" + 字典 + "startxref" + 偏移 + "%%EOF"│
└─────────────────────────────────────────────────────────────────────┘
代码入口 :CPDF_Parser::StartParse 负责定位 Header 和 startxref。
二、文件头 (Header) 结构
偏移(字节) 内容 说明
0 '%' PDF签名开始
1 'P'
2 'D'
3 'F'
4 '-'
5 '1' 主版本号 → 代码中 GetCharAt(5)
6 '.'
7 '4' 次版本号 → 代码中 GetCharAt(7)
8 '\r' 行结束
9 '\n'
10 '%' 二进制标记开始 (可选)
11 '\xE2'
12 '\xE3'
13 '\xCF'
14 '\xD3'
对应代码:
cpp
m_pSyntax->GetCharAt(5, ch); // 读取主版本号字符
m_FileVersion = FXSYS_DecimalCharToInt(ch) * 10;
m_pSyntax->GetCharAt(7, ch); // 读取次版本号字符
m_FileVersion += FXSYS_DecimalCharToInt(ch);
三、传统 XRef 表 (V4) 结构图
3.1 XRef 表整体结构
文件偏移(绝对) 内容
200: "xref" LF
"0 5" LF ← 子节1: 对象0~4
210: "0000000000 65535 f" LF ← 条目0 (20字节)
230: "0000000016 00000 n" LF ← 条目1
250: "0000000081 00000 n" LF ← 条目2
270: "0000000146 00000 n" LF ← 条目3
290: "0000000220 00000 n" LF ← 条目4
310: "5 3" LF ← 子节2: 对象5~7
320: "0000000330 00000 n" LF ← 条目5
340: "0000000388 00000 n" LF ← 条目6
360: "0000000430 00000 n" LF ← 条目7
380: "trailer" LF
3.2 单个条目的 20 字节布局
字节索引: 0 9 10 15 16 17 18 19
+----------+--+--------+--+--+--+--+
| 偏移量 |空格| 生成号 |空格| n|LF| (LF也可能是CR+LF)
+----------+--+--------+--+--+--+--+
示例: " 123 00000 n\r\n"
0 9 10 15 16 17 18 19
代码解析:
cpp
// 读取条目缓冲区(20字节)
char* pEntry = &buf[i * 20];
if (pEntry[17] == 'f') {
// 空闲对象
m_ObjectInfo[objnum].type = 0;
} else {
// 正常对象
FX_FILESIZE offset = FXSYS_atoi64(pEntry); // 取前10字节
int32_t version = FXSYS_atoi(pEntry + 11); // 取第12-16字节
m_ObjectInfo[objnum].pos = offset;
m_ObjectInfo[objnum].gennum = version;
m_ObjectInfo[objnum].type = 1;
}
四、XRef 流 (V5) 结构图
4.1 XRef 流对象结构
1 0 obj
<<
/Type /XRef
/Size 10 ← 对象总数(最大对象号+1)
/Prev 12345 ← 上一版 XRef 流位置
/W [1 4 1] ← 字段宽度: 类型1字节, 偏移4字节, 生成号1字节
/Index [0 5 6 4] ← 子节定义: 对象0~4, 对象6~9 (对象5被跳过)
>>
stream
(二进制数据,每个条目按 W 定义的长度排列)
endstream
endobj
4.2 W 数组决定条目布局
W = [1, 4, 1] 时,每个条目占用 1+4+1 = 6 字节:
+----------+-----------------------+----------+
| 类型(1B) | 字段2(4B, 大端) | 生成号(1B)|
+----------+-----------------------+----------+
0: 空闲 (字段2无用) 通常0
1: 正常对象 = 文件偏移 生成号
2: 压缩对象 = 对象流编号 生成号
二进制数据示例(十六进制):
01 00 00 01 00 00 → 类型1, 偏移256(0x100), 生成号0
02 00 00 00 01 00 → 类型2, 对象流编号1, 生成号0
00 00 00 00 00 00 → 类型0, 空闲
代码解析:
cpp
// 读取 W 数组
for (size_t i = 0; i < pArray->GetCount(); ++i)
WidthArray.push_back(pArray->GetIntegerAt(i));
uint32_t totalWidth = WidthArray[0] + WidthArray[1] + WidthArray[2];
// 遍历每个条目
const uint8_t* entry = pData + entry_index * totalWidth;
int type = GetVarInt(entry, WidthArray[0]); // 读取类型
int64_t field2 = GetVarInt(entry + WidthArray[0], WidthArray[1]); // 读取第二字段
int gen = GetVarInt(entry + WidthArray[0] + WidthArray[1], WidthArray[2]);
4.3 Index 数组的作用
/Index [0 5 6 4]
│ │ │ └─ 第二段数量=4 (对象6~9)
│ │ └── 第二段起始=6
│ └──── 第一段数量=5 (对象0~4)
└────── 第一段起始=0
最终流中包含的条目顺序: 对象0,1,2,3,4, 对象6,7,8,9 (对象5不在流中)
代码:
cpp
for (size_t i = 0; i < arrIndex.size(); i++) {
int startnum = arrIndex[i].first; // 起始对象号
int count = arrIndex[i].second; // 对象个数
// 处理这 count 个条目...
}
五、Trailer 字典与 Prev 链
5.1 Trailer 结构
trailer
<<
/Size 9 ← 对象总数(最大对象号+1)
/Root 1 0 R ← 根目录对象引用
/Info 8 0 R ← 信息字典
/Prev 12345 ← 上一版 XRef 的位置(绝对偏移)
/ID [<...>] ← 文件标识符
>>
startxref
54321 ← 当前 XRef 表(或流)的绝对偏移
%%EOF
5.2 Prev 链(增量更新)
文件布局(时间从下往上):
┌─────────────────────┐
│ 最新版 trailer │ ← Prev = 2000
│ startxref = 5000 │
├─────────────────────┤
│ 第二版 XRef (偏移5000)│
│ 第二版 trailer │ ← Prev = 1000
├─────────────────────┤
│ 第一版 XRef (偏移2000)│
│ 第一版 trailer │ ← Prev = 0
├─────────────────────┤
│ 原始对象体 │
└─────────────────────┘
代码加载 Prev 链:
cpp
xrefpos = GetDirectInteger(m_pTrailer.get(), "Prev"); // 获取 Prev 值
while (xrefpos) {
// 加载历史 XRef 表
LoadCrossRefV4(xrefpos, 0, true);
// 获取新的 Prev
xrefpos = GetDirectInteger(pDict.get(), "Prev");
}
六、代码与结构的完整对应流程
步骤1:寻找 startxref
文件末尾附近: ... startxref 54321 %%EOF
↑
BackwardsSearchToWord("startxref")
代码:
cpp
if (m_pSyntax->BackwardsSearchToWord("startxref", 4096)) {
m_pSyntax->GetKeyword(); // 读取 "startxref"
CFX_ByteString xrefpos_str = m_pSyntax->GetNextWord(&bNumber);
m_LastXRefOffset = FXSYS_atoi64(xrefpos_str.c_str());
}
步骤2:根据偏移加载 XRef
m_LastXRefOffset = 54321 → 跳转到该位置,开始解析 XRef
步骤3:判断 XRef 类型
位置 54321 处:
如果是 "xref" → 传统 V4 表
如果是数字(对象号) → 可能是 XRef 流
代码:
cpp
if (!LoadAllCrossRefV4(m_LastXRefOffset) &&
!LoadAllCrossRefV5(m_LastXRefOffset)) {
// 重建
}
步骤4:解析 V4 表
54321: "xref" → m_pSyntax->GetKeyword() 验证
54327: "0 5" → 读取 start_objnum=0, count=5
54333: 开始读取 5*20=100 字节条目数据
for (i=0;i<5;i++) 解析每个条目的偏移、生成号、状态
步骤5:解析 V5 流
54321: "1 0 obj" → ParseIndirectObjectAt 读取对象
<< /Type /XRef /W [1 4 1] ... >>
stream ... 二进制数据 ... endstream
代码通过 CPDF_StreamAcc 加载二进制数据,然后按 W 和 Index 解析。
步骤6:填充对象信息
cpp
// 最终得到 m_ObjectInfo 映射表:
m_ObjectInfo[1] = { pos=16, gennum=0, type=1 }
m_ObjectInfo[2] = { pos=81, gennum=0, type=1 }
m_ObjectInfo[5] = { pos=1, gennum=0, type=2 } // 压缩对象, 位于对象流1中
m_ObjectInfo[1] (流对象) = { pos=100, gennum=0, type=255 }
七、总结图:解析器内部数据结构
PDF 文件
│
├── 物理偏移 → CPDF_SyntaxParser (读取器)
│
└── StartParse
│
├── 找到 startxref → m_LastXRefOffset
│
├── LoadAllCrossRefV4 / V5
│ │
│ ├── 读取 XRef 条目 → 填充 m_ObjectInfo (map<objnum, Info>)
│ │
│ └── 记录所有偏移 → m_SortedOffset (set)
│
└── SetEncryptHandler → 解密支持
│
└── m_pDocument->LoadDoc() 利用 m_ObjectInfo 快速定位对象
核心思想:PDF 解析器先建立"对象号 → 文件偏移"的索引,然后就可以随机访问任何对象,无需扫描整个文件。
希望这种先图后码、图文对应的方式能帮助您彻底理解 PDF 交叉引用表的解析过程。如果还有某个细节需要更详细的图示,请指出!