PDF结构的清晰图示 文章目录PDF结构的清晰图示一、PDF文件全景结构图二、文件头 (Header) 结构三、传统 XRef 表 (V4) 结构图3.1 XRef 表整体结构3.2 单个条目的 20 字节布局四、XRef 流 (V5) 结构图4.1 XRef 流对象结构4.2 W 数组决定条目布局4.3 Index 数组的作用五、Trailer 字典与 Prev 链5.1 Trailer 结构5.2 Prev 链增量更新六、代码与结构的完整对应流程步骤1寻找 startxref步骤2根据偏移加载 XRef步骤3判断 XRef 类型步骤4解析 V4 表步骤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对应代码m_pSyntax-GetCharAt(5,ch);// 读取主版本号字符m_FileVersionFXSYS_DecimalCharToInt(ch)*10;m_pSyntax-GetCharAt(7,ch);// 读取次版本号字符m_FileVersionFXSYS_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 LF3.2 单个条目的 20 字节布局字节索引: 0 9 10 15 16 17 18 19 ---------------------------- | 偏移量 |空格| 生成号 |空格| n|LF| (LF也可能是CRLF) ---------------------------- 示例: 123 00000 n\r\n 0 9 10 15 16 17 18 19代码解析// 读取条目缓冲区20字节char*pEntrybuf[i*20];if(pEntry[17]f){// 空闲对象m_ObjectInfo[objnum].type0;}else{// 正常对象FX_FILESIZE offsetFXSYS_atoi64(pEntry);// 取前10字节int32_tversionFXSYS_atoi(pEntry11);// 取第12-16字节m_ObjectInfo[objnum].posoffset;m_ObjectInfo[objnum].gennumversion;m_ObjectInfo[objnum].type1;}四、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 endobj4.2 W 数组决定条目布局W [1, 4, 1] 时每个条目占用 141 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, 空闲代码解析// 读取 W 数组for(size_t i0;ipArray-GetCount();i)WidthArray.push_back(pArray-GetIntegerAt(i));uint32_ttotalWidthWidthArray[0]WidthArray[1]WidthArray[2];// 遍历每个条目constuint8_t*entrypDataentry_index*totalWidth;inttypeGetVarInt(entry,WidthArray[0]);// 读取类型int64_tfield2GetVarInt(entryWidthArray[0],WidthArray[1]);// 读取第二字段intgenGetVarInt(entryWidthArray[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不在流中)代码for(size_t i0;iarrIndex.size();i){intstartnumarrIndex[i].first;// 起始对象号intcountarrIndex[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 表(或流)的绝对偏移 %%EOF5.2 Prev 链增量更新文件布局(时间从下往上): ┌─────────────────────┐ │ 最新版 trailer │ ← Prev 2000 │ startxref 5000 │ ├─────────────────────┤ │ 第二版 XRef (偏移5000)│ │ 第二版 trailer │ ← Prev 1000 ├─────────────────────┤ │ 第一版 XRef (偏移2000)│ │ 第一版 trailer │ ← Prev 0 ├─────────────────────┤ │ 原始对象体 │ └─────────────────────┘代码加载 Prev 链xrefposGetDirectInteger(m_pTrailer.get(),Prev);// 获取 Prev 值while(xrefpos){// 加载历史 XRef 表LoadCrossRefV4(xrefpos,0,true);// 获取新的 PrevxrefposGetDirectInteger(pDict.get(),Prev);}六、代码与结构的完整对应流程步骤1寻找 startxref文件末尾附近: ... startxref 54321 %%EOF ↑ BackwardsSearchToWord(startxref)代码if(m_pSyntax-BackwardsSearchToWord(startxref,4096)){m_pSyntax-GetKeyword();// 读取 startxrefCFX_ByteString xrefpos_strm_pSyntax-GetNextWord(bNumber);m_LastXRefOffsetFXSYS_atoi64(xrefpos_str.c_str());}步骤2根据偏移加载 XRefm_LastXRefOffset 54321 → 跳转到该位置开始解析 XRef步骤3判断 XRef 类型位置 54321 处: 如果是 xref → 传统 V4 表 如果是数字(对象号) → 可能是 XRef 流代码if(!LoadAllCrossRefV4(m_LastXRefOffset)!LoadAllCrossRefV5(m_LastXRefOffset)){// 重建}步骤4解析 V4 表54321: xref → m_pSyntax-GetKeyword() 验证 54327: 0 5 → 读取 start_objnum0, count5 54333: 开始读取 5*20100 字节条目数据 for (i0;i5;i) 解析每个条目的偏移、生成号、状态步骤5解析 V5 流54321: 1 0 obj → ParseIndirectObjectAt 读取对象 /Type /XRef /W [1 4 1] ... stream ... 二进制数据 ... endstream代码通过CPDF_StreamAcc加载二进制数据然后按W和Index解析。步骤6填充对象信息// 最终得到 m_ObjectInfo 映射表:m_ObjectInfo[1]{pos16,gennum0,type1}m_ObjectInfo[2]{pos81,gennum0,type1}m_ObjectInfo[5]{pos1,gennum0,type2}// 压缩对象, 位于对象流1中m_ObjectInfo[1](流对象){pos100,gennum0,type255}七、总结图解析器内部数据结构PDF 文件 │ ├── 物理偏移 → CPDF_SyntaxParser (读取器) │ └── StartParse │ ├── 找到 startxref → m_LastXRefOffset │ ├── LoadAllCrossRefV4 / V5 │ │ │ ├── 读取 XRef 条目 → 填充 m_ObjectInfo (mapobjnum, Info) │ │ │ └── 记录所有偏移 → m_SortedOffset (set) │ └── SetEncryptHandler → 解密支持 │ └── m_pDocument-LoadDoc() 利用 m_ObjectInfo 快速定位对象核心思想PDF 解析器先建立“对象号 → 文件偏移”的索引然后就可以随机访问任何对象无需扫描整个文件。希望这种先图后码、图文对应的方式能帮助您彻底理解 PDF 交叉引用表的解析过程。如果还有某个细节需要更详细的图示请指出