CAJ 格式的页面内容 - HinTak/caj2pdf GitHub Wiki

文件结构

CAJ 格式的页面数据以 PDF body 的形式存储。其起始地址 addr 按如下步骤取得:

  1. 读偏移量 0x14 处的 int 值 addr_ptr
  2. 读偏移量 addr_ptr 处的 int 值 addr

通常,紧接 PDF body 为一段 XML 数据,通过寻找 XML 文件头即可确定 PDF body 的结束地址,但部分文件(样本)的尾部是一段错误信息。为增强兼容性,寻找文件中最后一个 PDF Object 结束标识 endobj 为 PDF body 的结束地址。

caj2pdf 的实现细节

通过以下步骤把 PDF body 还原为完整的 PDF 文件:

  1. 添加文件头
  2. 添加 Catalog 字典(即 PDF 根节点)
  3. 添加根 Pages 字典(即所有页面/页面树的父节点)
  4. 添加文件结束标记
  5. 调用 mutool 生成交叉引用表(xref)和文件尾(trailer)
  6. 为得到的 PDF 文件添加大纲项目

下面详细说明。

添加文件头

在头部添加 PDF 文件头:%PDF-1.3\r\n (25 50 44 46 2D 31 2E 33 0D 0A)

添加 Catalog 字典

首先寻找根 Pages 字典的对象编号 root_pages_id:在 PDF Body 中查找所有 /Parent 字段,找到其值当中的最小值,即为根 Pages 字典的对象编号。

然后添加 PDF object:

catalog_obj_no 0 obj
<<
/Type /Catalog
/Pages root_pages_id 0 R
>>
endobj

其中 catalog_obj_no 为 Catalog 字典对象的编号。为防止与已有的对象编号冲突,目前采取的办法是遍历 1~9999,寻找第一个未被使用的编号。

添加根 Pages 字典

尽管可以找到根 Pages 字典的对象编号,但在相当一部分文件中该对象并不存在,因此首先判断是否存在该对象,如果不,则按下述步骤手动添加。

首先找到根 Pages 字典对象的所有子对象 kids[]:在 PDF Body 中查找所有 /Parent 字段值为根 Pages 字典的对象,将该对象的编号依次添加到 kids[]

然后添加 PDF object:

root_pages_id 0 obj
<<
/Type /Pages
/Kids [kids[0] 0 R kids[1] 0 R ...]
/Count page_count
>>
endobj

其中 page_count 为 CAJ 文件的页面数。

添加文件结束标记

在尾部添加一行 PDF 文件结束标记:\r\n%%EOF\r (0D 0A 25 25 45 4F 46 0D)

生成交叉引用表和文件尾

将经过上述步骤的数据存为临时文件 pdf.tmp,调用 mutool 即可修复缺失的交叉引用表和文件尾,得到可以打开的 PDF 文件:

mutool clean pdf.tmp output.pdf

添加大纲项目

根据大纲项目一节中得到的信息,即可构造出完整的树状大纲结构,然后用 PyPDF2 添加 Outlines 字典对象和每个 outline item 字典对象(参考 PDF Reference, version 1.7 第 8.2.2 节 Document Outline),并在 Catalog 对象中添加相应的 /Outlines 字段。

具体实现上,采用二叉树(左孩右兄)代表大纲项目的层级树状结构,从而方便地找到父子兄弟节点并构建字典对象,见 utils.add_outlines 方法。

新的实现

dev 分支中,parser 采用了与上述不同的、由 @Maybeka 提出并实现的新的算法,详情请见 PR #3 的讨论。