PDF 标准详解(七)——参数字典

PDF 标准详解(七)——参数字典

Masimaro
2026-07-19 / 0 评论 / 2 阅读 / 正在检测是否收录...

在前面的文章中,我们提到,PDF支持5种基本数据结构和三种复杂结构,简单数据结构分别是:数字、字符串、名称、bool值、null对象。而复杂类型有数组、字典和流;

在线型那一篇博文我们看到了数组的使用,它非常的简单,一般来说它的使用与其他编程语言中数组的使用并没有什么特别的地方。随着文档内容的复杂化,如果每出现一个显示的元素都要使用操作符详细的定义它的位置、颜色、填充等属性那么着实有点复杂了。对于这种重复型的操作,编程语言中一般使用函数或者类来进行封装,而PDF中这种重复性的定义则使用字典来封装。

我们在前面的一系列示例中看到过别的数据类型的使用。目前对于简单类型都已经很清楚它的使用了,但是对于复杂的类型我计划是拆解pdf标准的内容,在合适的时候带入pdf数据结构的内容。所以这一篇我们先从字典开始

语法

我们先来了解一下它的语法。

如果你熟悉一些编程语言(Python、C/C++、Java、Go等等),那么一定对字典的概念不会陌生。字典是由一组键值对组成,可以通过键快速的查到它对应的值。

PDF 的字典本质上也是一个无序的键值对容器。但在PDF中,它的语法有两个极其鲜明的“硬核”特征:

  • 尖括号护体:字典必须由成对的双左尖括号 << 开始,并由双右尖括号 >> 结束。
  • 斜杠开路:字典里的 “键(Key)” 必须是 PDF 中的 Name(名称) 类型。这意味着它不需要加任何引号,而是直接以斜杠 / 开头。而 “值(Value)” 则可以是 PDF 支持的任意数据类型。

下面是一个具体的例子:

<< /Type /XObject
   /Subtype /Image
   /Width 640
   /Height 480
>>

上面的这个字典是由 一对 "<<" ">>" 包裹起来的。它有四个键值对。

重要的 /Type

PDF的字典和编程语言中的字典有点不一样。编程语言中的字典纯粹是为了快速的进行查询,它可以形容任何属性,而编译器也不关心这个字典的作用。但是PDF中不太一样,有时候PDF渲染器会读取字典来进行一些渲染工作,那么它就需要知道这个字典是用来干什么的。我们可以通过 /Type 来指明这个字典的作用,例如前面的例子中:

1 0 obj % 对象1
<< /Type /Pages     % 这是一个页面列表
   /Count 1         % 只有一页
   /Kids [2 0 R]    % 页面对象编号列表。这里只是对象2
>>
endobj  % 对象1结束

表示这是一个页面列表的字典。渲染器读到这里的时候知道这个文档有一个页面。页面的内容对应着对象2。此时渲染器再去读取对象2,发现它又是一个字典

2 0 obj
<< /Type /Page              % 这是一个页面
   /MediaBox [0 0 612 792]  % 纸张尺寸为美国信肖像(612点x792点)
   /Contents [3 0 R]        % 图形内容在对象4中
>>
endobj

在对象2 这个字典中,我们定义了它的/Type/Page 表示它是一个页面对象。它的内容在对象3中定义。读到对象3时发现它又是一个字典:

3 0 obj     % 页面内容流
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
20 w
[1 2 5 7 8 1 4 5 12 4 5] 0 d
100 100 m 400 100 l S
endstream   % 流结束
endobj

3终于不是字典了。它是一个流对象(又出现新的概念了)。

示例

上一节中我们写了一个复杂的示例:

0.5 0.5 0.5 RG
20 w
[1 2 5 7 8 1 4 5 12 4 5] 0 d
100 100 m 400 100 l S

在这个里面我们定义了线宽、虚线类型,然后进行了划线。在这个简单的示例中这么做完全没问题,但是对于一个复杂的PDF文档来说,页面中的内容可能不止一个流对象,而且也不止一个页面。如果我们将它作为条形码在每一页的关键位置使用,我们每一次都要写这么一个东西,而且如果某一天条形码变了,每个地方都要重写虚线的数组,那种工作量你碰到了可能会当场辞职。但是别担心,人类进步的一个重大原因是人类喜欢偷懒。对于这种重复无聊的工作有字典这个东西帮我们偷懒。

这里我们需要用到 扩展图形状态字典 这么一个东西。 对于上面一个关于虚线的封装,我们可以定义下面这样一个字典:

6 0 obj
<< /LW 20
   /D [ [1 2 5 7 8 1 4 5 12 4 5] 0] % 封装虚线模式、等效于 [1 2 5 7 8 1 4 5 12 4 5] 0 d
>>

那么我们怎么用它呢?在使用之前需要先在页面对象的资源字典中进行注册。我们修改一下2这个对象

2 0 obj
<< /Type /Page              % 这是一个页面
   /MediaBox [0 0 612 792]  % 纸张尺寸为美国信肖像(612点x792点)
   /Contents [3 0 R]        % 图形内容在对象4中
   /Resources <<
       /ExtGState <<        %注册图形状态字典
           /MyDashStyle 6 0 R % 绑定到对象6
   >>
>>
endobj

接着我们就可以在内容流中使用它

3 0 obj     % 页面内容流
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
/MyDashStyle gs
100 100 m 400 100 l S
endstream   % 流结束
endobj

需要注意一点,我们需要在使用图形状态字典的后面加上 gs 操作符,它表示 Graphics State,调用这个资源字典。你可以将它理解为调用函数的call指令

消失的 /Type

我们说 /Type 是很重要的内容,它可以告知渲染器这个字典是什么类型的字典。但是我们定义的字典似乎没有Type,它能正确的渲染,这也就说明 /Type 并不是必须的,PDF渲染器可以直接的(从Type中读取)或者间接的(自行推导)得知类型并根据类型进行渲染。我们可以尝试一下将之前例子中所有 /Type 都去掉,我们发现PDF渲染器仍然能正常工作。

我们可以结合之前所说的PDF的渲染过程来理解。

  • 首先渲染器会读取交叉引用表的内容。交叉引用表中 /Root 对应着4这个对象,此时渲染器知道了,4对象的Type为 Catalog
  • 4 对象 /Pages 键对应着1这个对象,渲染利用这个信息可以推导出 1 对象是一个 pages 类型
  • 1 对象中 /Kids 中有一个引用的子对象,就是2对象,所以2对象就是一个 /Page
  • 2 对象中,注册了一个 资源字典 6 对象,那么6对象肯定是资源字典了。

也就是说PDF对象可以根据上下文来推导出具体的类型,我们实际上并不需要每个都指定。而且推导的类型的优先级是要高于手工指定的,读者可以自行做一个实验,如果我们将6对象添加一个键值对 /Type /Pages 给出一个错误的类型,渲染器也不会报错,仍然可以正常工作。

自定义自己的键

上面我们看到定义字典中的键都是PDF内置的键,比如用来定义线宽的 /LW ,定义虚线的 /D。这里是不是说键一定是PDF内置的呢?

其实不然,我们当然可以定义自己的键。例如,读者可以自己对上面的资源字典做一些修改。加入一些自定义的键。PDF的标准中并没有规定字典的键。但是在PDF渲染器进行渲染时它会忽略不认识的键,只读取它认识的。一个是为了方便后面对标准进行扩展,另一个也是方便各大PDF渲染器的厂商能定义一些自己的扩展。

字典的查找顺序

这里我们将字典注册到了 Page 对象中。是不是意味着只能放到Page中进行注册呢?答案是否定的。实际上我们可以在解析过程中的任意位置进行注册。也就是对于多页公用的样式,我们可以注册到 /Pages 这个共同的父节点中

%PDF-1.0    % PDF 版本号为 1.0 的文件头

1 0 obj % 对象1
<< /Type /Pages     % 这是一个页面列表
   /Count 2         % 只有2页
   /Kids [2 0 R 8 0 R]    % 页面对象编号列表。
   /Resources <<
       /ExtGState <<        %注册图形状态字典
           /MyDashStyle 7 0 R % 绑定到对象7
   >>
>>
endobj  % 对象1结束

2 0 obj
<< /Type /Page              % 这是一个页面
   /MediaBox [0 0 612 792]  % 纸张尺寸为美国信肖像(612点x792点)
   /Contents [3 0 R]        % 图形内容在对象4中
   /Resources <<
       /ExtGState <<        %注册图形状态字典
           /MyDashStyle 6 0 R % 绑定到对象6
   >>
>>
endobj

3 0 obj     % 页面内容流
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
/MyDashStyle gs
100 100 m 400 100 l S
endstream   % 流结束
endobj

4 0 obj
<< /Type /Catalog %文件目录
   /Pages 1 0 R   %参考页面列表
>>
endobj

6 0 obj
<< /Type /ExtGState
   /LW 20
   /D [ [1 2 5 7 8 1 4 5 12 4 5] 0] % 封装虚线模式、等效于 [1 2 5 7 8 1 4 5 12 4 5] 0 d
>>

7 0 obj
<< /Type /ExtGState
   /LW 20
   /LC 1
>>

8 0 obj
<< /Type /Page              % 这是一个页面
   /MediaBox [0 0 612 792]  % 纸张尺寸为美国信肖像(612点x792点)
   /Contents [9 0 R]        % 图形内容在对象9中
>>
endobj

9 0 obj     % 页面内容流
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
/MyDashStyle gs
100 100 m 400 100 l S
endstream   % 流结束
endobj

xref %这里我们跳过了交叉引用表的开始
0 5
trailer
<< /Size 5 %交叉引用表的行数
   /Root 4 0 R % 参考文档目录
>>

startxref
0            %xref表开始的字节偏移量,这里设置成0
%%EOF

这里我们在第一页的 /Page 对象和对应的父对象 /Pages 中都注册了一个 /MyDashStyle 的资源数组。一个是虚线一个是实线。我们可以看到第一页显示为虚线,第二页显示为实线。也就是在第一页中发生的名称覆盖的现象。如果有一点编程语言的知识就很容易理解这个概念,它就相当于函数内部局部变量对全局变量的覆盖。

总结

最后我们补充一下关于图形状态字典的一些键,它们的作用与我们之前介绍的线型的操作符一一对应,作用也是一样的

操作符对应的键值
wLW
JLC
jLJ
dD
0

评论 (0)

取消