• 使用 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 扩展,它可以支持我们以十六进制格式打开文件,这样,我们可以很容易查到每个对象的起始字节值。然后,你可以切换回到文件格式,将这些值填入预留的地方。

    参考资料:

  • 相关阅读:
    动态添加placeholder
    texworks编码问题
    c++ 小知识(不定期更新)
    写一个简单的 Makefile
    ubuntu 忘了密码
    遍历各种组合
    分形
    C++ 尾递归优化
    基于 QQBot 实现简易 QQ 机器人
    遇到问题:在函数中开辟动态内存(已解决)
  • 原文地址:https://www.cnblogs.com/haogj/p/15969054.html
Copyright © 2020-2023  润新知