文章目录
-
- [模拟解析:宽度数组 `[1,2,1]`,10个条目的 XRef 流](#模拟解析:宽度数组
[1,2,1],10个条目的 XRef 流) -
- 一、设定场景
- 二、解析代码核心部分(与之前相同)
- [三、逐条解析(模拟 CPU 执行)](#三、逐条解析(模拟 CPU 执行))
-
- [辅助函数 `GetVarInt` 回顾](#辅助函数
GetVarInt回顾) - 条目0 (j=0, objnum=0)
- 条目1 (j=1, objnum=1)
- 条目2 (j=2, objnum=2)
- 条目3 (j=3, objnum=3)
- 条目4 (j=4, objnum=4)
- 条目5 (j=5, objnum=5)
- 条目6 (j=6, objnum=6)
- 条目7 (j=7, objnum=7)
- 条目8 (j=8, objnum=8)
- 条目9 (j=9, objnum=9)
- [辅助函数 `GetVarInt` 回顾](#辅助函数
- [四、最终 `m_ObjectInfo` 映射表](#四、最终
m_ObjectInfo映射表) - 五、代码中指针偏移计算的通用公式
- 六、扩展到其他宽度数组的示例
- 七、总结
- [模拟解析:宽度数组 `[1,2,1]`,10个条目的 XRef 流](#模拟解析:宽度数组
模拟解析:宽度数组 [1,2,1],10个条目的 XRef 流
一、设定场景
宽度数组 :/W [1 2 1]
- 类型字段宽度 = 1 字节
- 字段2宽度 = 2 字节(适合偏移量 < 65536 的小文件)
- 生成号宽度 = 1 字节
- 每个条目总字节数 = 1 + 2 + 1 = 4 字节
Index 数组 :/Index [0 10](对象 0~9 都在流中)
流数据(十六进制):共 10 个条目 × 4 字节 = 40 字节
条目0: 01 00 10 00 → 类型1, 偏移=0x0010=16, 生成0
条目1: 01 00 20 00 → 类型1, 偏移=32, 生成0
条目2: 02 00 01 00 → 类型2, 对象流编号=1, 生成0
条目3: 01 00 30 01 → 类型1, 偏移=48, 生成1
条目4: 00 00 00 00 → 类型0 (空闲)
条目5: 01 00 40 00 → 类型1, 偏移=64, 生成0
条目6: 02 00 02 00 → 类型2, 对象流编号=2, 生成0
条目7: 01 00 50 00 → 类型1, 偏移=80, 生成0
条目8: 01 00 60 00 → 类型1, 偏移=96, 生成0
条目9: 00 00 00 00 → 类型0 (空闲)
流数据内存视图(地址 0~39):
地址: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
数据: 01 00 10 00 01 00 20 00 02 00 01 00 01 00 30 01 00 00 00 00
地址: 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
数据: 01 00 40 00 02 00 02 00 01 00 50 00 01 00 60 00 00 00 00 00
二、解析代码核心部分(与之前相同)
cpp
std::vector<uint32_t> WidthArray = {1, 2, 1};
uint32_t totalWidth = 4; // 1+2+1
const uint8_t* pData = /* 指向流数据起始地址0 */;
uint32_t segindex = 0;
int startnum = 0;
uint32_t count = 10;
const uint8_t* segstart = pData + segindex * totalWidth; // segstart = pData
for (uint32_t j = 0; j < count; ++j) {
const uint8_t* entrystart = segstart + j * totalWidth; // 每个条目的起始地址
// 读取类型
int32_t type = 1;
if (WidthArray[0] > 0) // 1 > 0
type = GetVarInt(entrystart, WidthArray[0]);
// 读取字段2
FX_FILESIZE field2 = 0;
if (WidthArray[1] > 0) // 2 > 0
field2 = GetVarInt(entrystart + WidthArray[0], WidthArray[1]);
// 读取生成号
int32_t gen = 0;
if (WidthArray[2] > 0) // 1 > 0
gen = GetVarInt(entrystart + WidthArray[0] + WidthArray[1], WidthArray[2]);
uint32_t objnum = startnum + j;
// 存储
if (type == 0) {
m_ObjectInfo[objnum].pos = 0;
m_ObjectInfo[objnum].type = 0;
} else {
m_ObjectInfo[objnum].pos = field2;
m_ObjectInfo[objnum].gennum = gen;
if (type == 1) {
m_ObjectInfo[objnum].type = 1;
m_SortedOffset.insert(field2);
} else if (type == 2) {
m_ObjectInfo[objnum].type = 2;
m_ObjectInfo[field2].type = 255; // 标记对象流
}
}
}
三、逐条解析(模拟 CPU 执行)
辅助函数 GetVarInt 回顾
cpp
uint32_t GetVarInt(const uint8_t* p, int n) {
uint32_t r = 0;
for (int i = 0; i < n; ++i) r = r * 256 + p[i];
return r;
}
条目0 (j=0, objnum=0)
entrystart = pData + 0*4 = 地址0- 读类型:
GetVarInt(地址0, 1)→ 地址0=0x01 → type=1 - 读字段2:
GetVarInt(地址0+1, 2)→ 地址1=0x00, 地址2=0x10 → (0*256+0)*256+0x10 = 0x0010 = 16 - 读生成号:
GetVarInt(地址0+1+2, 1)→ 地址3=0x00 → gen=0 - type=1 →
m_ObjectInfo[0] = {pos=16, gennum=0, type=1},m_SortedOffset插入 16
条目1 (j=1, objnum=1)
entrystart = 地址4- 读类型:地址4=0x01 → type=1
- 读字段2:地址5=0x00, 地址6=0x20 → 0x0020 = 32
- 读生成号:地址7=0x00 → gen=0
m_ObjectInfo[1] = {pos=32, gennum=0, type=1},m_SortedOffset插入 32
条目2 (j=2, objnum=2)
entrystart = 地址8- 读类型:地址8=0x02 → type=2
- 读字段2:地址9=0x00, 地址10=0x01 → 1
- 读生成号:地址11=0x00 → gen=0
- type=2 →
m_ObjectInfo[2] = {pos=1, gennum=0, type=2},且m_ObjectInfo[1].type = 255(注意:对象流编号为1,即对象1被标记为对象流容器)
重要 :这里 field2=1 表示当前对象(对象2)是一个压缩对象,存储在对象流 #1 中。因此我们需要把对象流 #1 的类型标记为 255,以便将来从该流中解压对象。
条目3 (j=3, objnum=3)
entrystart = 地址12- 读类型:地址12=0x01 → type=1
- 读字段2:地址13=0x00, 地址14=0x30 → 0x0030 = 48
- 读生成号:地址15=0x01 → gen=1
m_ObjectInfo[3] = {pos=48, gennum=1, type=1},m_SortedOffset插入 48
条目4 (j=4, objnum=4)
entrystart = 地址16- 读类型:地址16=0x00 → type=0
- 读字段2:地址17=0x00, 地址18=0x00 → 0
- 读生成号:地址19=0x00 → 0
- type=0 →
m_ObjectInfo[4] = {pos=0, type=0}
条目5 (j=5, objnum=5)
entrystart = 地址20- 读类型:地址20=0x01 → type=1
- 读字段2:地址21=0x00, 地址22=0x40 → 0x0040 = 64
- 读生成号:地址23=0x00 → gen=0
m_ObjectInfo[5] = {pos=64, gennum=0, type=1},m_SortedOffset插入 64
条目6 (j=6, objnum=6)
entrystart = 地址24- 读类型:地址24=0x02 → type=2
- 读字段2:地址25=0x00, 地址26=0x02 → 2
- 读生成号:地址27=0x00 → gen=0
m_ObjectInfo[6] = {pos=2, gennum=0, type=2},且m_ObjectInfo[2].type = 255(对象流 #2 被标记)
条目7 (j=7, objnum=7)
entrystart = 地址28- 读类型:地址28=0x01 → type=1
- 读字段2:地址29=0x00, 地址30=0x50 → 0x0050 = 80
- 读生成号:地址31=0x00 → gen=0
m_ObjectInfo[7] = {pos=80, gennum=0, type=1},m_SortedOffset插入 80
条目8 (j=8, objnum=8)
entrystart = 地址32- 读类型:地址32=0x01 → type=1
- 读字段2:地址33=0x00, 地址34=0x60 → 0x0060 = 96
- 读生成号:地址35=0x00 → gen=0
m_ObjectInfo[8] = {pos=96, gennum=0, type=1},m_SortedOffset插入 96
条目9 (j=9, objnum=9)
entrystart = 地址36- 读类型:地址36=0x00 → type=0
- 读字段2:地址37=0x00, 地址38=0x00 → 0
- 读生成号:地址39=0x00 → 0
m_ObjectInfo[9] = {pos=0, type=0}
四、最终 m_ObjectInfo 映射表
| 对象号 | pos | gennum | type | 含义 |
|---|---|---|---|---|
| 0 | 16 | 0 | 1 | 未压缩对象,位于文件偏移16 |
| 1 | 32 | 0 | 1 | 未压缩对象,偏移32 |
| 2 | 1 | 0 | 2 | 压缩对象,存储在对象流#1中 |
| 3 | 48 | 1 | 1 | 未压缩对象,偏移48,生成号1 |
| 4 | 0 | 0 | 0 | 空闲对象 |
| 5 | 64 | 0 | 1 | 未压缩对象,偏移64 |
| 6 | 2 | 0 | 2 | 压缩对象,存储在对象流#2中 |
| 7 | 80 | 0 | 1 | 未压缩对象,偏移80 |
| 8 | 96 | 0 | 1 | 未压缩对象,偏移96 |
| 9 | 0 | 0 | 0 | 空闲对象 |
另外,对象流容器标记:
m_ObjectInfo[1].type被设置为 255(因为条目2中 field2=1)m_ObjectInfo[2].type被设置为 255(因为条目6中 field2=2)
注意:对象1原本是未压缩对象(type=1),现在又被标记为255,这在真实PDF中不会发生。这里只是展示解析过程,实际中对象1和对象2作为对象流,它们自己的XRef条目应该是类型1(指向文件中的流对象),而不是同时作为压缩对象。为了避免混淆,我们应该把对象流编号设为一个不与其他普通对象冲突的值,比如100和101。但为了保持示例简单,请理解这是演示目的。
五、代码中指针偏移计算的通用公式
对于任意条目 j:
- 条目起始地址 =
pData + j * totalWidth - 类型字段起始 = 条目起始地址
- 字段2起始 = 条目起始地址 +
WidthArray[0] - 生成号起始 = 条目起始地址 +
WidthArray[0] + WidthArray[1]
无论宽度数组是什么,这个公式都适用。因为 totalWidth 是预计算的总宽度,而每个字段的偏移是通过累加前面字段的宽度得到的。
为什么这样设计?
- 如果硬编码偏移(如类型在0,字段2在1,生成号在5),那么当宽度变化时代码就失效了。
- 通过动态累加,代码可以适应任何
/W组合,实现完全通用。
六、扩展到其他宽度数组的示例
假设 /W [0 4 0](只有4字节偏移,类型默认1,生成号默认0):
WidthArray[0]=0, WidthArray[1]=4, WidthArray[2]=0totalWidth = 4- 解析时:
type保持默认1field2 = GetVarInt(entrystart + 0, 4)读4字节gen保持默认0
- 这样每个条目只存4字节偏移,非常紧凑。
假设 /W [1 8 2](大文件):
totalWidth = 11- 解析时:
type读1字节field2读8字节(可表示最大 2^64-1 偏移)gen读2字节
- 支持超大文件(64位偏移)和更大生成号。
七、总结
通过这个包含10个条目的完整示例,我们演示了:
- 动态读取宽度数组 → 确定每个字段的字节数
- 计算 totalWidth → 实现 O(1) 跳转到任意条目
- 使用
GetVarInt读取大端整数 → 跨平台、任意宽度 - 根据类型分支处理 → 区分未压缩对象、压缩对象、空闲对象
- 标记对象流容器 → 为后续解压做准备
整个解析过程完全由 /W 数组驱动,不需要修改代码就能处理任何宽度组合。这正是 PDFium 设计的通用性和健壮性所在。