文章目录
- PDF交叉引用表解析:极致详解
-
- 目录
- [1. PDF文件整体结构(逐字节分析)](#1. PDF文件整体结构(逐字节分析))
-
- [1.1 文件头 (Header)](#1.1 文件头 (Header))
- [1.2 对象体 (Body)](#1.2 对象体 (Body))
- [1.3 交叉引用表 (XRef Table) - 传统V4格式](#1.3 交叉引用表 (XRef Table) - 传统V4格式)
- [1.4 文件尾 (Trailer)](#1.4 文件尾 (Trailer))
- [2. 传统XRef表(V4)的二进制布局(极致细节)](#2. 传统XRef表(V4)的二进制布局(极致细节))
-
- [2.1 整体结构](#2.1 整体结构)
- [2.2 子节 (Subsection) 结构](#2.2 子节 (Subsection) 结构)
- [2.3 XRef条目(20字节固定)](#2.3 XRef条目(20字节固定))
- [2.4 条目解析算法](#2.4 条目解析算法)
- [3. XRef流(V5)的二进制布局(极致细节)](#3. XRef流(V5)的二进制布局(极致细节))
-
- [3.1 流对象结构](#3.1 流对象结构)
- [3.2 W数组](#3.2 W数组)
- [3.3 Index数组](#3.3 Index数组)
- [3.4 二进制条目布局](#3.4 二进制条目布局)
- [3.5 变长整数读取(GetVarInt)](#3.5 变长整数读取(GetVarInt))
- [4. 代码前置:关键数据结构](#4. 代码前置:关键数据结构)
-
- [4.1 CPDF_SyntaxParser](#4.1 CPDF_SyntaxParser)
- [4.2 CPDF_Parser中的对象信息映射](#4.2 CPDF_Parser中的对象信息映射)
- [4.3 常量定义](#4.3 常量定义)
- [5. StartParse:入口函数逐行解析](#5. StartParse:入口函数逐行解析)
-
- [5.1 函数签名与初始检查](#5.1 函数签名与初始检查)
- [5.2 查找文件头](#5.2 查找文件头)
- [5.3 初始化语法解析器](#5.3 初始化语法解析器)
- [5.4 读取版本号](#5.4 读取版本号)
- [5.5 定位到文件末尾附近](#5.5 定位到文件末尾附近)
- [5.6 向后搜索startxref](#5.6 向后搜索startxref)
- [5.7 读取XRef偏移量](#5.7 读取XRef偏移量)
- [5.8 加载交叉引用表](#5.8 加载交叉引用表)
- [5.9 没有startxref的情况](#5.9 没有startxref的情况)
- [5.10 设置加密处理器](#5.10 设置加密处理器)
- [5.11 加载文档并验证](#5.11 加载文档并验证)
- [5.12 根对象编号检查](#5.12 根对象编号检查)
- [5.13 Metadata特殊处理](#5.13 Metadata特殊处理)
- [6. LoadAllCrossRefV4:加载传统XRef链](#6. LoadAllCrossRefV4:加载传统XRef链)
-
- [6.1 设计意图](#6.1 设计意图)
- [6.2 代码逐行解析](#6.2 代码逐行解析)
- [7. LoadCrossRefV4:解析单个XRef表](#7. LoadCrossRefV4:解析单个XRef表)
-
- [7.1 函数签名](#7.1 函数签名)
- [7.2 定位与关键字验证](#7.2 定位与关键字验证)
- [7.3 记录位置](#7.3 记录位置)
- [7.4 循环解析子节](#7.4 循环解析子节)
- [7.5 解析子节头](#7.5 解析子节头)
- [7.6 处理条目数据(如果bSkip==false)](#7.6 处理条目数据(如果bSkip==false))
- [7.7 移动到下一个子节](#7.7 移动到下一个子节)
- [7.8 处理关联的XRef流](#7.8 处理关联的XRef流)
- [8. LoadAllCrossRefV5:加载XRef流链](#8. LoadAllCrossRefV5:加载XRef流链)
- [9. LoadCrossRefV5:解析单个XRef流](#9. LoadCrossRefV5:解析单个XRef流)
-
- [9.1 解析流对象](#9.1 解析流对象)
- [9.2 验证为流对象](#9.2 验证为流对象)
- [9.3 处理Trailer](#9.3 处理Trailer)
- [9.4 解析Index数组](#9.4 解析Index数组)
- [9.5 解析W数组](#9.5 解析W数组)
- [9.6 加载流数据](#9.6 加载流数据)
- [9.7 遍历所有子节](#9.7 遍历所有子节)
- [9.8 遍历子节中的每个条目](#9.8 遍历子节中的每个条目)
- [10. 辅助函数与对象映射表](#10. 辅助函数与对象映射表)
-
- [10.1 GetObjectType](#10.1 GetObjectType)
- [10.2 GetObjectOffset](#10.2 GetObjectOffset)
- [10.3 ShrinkObjectMap](#10.3 ShrinkObjectMap)
- [10.4 GetVarInt](#10.4 GetVarInt)
- [11. 总结:数据流图](#11. 总结:数据流图)
- [12. 常见问题与边界情况](#12. 常见问题与边界情况)
-
- [Q1: 为什么传统XRef条目是20字节?](#Q1: 为什么传统XRef条目是20字节?)
- [Q2: 为什么需要`m_SortedOffset`?](#Q2: 为什么需要
m_SortedOffset?) - [Q3: 如何处理损坏的XRef表?](#Q3: 如何处理损坏的XRef表?)
- [Q4: 为什么`LoadCrossRefV4`中要检查偏移为0时的数字有效性?](#Q4: 为什么
LoadCrossRefV4中要检查偏移为0时的数字有效性?) - [Q5: 压缩对象(type=2)为什么要设置对象流的type=255?](#Q5: 压缩对象(type=2)为什么要设置对象流的type=255?)
PDF交叉引用表解析:极致详解
目录
- PDF文件整体结构(逐字节分析)
- 传统XRef表(V4)的二进制布局
- XRef流(V5)的二进制布局
- 代码前置:关键数据结构
- StartParse:入口函数逐行解析
- LoadAllCrossRefV4:加载传统XRef链
- LoadCrossRefV4:解析单个XRef表
- LoadAllCrossRefV5:加载XRef流链
- LoadCrossRefV5:解析单个XRef流
- 辅助函数与对象映射表
1. PDF文件整体结构(逐字节分析)
一个完整的PDF文件由四个物理部分组成:
[Header] [Body] [Cross-Reference Table] [Trailer]
1.1 文件头 (Header)
位置:文件的最开头(可能有前导垃圾数据,但规范要求%PDF-1.x在文件前1024字节内)
偏移(hex) 内容(ASCII) 说明
0x00 '%' 25h
0x01 'P' 50h
0x02 'D' 44h
0x03 'F' 46h
0x04 '-' 2Dh
0x05 '1' 31h ← 主版本号
0x06 '.' 2Eh
0x07 '4' 34h ← 次版本号
0x08 '\r' 0Dh
0x09 '\n' 0Ah
0x0A '%' 25h
0x0B '\xE2' (二进制标记)
0x0C '\xE3'
0x0D '\xCF'
0x0E '\xD3'
0x0F '\r'
0x10 '\n'
1.2 对象体 (Body)
由一系列间接对象组成。每个间接对象格式:
<objnum> <gennum> obj
<< ... >> (字典)
stream
...数据...
endstream
endobj
示例:
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>
stream
...页面内容流...
endstream
endobj
1.3 交叉引用表 (XRef Table) - 传统V4格式
位置:通常在文件末尾附近,但可以由startxref指定任意位置。
xref
0 3
0000000000 65535 f
0000000016 00000 n
0000000081 00000 n
trailer
<< /Size 3 /Root 1 0 R >>
startxref
200
%%EOF
1.4 文件尾 (Trailer)
包含字典、startxref和%%EOF。字典中关键字段:
| 字段 | 含义 |
|---|---|
| /Size | 文件中对象总数(最大对象号+1) |
| /Root | 根目录对象引用 |
| /Info | 信息字典引用 |
| /Prev | 上一版本XRef表位置(增量更新) |
| /ID | 文件标识符数组 |
| /Encrypt | 加密字典 |
2. 传统XRef表(V4)的二进制布局(极致细节)
2.1 整体结构
"xref" LF? ← 关键字,后面可跟空格或换行
<subsection1>
<subsection2>
...
"trailer" LF?
<dictionary>
"startxref" LF?
<number> LF?
"%%EOF"
2.2 子节 (Subsection) 结构
每个子节行:
<start> <count> LF
<start>: 十进制整数,本子节第一个对象号<count>: 十进制整数,本子节连续对象的个数- 中间用一个空格分隔,末尾可以是LF或CR+LF
2.3 XRef条目(20字节固定)
每个条目占用恰好20字节(包括换行符)。PDF规范ISO 32000-1:2008 表17定义:
偏移(字节) 长度 内容 示例
0-9 10 对象偏移量,十进制,右对齐,前导空格 " 123"
10 1 空格 " "
11-15 5 生成号,十进制,右对齐,前导空格 " 0"
16 1 空格 " "
17 1 状态:'n' 或 'f' "n"
18-19 2 行结束符:空格+换行 或 回车+换行 "\r\n"
重要:偏移量是相对于文件开头的字节数,从0开始计数。
示例条目:
" 123 00000 n\r\n"
0 9 10 16 17 19
2.4 条目解析算法
给定一个指向条目开始的指针p(char*):
- 偏移量 = atoi§ // 前10个字符,忽略前导空格
- 生成号 = atoi(p+11) // 从第12个字符开始(0索引11)
- 状态 = p[17] // 第18个字符
3. XRef流(V5)的二进制布局(极致细节)
3.1 流对象结构
pdf
<objnum> 0 obj
<<
/Type /XRef
/Size <integer>
/Prev <integer>
/W [<int> <int> <int>]
/Index [<int> <int> ...]
/DecodeParms << /Columns <int> /Predictor <int> >> (可选)
>>
stream
...二进制数据...
endstream
endobj
3.2 W数组
W数组定义每个XRef条目的字段宽度(单位:字节)。长度固定为3(类型、字段2、字段3)。例如:
/W [1 4 1]→ 每个条目1+4+1=6字节/W [0 8 0]→ 每个条目8字节(类型字段宽度0表示该字段不使用,默认为0)
字段含义:
- 字段1:类型 (Type)
- 0 = 空闲对象
- 1 = 未压缩对象(字段2 = 文件偏移)
- 2 = 压缩对象(字段2 = 包含该对象的对象流编号)
- 字段2:根据类型不同,含义不同
- 字段3:生成号(通常为0,除非该对象被重用时增加)
3.3 Index数组
Index数组定义哪些对象号范围在流中有条目。格式:
/Index [ start1 count1 start2 count2 ... ]
如果没有Index数组,则默认从0到Size-1。
3.4 二进制条目布局
条目按顺序连续存储,每个条目使用W数组定义的宽度。
示例:W=[1,4,1],条目数据(十六进制):
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,空闲对象
3.5 变长整数读取(GetVarInt)
由于字段宽度可变(可能是0,1,2,4,8字节),需要按大端序读取:
cpp
uint32_t GetVarInt(const uint8_t* p, int32_t n) {
uint32_t result = 0;
for (int32_t i = 0; i < n; ++i)
result = (result << 8) | p[i];
return result;
}
示例:
- p = {0x00, 0x01, 0x00}, n=3 → 0x000100 = 256
- p = {0x12, 0x34}, n=2 → 0x1234 = 4660
4. 代码前置:关键数据结构
在解析代码前,必须理解以下几个核心数据结构。
4.1 CPDF_SyntaxParser
语法解析器,维护文件读取状态:
cpp
class CPDF_SyntaxParser {
FX_FILESIZE m_Pos; // 当前读取位置(相对于文件头)
FX_FILESIZE m_FileLen; // 文件总长度
CFX_RetainPtr<IFX_SeekableReadStream> m_pFileAccess; // 文件访问接口
uint32_t m_MetadataObjnum; // 特殊处理的Metadata对象号
};
4.2 CPDF_Parser中的对象信息映射
cpp
struct {
FX_FILESIZE pos; // 文件偏移(type=1)或对象流编号(type=2)
uint16_t gennum; // 生成号
uint8_t type; // 0=空闲,1=正常,2=压缩,255=压缩流标记
} ObjectInfo;
std::map<uint32_t, ObjectInfo> m_ObjectInfo; // 对象号 -> 信息
std::set<FX_FILESIZE> m_SortedOffset; // 所有对象偏移的有序集合(用于计算对象大小)
4.3 常量定义
cpp
const int32_t kMaxXRefSize = 1048576; // 最大XRef表大小(1M个对象)
const uint32_t kMaxObjectNumber = 0x3FFFFFFF; // 最大对象号(约10亿)
5. StartParse:入口函数逐行解析
5.1 函数签名与初始检查
cpp
CPDF_Parser::Error CPDF_Parser::StartParse(
const CFX_RetainPtr<IFX_SeekableReadStream>& pFileAccess,
CPDF_Document* pDocument) {
参数:
pFileAccess:文件读取接口,支持Seek和ReadpDocument:要填充的文档对象
返回值:枚举Error(SUCCESS, FORMAT_ERROR, PASSWORD_ERROR等)
cpp
ASSERT(!m_bHasParsed);
m_bHasParsed = true; // 防止重复解析
m_bXRefStream = false; // 初始假设为传统XRef
m_LastXRefOffset = 0;
5.2 查找文件头
cpp
int32_t offset = GetHeaderOffset(pFileAccess);
if (offset == -1)
return FORMAT_ERROR;
GetHeaderOffset实现(伪代码):
- 读取文件前1024字节
- 搜索"%PDF"字符串
- 返回'%'的位置,如果没找到返回-1
为什么需要这个:有些PDF文件可能被包裹在其他数据中(如邮件附件、HTTP响应),所以PDF头不一定在文件开头。
5.3 初始化语法解析器
cpp
m_pSyntax->InitParser(pFileAccess, offset);
InitParser设置文件访问接口,并将当前读取位置设置为offset(即PDF头的起始位置)。这样后续所有GetChar、GetWord等操作都从PDF头开始。
5.4 读取版本号
PDF版本号位于文件头的第6和第8个字符(0索引):
位置: 0 1 2 3 4 5 6 7 8 9
字符: % P D F - 1 . 4 \r \n
↑ ↑
主版本 次版本
cpp
uint8_t ch;
if (!m_pSyntax->GetCharAt(5, ch))
return FORMAT_ERROR;
if (std::isdigit(ch))
m_FileVersion = FXSYS_DecimalCharToInt(static_cast<wchar_t>(ch)) * 10;
if (!m_pSyntax->GetCharAt(7, ch))
return FORMAT_ERROR;
if (std::isdigit(ch))
m_FileVersion += FXSYS_DecimalCharToInt(static_cast<wchar_t>(ch));
GetCharAt(5, ch):从m_pSyntax当前基础位置(即offset)加上5字节处读取一个字符。
版本号存储为整数:例如"1.4" → 10+4=14。这样方便比较版本。
5.5 定位到文件末尾附近
cpp
if (m_pSyntax->m_FileLen < m_pSyntax->m_HeaderOffset + 9)
return FORMAT_ERROR;
m_pSyntax->SetPos(m_pSyntax->m_FileLen - m_pSyntax->m_HeaderOffset - 9);
计算:
m_HeaderOffset是PDF头在文件中的绝对偏移(通常就是offset)m_FileLen是文件总长度m_pSyntax->SetPos设置的是相对于PDF头的偏移(即逻辑位置)- 因此逻辑位置 = 绝对位置 - m_HeaderOffset
m_FileLen - m_HeaderOffset - 9:文件末尾倒数第9个字节(因为"%%EOF"至少占5字节,留出空间)。这样我们可以向后搜索"startxref"。
5.6 向后搜索startxref
cpp
m_pDocument = pDocument;
bool bXRefRebuilt = false;
if (m_pSyntax->BackwardsSearchToWord("startxref", 4096)) {
BackwardsSearchToWord实现:
- 从当前位置向文件开头方向扫描,每次读取一个字符
- 匹配"startxref"字符串
- 最多搜索
4096字节(PDF规范要求startxref在文件最后1024字节内,这里多搜了4倍,更安全)
如果找到,函数返回true,并且当前解析器位置设置在"startxref"第一个字符处。
5.7 读取XRef偏移量
cpp
m_SortedOffset.insert(m_pSyntax->GetPos());
m_pSyntax->GetKeyword();
bool bNumber;
CFX_ByteString xrefpos_str = m_pSyntax->GetNextWord(&bNumber);
if (!bNumber)
return FORMAT_ERROR;
m_LastXRefOffset = (FX_FILESIZE)FXSYS_atoi64(xrefpos_str.c_str());
m_SortedOffset记录每个XRef表的位置,用于后续计算对象大小。GetKeyword()读取"startxref"并消费它。GetNextWord读取下一个token(应该是数字字符串),并判断是否为数字。- 转换为64位整数(文件可能大于4GB)。
5.8 加载交叉引用表
cpp
if (!LoadAllCrossRefV4(m_LastXRefOffset) &&
!LoadAllCrossRefV5(m_LastXRefOffset)) {
if (!RebuildCrossRef())
return FORMAT_ERROR;
bXRefRebuilt = true;
m_LastXRefOffset = 0;
}
先尝试V4传统格式,失败则尝试V5流格式,都失败则重建(通过扫描文件中的所有对象)。
5.9 没有startxref的情况
cpp
} else {
if (!RebuildCrossRef())
return FORMAT_ERROR;
bXRefRebuilt = true;
}
如果文件中找不到"startxref",直接重建。
5.10 设置加密处理器
cpp
Error eRet = SetEncryptHandler();
if (eRet != SUCCESS)
return eRet;
SetEncryptHandler会检查trailer中的/Encrypt字典,如果是标准加密则初始化安全处理器。
5.11 加载文档并验证
cpp
m_pDocument->LoadDoc();
if (!m_pDocument->GetRoot() || m_pDocument->GetPageCount() == 0) {
if (bXRefRebuilt)
return FORMAT_ERROR;
ReleaseEncryptHandler();
if (!RebuildCrossRef())
return FORMAT_ERROR;
eRet = SetEncryptHandler();
if (eRet != SUCCESS)
return eRet;
m_pDocument->LoadDoc();
if (!m_pDocument->GetRoot())
return FORMAT_ERROR;
}
LoadDoc会解析根字典和页面树。如果根字典不存在或没有页面,且之前没有重建过XRef,则尝试重建后再加载。
5.12 根对象编号检查
cpp
if (GetRootObjNum() == 0) {
ReleaseEncryptHandler();
if (!RebuildCrossRef() || GetRootObjNum() == 0)
return FORMAT_ERROR;
eRet = SetEncryptHandler();
if (eRet != SUCCESS)
return eRet;
}
GetRootObjNum返回trailer中/Root引用的对象号。如果为0,说明trailer无效,重建。
5.13 Metadata特殊处理
cpp
if (m_pSecurityHandler && !m_pSecurityHandler->IsMetadataEncrypted()) {
CPDF_Reference* pMetadata =
ToReference(m_pDocument->GetRoot()->GetObjectFor("Metadata"));
if (pMetadata)
m_pSyntax->m_MetadataObjnum = pMetadata->GetRefObjNum();
}
如果存在安全处理器且元数据未加密,则记录Metadata的对象号。这样在解析时,即使文档加密,Metadata流也可以不被解密(因为其本身可能未加密)。
cpp
return SUCCESS;
}
6. LoadAllCrossRefV4:加载传统XRef链
6.1 设计意图
PDF支持增量更新:每次保存时,新内容追加到文件末尾,并创建一个新的XRef表,通过/Prev指向上一个XRef表。这样就形成了一个链表(最新的在文件末尾)。LoadAllCrossRefV4的任务是:
- 加载最新的XRef表(包括trailer)
- 沿着
/Prev链回溯,加载所有历史XRef表 - 合并所有条目,较新的条目覆盖较旧的
6.2 代码逐行解析
cpp
bool CPDF_Parser::LoadAllCrossRefV4(FX_FILESIZE xrefpos) {
// 步骤1:加载最新的XRef表,bSkip=true表示只解析子节头,不解析具体条目
if (!LoadCrossRefV4(xrefpos, 0, true))
return false;
xrefpos是startxref给出的最新XRef表位置(绝对偏移)。LoadCrossRefV4会验证"xref"关键字,并跳过条目数据(只解析子节结构),同时也会读取紧随其后的trailer字典并存储在m_pTrailer中。
cpp
m_pTrailer = LoadTrailerV4();
if (!m_pTrailer)
return false;
LoadTrailerV4假设当前解析器位置正好在"trailer"关键字处,读取字典并返回。注意这里又调用了一次,但实际上LoadCrossRefV4已经读取了trailer并存储在了某个地方?让我们看LoadCrossRefV4的实现:当bSkip=true时,它不会读取trailer,而是解析完子节后退出,此时解析器位置应该在"trailer"之前。所以需要单独调用LoadTrailerV4。
cpp
int32_t xrefsize = GetDirectInteger(m_pTrailer.get(), "Size");
if (xrefsize > 0 && xrefsize <= kMaxXRefSize)
ShrinkObjectMap(xrefsize);
ShrinkObjectMap(xrefsize)删除所有对象号 >= xrefsize的条目,因为Size声明了对象总数,超出部分是无效的。
cpp
std::vector<FX_FILESIZE> CrossRefList;
std::vector<FX_FILESIZE> XRefStreamList;
std::set<FX_FILESIZE> seen_xrefpos;
CrossRefList.push_back(xrefpos);
XRefStreamList.push_back(GetDirectInteger(m_pTrailer.get(), "XRefStm"));
seen_xrefpos.insert(xrefpos);
XRefStm是PDF 1.5中引入的,允许混合使用传统XRef表和XRef流。如果存在,它指向一个XRef流的位置。这里先记录下来,后面会用到。
cpp
xrefpos = GetDirectInteger(m_pTrailer.get(), "Prev");
while (xrefpos) {
// 检查循环引用
if (pdfium::ContainsKey(seen_xrefpos, xrefpos))
return false;
seen_xrefpos.insert(xrefpos);
// 插入到链表开头(保持时间顺序从旧到新)
CrossRefList.insert(CrossRefList.begin(), xrefpos);
LoadCrossRefV4(xrefpos, 0, true);
std::unique_ptr<CPDF_Dictionary> pDict(LoadTrailerV4());
if (!pDict)
return false;
xrefpos = GetDirectInteger(pDict.get(), "Prev");
XRefStreamList.insert(XRefStreamList.begin(),
pDict->GetIntegerFor("XRefStm"));
m_Trailers.push_back(std::move(pDict));
}
这个循环沿着Prev链回溯,直到Prev为0。注意CrossRefList和XRefStreamList都是插入到开头,这样最终顺序是:索引0对应最旧的XRef,索引n-1对应最新的XRef。而m_Trailers则是按从旧到新的顺序存储(每次push_back)。
cpp
for (size_t i = 0; i < CrossRefList.size(); ++i) {
if (!LoadCrossRefV4(CrossRefList[i], XRefStreamList[i], false))
return false;
if (i == 0 && !VerifyCrossRefV4())
return false;
}
return true;
}
现在按从旧到新的顺序加载实际条目(bSkip=false)。最新的XRef表(i==0)会覆盖旧的条目。加载完成后,验证最新XRef表的一致性(VerifyCrossRefV4会抽查几个对象,检查对象号是否匹配)。
7. LoadCrossRefV4:解析单个XRef表
这是最核心的函数,处理一个传统XRef表(可能包含多个子节)。
7.1 函数签名
cpp
bool CPDF_Parser::LoadCrossRefV4(FX_FILESIZE pos,
FX_FILESIZE streampos,
bool bSkip)
pos:XRef表起始位置(绝对偏移)streampos:关联的XRef流位置(可为0)bSkip:true时只解析子节头,不解析条目;false时解析条目
7.2 定位与关键字验证
cpp
m_pSyntax->SetPos(pos);
if (m_pSyntax->GetKeyword() != "xref")
return false;
SetPos设置逻辑位置(pos - m_HeaderOffset)。GetKeyword读取下一个token,忽略空白,必须为"xref"。
7.3 记录位置
cpp
m_SortedOffset.insert(pos);
if (streampos)
m_SortedOffset.insert(streampos);
m_SortedOffset存储所有交叉引用表的位置(包括XRef流的位置),用于后续计算每个对象的占用大小(通过查找下一个对象的位置)。
7.4 循环解析子节
cpp
while (1) {
FX_FILESIZE SavedPos = m_pSyntax->GetPos();
bool bIsNumber;
CFX_ByteString word = m_pSyntax->GetNextWord(&bIsNumber);
if (word.IsEmpty())
return false;
if (!bIsNumber) {
m_pSyntax->SetPos(SavedPos);
break;
}
读取第一个token。如果是数字,则认为是子节起始对象号;如果不是数字(可能是"trailer"),则退出循环,回退位置。
7.5 解析子节头
cpp
uint32_t start_objnum = FXSYS_atoui(word.c_str());
if (start_objnum >= kMaxObjectNumber)
return false;
uint32_t count = m_pSyntax->GetDirectNum();
m_pSyntax->ToNextWord();
SavedPos = m_pSyntax->GetPos();
const int32_t recordsize = 20;
GetDirectNum读取下一个数字(对象数量)。ToNextWord跳过空白,使位置指向条目数据的开始。SavedPos保存条目数据的起始逻辑位置。
7.6 处理条目数据(如果bSkip==false)
cpp
m_dwXrefStartObjNum = start_objnum;
if (!bSkip) {
std::vector<char> buf(1024 * recordsize + 1);
buf[1024 * recordsize] = '\0';
分配一个缓冲区,一次读取1024个条目(20KB)。加1是为了安全地放置结束符。
cpp
int32_t nBlocks = count / 1024 + 1;
for (int32_t block = 0; block < nBlocks; block++) {
int32_t block_size = block == nBlocks - 1 ? count % 1024 : 1024;
m_pSyntax->ReadBlock(reinterpret_cast<uint8_t*>(buf.data()),
block_size * recordsize);
ReadBlock从当前文件位置读取指定字节数到缓冲区。注意:m_pSyntax的当前位置在上次调用ToNextWord后已经指向条目数据开始,并且每次读取后会自动移动位置。
cpp
for (int32_t i = 0; i < block_size; i++) {
uint32_t objnum = start_objnum + block * 1024 + i;
char* pEntry = &buf[i * recordsize];
计算对象号,并获取指向该条目的指针。
cpp
if (pEntry[17] == 'f') {
m_ObjectInfo[objnum].pos = 0;
m_ObjectInfo[objnum].type = 0;
} else {
检查第18个字符(索引17)。PDF规范允许条目前面有空格,但偏移量固定10字节,所以pEntry[17]一定是'n'或'f',除非文件损坏。
cpp
FX_FILESIZE offset = (FX_FILESIZE)FXSYS_atoi64(pEntry);
if (offset == 0) {
for (int32_t c = 0; c < 10; c++) {
if (!std::isdigit(pEntry[c]))
return false;
}
}
atoi64会跳过前导空格,所以即使偏移量字段有前导空格也能正确转换。如果偏移为0,需要额外验证前10个字符都是数字,防止将全空格字符串误认为0。
cpp
m_ObjectInfo[objnum].pos = offset;
int32_t version = FXSYS_atoi(pEntry + 11);
if (version >= 1)
m_bVersionUpdated = true;
m_ObjectInfo[objnum].gennum = version;
if (m_ObjectInfo[objnum].pos < m_pSyntax->m_FileLen)
m_SortedOffset.insert(m_ObjectInfo[objnum].pos);
m_ObjectInfo[objnum].type = 1;
}
}
}
}
生成号从第12个字符(索引11)开始解析,长度为5。注意:atoi也会跳过前导空格,所以即使生成号字段有前导空格也没问题。
如果偏移小于文件长度,则将该偏移加入m_SortedOffset,以便后续计算对象大小。
7.7 移动到下一个子节
cpp
m_pSyntax->SetPos(SavedPos + count * recordsize);
}
当前子节处理完后,将解析器位置设置为当前子节起始位置 + 条目数*20。这样下一个循环就能读取下一个子节的起始对象号。
7.8 处理关联的XRef流
cpp
return !streampos || LoadCrossRefV5(&streampos, false);
}
如果streampos非0,则调用LoadCrossRefV5加载该XRef流(bMainXRef=false)。这允许混合XRef表与XRef流(PDF 1.5+)。
8. LoadAllCrossRefV5:加载XRef流链
与V4类似,但处理的是XRef流(每个XRef流也是一个间接对象,通过/Prev链接)。
cpp
bool CPDF_Parser::LoadAllCrossRefV5(FX_FILESIZE xrefpos) {
if (!LoadCrossRefV5(&xrefpos, true))
return false;
std::set<FX_FILESIZE> seen_xrefpos;
while (xrefpos) {
seen_xrefpos.insert(xrefpos);
if (!LoadCrossRefV5(&xrefpos, false))
return false;
if (pdfium::ContainsKey(seen_xrefpos, xrefpos))
return false;
}
m_ObjectStreamMap.clear();
m_bXRefStream = true;
return true;
}
LoadCrossRefV5的第一个参数是指针,函数会修改它指向Prev的值。bMainXRef=true表示这是最新的主XRef流,会设置m_pTrailer。bMainXRef=false表示是历史版本,trailer存入m_Trailers链表。- 清空
m_ObjectStreamMap(因为XRef流不需要传统的对象流映射)。 - 设置
m_bXRefStream=true,后续解析知道使用流模式。
9. LoadCrossRefV5:解析单个XRef流
这是最复杂的函数,涉及流对象解析、W数组、Index数组、二进制数据读取。
9.1 解析流对象
cpp
bool CPDF_Parser::LoadCrossRefV5(FX_FILESIZE* pos, bool bMainXRef) {
std::unique_ptr<CPDF_Object> pObject(
ParseIndirectObjectAt(m_pDocument, *pos, 0));
if (!pObject)
return false;
uint32_t objnum = pObject->m_ObjNum;
if (!objnum)
return false;
CPDF_Object* pUnownedObject = pObject.get();
if (m_pDocument) {
CPDF_Dictionary* pRootDict = m_pDocument->GetRoot();
if (pRootDict && pRootDict->GetObjNum() == objnum)
return false;
if (!m_pDocument->ReplaceIndirectObjectIfHigherGeneration(
objnum, std::move(pObject))) {
return false;
}
}
ParseIndirectObjectAt解析指定位置的对象(可能是任何类型)。如果对象是根字典,则忽略(防止循环)。ReplaceIndirectObjectIfHigherGeneration检查如果已存在相同对象号但生成号更高,则替换;否则保留旧的。
9.2 验证为流对象
cpp
CPDF_Stream* pStream = pUnownedObject->AsStream();
if (!pStream)
return false;
CPDF_Dictionary* pDict = pStream->GetDict();
*pos = pDict->GetIntegerFor("Prev");
int32_t size = pDict->GetIntegerFor("Size");
if (size < 0)
return false;
获取流字典,读取/Prev和/Size。注意*pos被更新为上一版本XRef流的位置。
9.3 处理Trailer
cpp
std::unique_ptr<CPDF_Dictionary> pNewTrailer = ToDictionary(pDict->Clone());
if (bMainXRef) {
m_pTrailer = std::move(pNewTrailer);
ShrinkObjectMap(size);
for (auto& it : m_ObjectInfo)
it.second.type = 0;
} else {
m_Trailers.push_back(std::move(pNewTrailer));
}
克隆字典作为trailer。如果是主XRef,则重置m_ObjectInfo中所有对象的type为0(准备用新条目填充)。注意:这里ShrinkObjectMap(size)会删除对象号>=size的条目,但不清除其余条目的type,所以紧接着手动将所有type设为0。
9.4 解析Index数组
cpp
std::vector<std::pair<int32_t, int32_t>> arrIndex;
CPDF_Array* pArray = pDict->GetArrayFor("Index");
if (pArray) {
for (size_t i = 0; i < pArray->GetCount() / 2; i++) {
CPDF_Object* pStartNumObj = pArray->GetObjectAt(i * 2);
CPDF_Object* pCountObj = pArray->GetObjectAt(i * 2 + 1);
if (ToNumber(pStartNumObj) && ToNumber(pCountObj)) {
int nStartNum = pStartNumObj->GetInteger();
int nCount = pCountObj->GetInteger();
if (nStartNum >= 0 && nCount > 0)
arrIndex.push_back(std::make_pair(nStartNum, nCount));
}
}
}
if (arrIndex.size() == 0)
arrIndex.push_back(std::make_pair(0, size));
Index数组每两个元素一组:(start, count)。如果没有Index,则默认从0到size-1。
9.5 解析W数组
cpp
pArray = pDict->GetArrayFor("W");
if (!pArray)
return false;
std::vector<uint32_t> WidthArray;
FX_SAFE_UINT32 dwAccWidth = 0;
for (size_t i = 0; i < pArray->GetCount(); ++i) {
WidthArray.push_back(pArray->GetIntegerAt(i));
dwAccWidth += WidthArray[i];
}
if (!dwAccWidth.IsValid() || WidthArray.size() < 3)
return false;
uint32_t totalWidth = dwAccWidth.ValueOrDie();
FX_SAFE_UINT32用于防止整数溢出。totalWidth是每个XRef条目的总字节数。
9.6 加载流数据
cpp
auto pAcc = pdfium::MakeRetain<CPDF_StreamAcc>(pStream);
pAcc->LoadAllData();
const uint8_t* pData = pAcc->GetData();
uint32_t dwTotalSize = pAcc->GetSize();
uint32_t segindex = 0;
CPDF_StreamAcc负责解压流(如果压缩了)并提供连续的内存数据。
9.7 遍历所有子节
cpp
for (uint32_t i = 0; i < arrIndex.size(); i++) {
int32_t startnum = arrIndex[i].first;
if (startnum < 0)
continue;
m_dwXrefStartObjNum = pdfium::base::checked_cast<uint32_t>(startnum);
uint32_t count = pdfium::base::checked_cast<uint32_t>(arrIndex[i].second);
FX_SAFE_UINT32 dwCaculatedSize = segindex;
dwCaculatedSize += count;
dwCaculatedSize *= totalWidth;
if (!dwCaculatedSize.IsValid() ||
dwCaculatedSize.ValueOrDie() > dwTotalSize) {
continue;
}
const uint8_t* segstart = pData + segindex * totalWidth;
FX_SAFE_UINT32 dwMaxObjNum = startnum;
dwMaxObjNum += count;
uint32_t dwV5Size = m_ObjectInfo.empty() ? 0 : GetLastObjNum() + 1;
if (!dwMaxObjNum.IsValid() || dwMaxObjNum.ValueOrDie() > dwV5Size)
continue;
segindex是当前已处理的条目数(在所有子节中累计)。segstart指向当前子节在流中的起始位置。检查计算出的总字节数不超过流总大小。同时检查对象号范围是否超出当前对象映射表的大小(如果之前ShrinkObjectMap已经设置了大小,则不能超出)。
9.8 遍历子节中的每个条目
cpp
for (uint32_t j = 0; j < count; j++) {
int32_t type = 1;
const uint8_t* entrystart = segstart + j * totalWidth;
if (WidthArray[0])
type = GetVarInt(entrystart, WidthArray[0]);
if (GetObjectType(startnum + j) == 255) {
FX_FILESIZE offset =
GetVarInt(entrystart + WidthArray[0], WidthArray[1]);
m_ObjectInfo[startnum + j].pos = offset;
m_SortedOffset.insert(offset);
continue;
}
if (GetObjectType(startnum + j))
continue;
m_ObjectInfo[startnum + j].type = type;
if (type == 0) {
m_ObjectInfo[startnum + j].pos = 0;
} else {
FX_FILESIZE offset =
GetVarInt(entrystart + WidthArray[0], WidthArray[1]);
m_ObjectInfo[startnum + j].pos = offset;
if (type == 1) {
m_SortedOffset.insert(offset);
} else {
if (offset < 0 || !IsValidObjectNumber(offset))
return false;
m_ObjectInfo[offset].type = 255;
}
}
}
segindex += count;
}
return true;
}
详细说明:
-
GetVarInt读取指定宽度的整数(大端序)。 -
如果当前对象的类型已经是255(之前被标记为压缩流对象),则跳过(保持原状)。
-
如果对象类型已经非0(已被更早的XRef表定义),也跳过(新条目不覆盖旧条目?等等,这里逻辑有点反直觉。通常较新的XRef表应该覆盖旧的,但这里如果
GetObjectType(startnum+j)返回非0,就直接continue。这意味着一旦某个对象号有了类型,就不会被后续子节覆盖。但注意,在主XRef加载时,之前已经将所有type设为0,所以实际上只有同一个XRef流内的多个Index段之间可能存在覆盖,但后面的段不会覆盖前面的?实际上arrIndex的顺序就是Index数组中定义的顺序,后面的段会覆盖前面的段吗?不会,因为遇到非0就跳过。这意味着Index数组中后面的段如果和前面有重叠,后面的会被忽略。这可能是设计上的缺陷,但实际使用中Index数组的段不应重叠。) -
类型0:空闲对象,偏移设0。
-
类型1:普通对象,记录偏移,并将偏移加入
m_SortedOffset。 -
类型2:压缩对象,offset实际是对象流编号。检查该编号有效后,将该对象流对象(编号为offset)的类型标记为255,表示它是一个对象流(容器)。注意:这里
m_ObjectInfo[offset].type = 255,而offset是对象流编号,不是当前对象号。这建立了压缩对象到对象流的间接关系。
10. 辅助函数与对象映射表
10.1 GetObjectType
cpp
uint8_t CPDF_Parser::GetObjectType(uint32_t objnum) const {
ASSERT(IsValidObjectNumber(objnum));
auto it = m_ObjectInfo.find(objnum);
return it != m_ObjectInfo.end() ? it->second.type : 0;
}
通过map查找对象类型,如果不存在则返回0(空闲)。
10.2 GetObjectOffset
cpp
FX_FILESIZE CPDF_Parser::GetObjectOffset(uint32_t objnum) const {
if (!IsValidObjectNumber(objnum))
return 0;
if (GetObjectType(objnum) == 1)
return GetObjectPositionOrZero(objnum);
if (GetObjectType(objnum) == 2) {
FX_FILESIZE pos = GetObjectPositionOrZero(objnum);
return GetObjectPositionOrZero(pos);
}
return 0;
}
- 类型1:直接返回存储的偏移。
- 类型2:返回的pos是对象流编号,再查该对象流的位置(类型应为255)。
- 类型0或255:返回0。
10.3 ShrinkObjectMap
cpp
void CPDF_Parser::ShrinkObjectMap(uint32_t objnum) {
if (objnum == 0) {
m_ObjectInfo.clear();
return;
}
auto it = m_ObjectInfo.lower_bound(objnum);
while (it != m_ObjectInfo.end()) {
auto saved_it = it++;
m_ObjectInfo.erase(saved_it);
}
if (!pdfium::ContainsKey(m_ObjectInfo, objnum - 1))
m_ObjectInfo[objnum - 1].pos = 0;
}
删除所有对象号 >= objnum 的条目。最后确保 objnum-1 存在(如果不存在则创建一个占位条目,pos=0)。这是为了后续GetLastObjNum()能正确返回最大对象号。
10.4 GetVarInt
cpp
uint32_t GetVarInt(const uint8_t* p, int32_t n) {
uint32_t result = 0;
for (int32_t i = 0; i < n; ++i)
result = result * 256 + p[i];
return result;
}
大端序转换。例如n=4时,将p[0]<<24 + p[1]<<16 + p[2]<<8 + p[3]。
11. 总结:数据流图
PDF文件
│
▼
StartParse: 找到startxref → 获取最新XRef位置
│
├─► LoadAllCrossRefV4 (传统模式)
│ │
│ ├─► LoadCrossRefV4 (bSkip=true) → 解析子节头,获取trailer和Prev
│ │
│ └─► 循环Prev链,收集所有XRef位置
│ │
│ └─► 从旧到新 LoadCrossRefV4 (bSkip=false)
│ │
│ └─► 解析20字节条目,填充 m_ObjectInfo
│
└─► LoadAllCrossRefV5 (流模式)
│
├─► LoadCrossRefV5 (bMainXRef=true) → 解析XRef流对象
│ │
│ ├─► 读取/Size, /Prev, /W, /Index
│ ├─► 加载流数据
│ └─► 按Index分段解析二进制条目,填充 m_ObjectInfo
│
└─► 循环Prev链,加载历史XRef流
最终,m_ObjectInfo包含了所有对象的位置/类型信息,m_SortedOffset包含了所有对象偏移的有序列表,可用于计算每个对象的精确大小。
12. 常见问题与边界情况
Q1: 为什么传统XRef条目是20字节?
A: PDF 1.0设计时为了简单,固定长度便于随机访问。20字节可以容纳最大10位十进制偏移(2^32约10位),5位生成号,以及空格和换行。
Q2: 为什么需要m_SortedOffset?
A: 为了计算对象的大小。一个对象的结束位置是下一个对象的开始位置。通过将所有对象偏移排序,可以快速得到每个对象占用的字节数。
Q3: 如何处理损坏的XRef表?
A: 如果LoadAllCrossRefV4和LoadAllCrossRefV5都失败,会调用RebuildCrossRef,该函数会扫描整个文件,查找所有"obj"关键字,重建对象位置映射。
Q4: 为什么LoadCrossRefV4中要检查偏移为0时的数字有效性?
A: 有些损坏的文件可能将偏移字段写成全空格,导致atoi返回0。此时需要额外验证是否真的是数字0,还是无效数据。
Q5: 压缩对象(type=2)为什么要设置对象流的type=255?
A: 这是一种标记,表示该对象号对应的是一个对象流(而不是普通对象)。当解析压缩对象时,需要先找到对象流,然后从流中读取对象。
希望这个极致详细的解释能帮助您完全理解PDF交叉引用表的解析过程。如果有任何具体细节仍不清楚,请指出,我可以进一步深入。