PDF结构的清晰图示

文章目录

  • 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 加载二进制数据,然后按 WIndex 解析。

步骤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 交叉引用表的解析过程。如果还有某个细节需要更详细的图示,请指出!

相关推荐
MinterFusion2 小时前
Java后端高频术语表
java·开发语言·后端·程序员·大厂面试·术语
indexsunny2 小时前
互联网大厂Java面试实录:Spring Boot到微服务的深入探讨
java·spring boot·微服务·面试·eureka·kafka·jwt
@insist1232 小时前
网络工程师-因特网与网络互联(二):ARP 与 ICMP,网络层排错双雄
服务器·网络·网络协议·网络工程师·软考·软件水平考试
xin_yao_xin2 小时前
Linux下项目开机自启服务
linux·运维·服务器
鸽鸽程序猿2 小时前
【JavaEE】【SpringAI】Tool Calling(工具调用)
java·java-ee
怀君2 小时前
Uniapp——View布局生成图片转PDF
pdf·uni-app
于先生吖2 小时前
高并发稳定运营,JAVA 动漫短剧小程序 + H5 源码
java·开发语言·小程序
云和数据.ChenGuang2 小时前
鸿蒙应用对接DeepSeek大模型:构建智能问答系统的技术实践
java·华为·langchain·harmonyos·euler·openduler
曹牧2 小时前
在 Eclipse 中变更 SVN 地址
java·svn·eclipse