冠军

导航

使用 VS Code 徒手构建 PDF 文件

使用 VS Code 徒手构建 PDF 文件

PDF 文件是广泛应用的页面描述文件格式,从本质上讲,文件内部的结构混合使用了文本格式描述和二进制格式描述,对于简单的文件,比如说我们今天要创建的第一个 PDF 文件来说,里面只包含一个 Hello, world 的字符串,其中主要的内容就可以使用文件来描述,只有很少的一部分需要特殊处理。借助于强大的的 VS Code,我们完全可以手工将这个简单的文件构建出来。

1. PDF 文件结构

从整体上讲,PDF 文件基本上可以分成 3 个顺序组成的部分:

  1. 头部
  2. 内容
  3. 尾部

1.1 头部

PDF 文件的头部从文件的字节 0 位置开始,至少包含 8 个字节,以及跟随的行结束符号。

PDF 文件的第一行为文件类型说明和该文件使用的 PDF 标准的版本号。开头的 5 个字符为:%PDF-。后面跟上版本号,目前一般使用 1.5 版本。这样第一行的内容就是一如下的文本:

%PDF-1.5

通常,PDF 文件内部不会只有文本内容,例如内部可能嵌入了图片,或者字体等等二进制内容。为了防止被误认为这是一个纯文本文件,那么文件的第二行就需要存在。第二行的第一个字符是 %,这是 PDF 中的注释符号,随后是至少 4 个字节的字符编码大于 127 的 ASCII 符号,尽管可以是符合的任意字符,通常使用的是二进制表示的 (0xE2,0xE3,0xCF,0xD3) 这 4 个字符,随后也有一个行结束符号。

1.2 尾部

PDF 文件的尾部使用 trailer 开始。直到最行一行的 %%EOF 最终结束。

trailer
<<
/Size 7
/Root 6 0 R
>>
startxref
478
%%EOF

这两行中间的内容又分为 2 个部分:

  • 尾部字典
  • 交叉索引表位置

1.2.1 尾部字典

在 PDF 内部,定义了用来描述各种数据结构的定义机制,为了方便,我们并不枯燥地介绍这些数据结构,而是按照我们遇到顺序来逐渐介绍它们。

这里我们遇到的第一个数据结构是字典。在 PDF 语法中,字典使用一对双尖括号包围起来。所以我们看到的从 << 到 >> 这一部分,实际上表示来一个字典结构。

在字典结构中,其构成元素是 key 与 value 对。在 PDF 定义中,每行表示一个 key/value 对,其中使用空格进行分隔,所以第一行中的 /Size 就是其中的一个 key,而数字 7 则是其值。第二行中的 /Root 则为其 key,而剩下的就是其值。

这里我们需要学到 2 种新的 PDF 数据类型:

  • Name 名称对象
  • 数值对象

在 PDF 定义中,名称对象表示一个唯一的名称,名称对象使用正斜线来引导,这就是 /Size 和 /Root 这两个名称前面的正斜线出现的原因。名称对象使用正斜线开始,后面是一个 UTF-8 的字符串。

在 PDF 中,已经预先定义了大量的名称对象,这里出现的 /Size 表示这个 PDF 文件中出现的对象数量。PDF 文件是使用对象树来描述的。/Root 表示其中的根对象是那个对象。这里的 6 0 R 表示根对象是在其它位置的一的 6 号对象。

1.2.2 交叉索引表的位置

上面已经提到了,PDF 文件是使用对象树来描述的,那么,这些对象在哪里可以找到呢?

交叉索引表就是该文件内部包含的所有 PDF 对象的索引,或者称为目录。所以,找到这些对象的第一步就是找到该索引表,这里的 478 表示可以从该文件的第 478 字节位置找到该交叉索引表。

1.3 内容部分

在头部和尾部之间的内容就是内容部分了。内容部分包含了 PDF 实际的详细定义。我们专门单列出来进行说明。

2. 内容部分

PDF 实际上是由一系列对象进行描述的,所以,在内容部分,就是一系列对象的定义。

作为入门,我们准备创建一个包含一个页面,页面上有一行 Hello, World 文本的 PDF 文件。我们就以此为例来说明涉及到的各种对象。

2.1 对象

在 PDF 内部,各种数据是通过对象来描述的。为了引用方便,PDF 内部使用唯一的数字编号来区分各个对象。描述形式如下:

1 0 obj

endobj

1 表示对象的编号,0 表示对象的版本号,一般不会使用,所以通常见到的就是 0 本身。最后的 obj 表示这是一个对象。这 3 个部分之间使用空格进行分隔。随后就是该对象的说明。

最后一行的 endobj 表示对象说明的结束,对象的说明我们随后就会看到。

2.2 字体对象

作为页面描述语言,我们不会不使用字体说明。字体描述也使用字典结构来描述。字典中的各个条目用来描述字体信息。

在 PDF 中支持 3 种类型的字体:

  • Type 1,这是 Adobe 原创的用于 Postscript 语言打印机的矢量字体格式。
  • TrueType,Windows 用户应该比较熟悉这种类型的字体,这是由 Apple 和 Microsoft 联合创建的矢量字体格式,
  • OpenType,尽管 Type1 和 TrueType 各有优势,却导致了字体标准的战争。OpenType 字体标准合并了上面两种,内部可能是 Type1 或者 TrueType。
  • Type 3,嵌入的点阵字体
  • Type 0,或者称为 CID 字体。对于像中文这样的字体来说,其中包含大量的字体,但是在 PDF 文件中却并不都会用到。可以从字库中抽取在该 PDF 文件中使用到的字体描述来构建一个字体的子集,以缩减文件的尺寸。

实际上,PDF 标准还定义了 14 种直接支持的字体,称为 Base14,它们可以直接在 PDF 文件中使用而不需要嵌入字体本身。

  • Times-Roman
  • Times-Bold
  • Times-Italic
  • Times-BoldItalic
  • Helvetica
  • Helvetica-Bold
  • Helvetica-Oblique
  • Helvetica-BoldOblique
  • Courier
  • Courier-Bold
  • Courier-Oblique
  • Courier-BoldOblique
  • Symbol
  • ZapfDingbats

下面就是字体描述的一个实例。

4 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont /Helvetica
>>
endobj

这个字体对象的编号是 4。它使用一个字典来进行描述,/Type 是预定义的名称,用来表示该对象的类型,这里是 /Font 字体类型的对象。
/Subtype 表示该字体的格式类型,值为 /Type1。
/BaseFont 表示使用的实际字体是 /Helvetica,从前面我们知道,这是一种预先定义,可以直接使用的字体。
/Name 表示重新命名该字体在 PDF 内部使用的名称,这里重新命名为 /F1。以后就可以使用 /F1 表示 /Helvetica 这种字体了。

2.3 资源对象

描述 PDF 中使用的资源,

3 0 obj
<<
/ProcSet [/PDF/Text]
/Font <</F1 4 0 R >>
>>
endobj

这里说明该资源是文本资源,资源中包含了前面定义的字体。

2.4 流对象

该介绍我们的文本 Hello, World 是如何描述了。

文本可以通过 stream 对象来描述。
流对象的开头是一个描述它的字典,/Length 表示该流对象的字节长度。

2 0 obj
<<
/Length 53
>>
stream
BT
/F1 24 Tf
1 0 0 1 260 600 Tm
(Hello World)Tj
ET
endstream
endobj

stream 和 endstream 表示流对象的开始与结束。
BT 的意思是:文本开始,即 Begin Text,当然 ET 就是文本结束,即 End Text。

其中的描述比较有意思,操作符在后面。
Tf 表示文本字体,/F1 24 Tf 表示使用 24 号的 /Helvetica 字体。
Tm 表示转换矩阵,1 0 0 1 260 600 Tm 表示将原点平移到 (260, 600)。
Tj 表示显示一个字符串,需要注意的是 PDF 中使用圆括号来表示字符串,而不是常见的双引号。

2.5 页面对象

万事俱备,我们可以创建一个页面了。
示例如下:

1 0 obj
<<
/Type /Page
/Parent 5 0 R
/MediaBox [ 0 0 612 792 ]
/Resources 3 0 R
/Contents 2 0 R
>>
endobj

页面也是使用字典来描述的,页面的类型是 /Type。
/Resources 表示页面使用的资源,该资源是前面的 3 号对象定义的。这里的 R 表示引用其它位置定义的对象。
/Contents 表示页面的内容,这里引用 2 号对象定义的字符串。
/MediaBox 比较重要,可以认为定义了页面的尺寸,
/Parent 表示该对象的父对象,我们马上就可以看到。

2.6 页面集

PDF 文件是多个页面所组成的,/Pages 对象表示页面的集合。

示例如下:

5 0 obj
<<
/Type /Pages
/Kids [ 1 0 R ]
/Count 1
>>
endobj

/Kids 表示其包含的子页面集合,数组是使用中括号来表示的。1 0 R 表示我们刚刚定义的 1 号页面对象。

2.7

目录对象的类型是 /Catalog。其中包含了页面集对象。

示例如下:

6 0 obj
<<
/Type /Catalog
/Pages 5 0 R
>>
endobj

这个 6 号对象就是我们在尾部看到的根对象。

2.8 交叉索引表

为了随机访问各个对象的便利,例如直接跳转到某个页面,我们需要一个包含每个对象位置的索引表,称为交叉索引表。

交叉索引表使用 xref 来开始。第二行由空格隔开的两个数字组成,第一个表示索引表中起始对象的编号,它总是 0,我们总是从编号 1 开始来进行定义,0 是特定的。第二个数字表示总共的对象数量,我们定义了 6 个对象,加上特殊的 0 号对象,合计为 7 个对象。

随后是相应数量的行,对象的编号从 0 开始递增。每行描述一个对象的起始字节数。虽然在定义对象的时候可以是无序的,但是,在交叉表中是按照编号依次排列的。

每行由 3 个部分组成:

  • 十进制的对象开始位置
  • 固定 00000
  • 对象是否被使用

其中第 1 行的内容是固定的。

示例如下:

xref
0 7
0000000000 65535 f
0000000072 00000 n
0000000257 00000 n
0000000416 00000 n
0000000459 00000 n
0000000357 00000 n
0000000023 00000 n

你需要检查每个对象以二进制字节计算的起始位置。

3 创建第一个 PDF 文件

将所有内容合在一起,就是一个 PDF 文件了。

%PDF-1.5
%����
6 0 obj
<<
/Type /Catalog
/Pages 5 0 R
>>
endobj
1 0 obj
<<
/Type /Page
/Parent 5 0 R
/MediaBox [ 0 0 612 792 ]
/Resources 3 0 R
/Contents 2 0 R
>>
endobj
4 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont /Helvetica
>>
endobj
2 0 obj
<<
/Length 53
>>
stream
BT
/F1 24 Tf
1 0 0 1 260 600 Tm
(Hello World)Tj
ET
endstream
endobj
5 0 obj
<<
/Type /Pages
/Kids [ 1 0 R ]
/Count 1
>>
endobj
3 0 obj
<<
/ProcSet [/PDF/Text]
/Font <</F1 4 0 R >>
>>
endobj
xref
0 7
0000000000 65535 f
0000000072 00000 n
0000000257 00000 n
0000000416 00000 n
0000000459 00000 n
0000000357 00000 n
0000000023 00000 n
trailer
<<
/Size 7
/Root 6 0 R
>>
startxref
478
%%EOF

需要注意的是交叉表中每个对象在 PDF 文件中的字节位置和交叉索引表本身在 PDF 文件中的字节位置。

你可以安装来自微软的 VS Code 扩展 HexEditor 扩展,它可以支持我们以十六进制格式打开文件,这样,我们可以很容易查到每个对象的起始字节值。然后,你可以切换回到文件格式,将这些值填入预留的地方。

参考资料:

posted on 2022-03-05 19:23  冠军  阅读(282)  评论(0编辑  收藏  举报