完全用 Linux 工作 之 给 pdf 电子书加目录

对许多人来说 pdf 格式的电子书最头疼的两件事 → 1) 每页都是没经过 OCR 处理过的图片 2) 没有目录。

以下这个批量加目录的方法我用好久了,见过我这么操作过的都想学一下,这里详细地记录以下,也方便以后有人再问的时候 :)

用到的软件是 pdftk pdftk-java / pdftk-java · GitLab 。linux 发行版一般都有这个这个软件可以直接安装。

pdftk 的用法就是:输出 (dump_data) pdf 的元信息 (data.txt),编辑以后,重新倒入 (update_info) 到 pdf 文件里面

主要是这两条命令:

pdftk [my.pdf] dump_data > [data.txt]
pdftk [my.pdf] update_info [data.txt] output my2.pdf

在第一条命令输出的 data.txt 里面加入如下的内容,然后通过第二条命令就可以创建新的目录条目

BookmarkBegin
BookmarkTitle: name
BookmarkLevel: level
BookmarkPageNumber: page number

另外电子书的第一页通常是封面,紧接着的是其它的东西。但是书里面标注的页码的第一页往往后面的某页。

PDF 支持把页码标注成其它的格式 (page_labels),第一页标注成 cover,第二到第十页标注成罗马数字,然后从第十一页标注成 1,2,3,4,5,6…

# 把第一页标注成名字为 cover 的非数字 (NoNumber)
PageLabelBegin
PageLabelNewIndex: 1
PageLabelStart: 1
PageLabelPrefix: cover
PageLabelNumStyle: NoNumber

# 从第二页 (PageLabelNewIndex) 开始标注成小写罗马数字 (LowercaseRomanNumerals)
PageLabelBegin
PageLabelNewIndex: 2
PageLabelStart: 1 # 从数字 1 开始数,如果这里变成 3  => 起始的罗马数字会是 iii
PageLabelNumStyle: LowercaseRomanNumerals

# 从 {true start page} 开始用普通的数字标注
PageLabelBegin
PageLabelNewIndex: {true start page}
PageLabelStart: 1
PageLabelNumStyle: DecimalArabicNumerals

对于一本书,这种手动添加的方法会很慢,下面是一个小脚本来半自动化。


由于电子书 100% 可以搜索到这种格式的目录 编号 标题 页码。如果搜索不到,也可以直接从书里面复制。


复制粘贴一下,调整成这种格式

14
I: Reduction Semantics	1
	1 Semantics via Syntax	5
	2 Analyzing Syntactic Semantics	13
	3 The λ-Calculus	23
	4 ISWIM	45
II: PLT Redex	201
	11 The Basics	205
	12 Variables and Meta-functions	217
	13 Layered Development	227
	14 Testing	237
......

第一行是对于人类,而非 pdf 格式来说真正的第一页

后面根据行首 tab 的数量来决定目录的层级

每行后面的数字是页码

然后用这个小脚本 toc-gen.py

#!/usr/bin/env python3

#
# Usage
# toc-gen.py  < edited-toc.txt
#

def make_offset(off: int):
    if off > 1:
        print("""PageLabelBegin
PageLabelNewIndex: 1
PageLabelStart: 1
PageLabelPrefix: cover
PageLabelNumStyle: NoNumber""")

    if off > 2:
        print("""PageLabelBegin
PageLabelNewIndex: 2
PageLabelStart: 1
PageLabelNumStyle: LowercaseRomanNumerals""")

    print(f"""PageLabelBegin
PageLabelNewIndex: {off}
PageLabelStart: 1
PageLabelNumStyle: DecimalArabicNumerals""")


def make_bookmark(t: str, l: int, p: int):
    print(f"""BookmarkBegin
BookmarkTitle: {t}
BookmarkLevel: {l}
BookmarkPageNumber: {p}""")


if __name__ == '__main__':
    offset = int(input())
    make_offset(offset)
    while True:
        try:
            line = input()
            if not line.strip():
                break
        except EOFError:
            break

        title = " ".join(line.split()[0:-1])
        n_of_tabs = len(line) - len(line.lstrip())
        page = int(line.split()[-1])

        make_bookmark(t=title,
                      l=n_of_tabs + 1,
                      p=page + offset)

来获取这些内容,把这些内容粘贴到 [data.txt] 后面,然后再用 pdftk 的第二条命令

PageLabelBegin
PageLabelNewIndex: 1
PageLabelStart: 1
PageLabelPrefix: cover
PageLabelNumStyle: NoNumber
PageLabelBegin
PageLabelNewIndex: 2
PageLabelStart: 1
PageLabelNumStyle: LowercaseRomanNumerals
PageLabelBegin
PageLabelNewIndex: 14
PageLabelStart: 1
PageLabelNumStyle: DecimalArabicNumerals
BookmarkBegin
BookmarkTitle: Reduction Semantics
BookmarkLevel: 1
BookmarkPageNumber: 15
BookmarkBegin
BookmarkTitle: Semantics via Syntax
BookmarkLevel: 2
BookmarkPageNumber: 19
BookmarkBegin
BookmarkTitle: Analyzing Syntactic Semantics
BookmarkLevel: 2
BookmarkPageNumber: 27
BookmarkBegin
BookmarkTitle: The λ-Calculus
BookmarkLevel: 2
BookmarkPageNumber: 37
BookmarkBegin
BookmarkTitle: ISWIM
BookmarkLevel: 2
BookmarkPageNumber: 59
BookmarkBegin
BookmarkTitle: An Abstract Syntax Machine
BookmarkLevel: 2
BookmarkPageNumber: 79
BookmarkBegin
BookmarkTitle: Abstract Register Machines
BookmarkLevel: 2
BookmarkPageNumber: 103
BookmarkBegin
BookmarkTitle: Tail Calls and More Space Savings
BookmarkLevel: 2
BookmarkPageNumber: 121
BookmarkBegin
BookmarkTitle: Control: Errors, Exceptions, and Continuations
BookmarkLevel: 2
BookmarkPageNumber: 129
BookmarkBegin
BookmarkTitle: State: Imperative Assignment
..............

Bingo! 这下舒服了 :)

Ref:

adjust logical & real pdf pages

pdftk’s guide on toc

SEO keywords: Ubuntu Debian Arch Linux pdftk pdf 目录

14赞

哇塞好厉害啊。。。。(流口水中):+1: :+1: :+1: :+1: :+1: :+1: :+1: :heart_eyes: :heart_eyes: :heart_eyes: :heart_eyes: :heart_eyes: :heart_eyes: :heart_eyes:

就算是商业的 pdf 编辑器,还需要用鼠标点击,一个一个地加目录。这里只需要调整以下能随手搜索到的目录,然后批量导入既可。

pdftk 原来是 perl 写的,后来就和所有 perl 写的软件一样没法维护了,然后不知道是不是原作者用 java 重写了一个,堪称人类高质量开源软件!

有人维护的同类软件还有 qpdf, pystitcher,

由于每个 pdf 文件情况不太一样,所以那个 py 小脚本需要的其它操作并没有自动化,而且代码很简单,不用太多编程的知识就可以改一改。

这个方法太棒了!
不过好像把 PageLabelBookmark 的文件进行 update_info 就行,我先全部信息 update 结果 okular 看全是白的。。。
update 完成后甚至文件缩小了 300 kb
现在就剩下更好的 ocr 了

1赞

确认了一下,的确只把改的内容放到 update_info 的输入里面就行了。

话说楼主这样生成的目录也是只能指向页开始的地方吗

试了一下 OCRmyPDF,用 -l chi_sim 选中文效果还可以,至少高亮没什么问题,模糊搜索给力点也能搜。

虽然和商业软件 (ABBYY) 比起来应该还差老远了。

不确定指向页是什么意思,我觉得你的意思是把对读者来说逻辑上第一页(扫描书打印出来的那个第一页),改成对应 pdf 文件真实的某页。

那个可以用 PageLabel 那块改。文中这段的意思是把” 第一页 “ 改成 PageLabelNumStyle:NoNumber 并起 cover 为名,然后把从第二到一个 offset (14) 的页改成i, ii, iii, iv, v, vi...( LowercaseRomanNumerals),然后 offset 后面的改成 DecimalArabicNumerals,也就是 1 2 3 。

这样点击一个书签,或者直接在选页码的地方输入数字就会跳转到对应的页。

PageLabelBegin
PageLabelNewIndex: 1
PageLabelStart: 1
PageLabelPrefix: cover
PageLabelNumStyle: NoNumber

PageLabelBegin
PageLabelNewIndex: 2
PageLabelStart: 1
PageLabelNumStyle: LowercaseRomanNumerals

PageLabelBegin
PageLabelNewIndex: 14
PageLabelStart: 1
PageLabelNumStyle: DecimalArabicNumerals

具体原理可以看文中引用的第一个链接 @GinShio

下来试一试

应该是这样。好像自己加的目录只能跳转到那一页。不能像 LaTeX 生成的 PDF 那样跳转到标题上。
不过也没关系了,这样目前来看也挺好。

1赞

研究了一下 pdftk 应该不可以。

不过受 superuser 上那个问题的第一个回答的启发,可以通过用文本编辑器直接编辑 pdf 来做到 !! WTF ?? :nerd_face: ??

原理在这个 Adobe 的 PDF 参考文档 https://ghostscript.com/~robin/pdf_reference17.pdf8.2 Document-Level Navigation,下面是操作

直接以文本的方式打开 pdf 文件,然后直接搜索书签的名字,比如 (Section 1)

219 0 obj 
<<
/Title (Section 1)
/Parent 218 0 R
/Last 227 0 R
/Next 228 0 R
/First 220 0 R
/Dest [3 0 R /XYZ 300.000 500.000 null]
/Count 8
>>
endobj

这段是 PDF 的一个 object 的定义,

219 是 PDF 内部 object 的编号,后面<< >>内的部分是数据及对应名称。
title 就是书签的标题,

中间的 parent last next first 是 object 或者书签之间的指向关系,暂时不重要。(pdf bookmarks 本质上是双向链表)

/Dest [3 0 R /XYZ 300.000 500.000 null] 前面的 [3 0 是另一个 object 的编号,用搜索也能搜到,不过 PDF 具体怎么表示页码的暂时不重要。

3 0 obj 
<<
/Annots [4 0 R 5 0 R 7 0 R 9 0 R 11 0 R 13 0 R 15 0 R 17 0 R]
/Resources 19 0 R
/Type /Page
/Contents 32 0 R
/Parent 1 0 R
/MediaBox [0 0 612 792]
>>

/Dest [3 0 R /XYZ 300.000 500.000 null] 后面 的 /XYZ 300.000 500.000 null 代表的是 destination 的点击书签后跳转的位置。

如果写 /Dest [3 0 R /Fit] 会单纯地跳转到那一页。

/XYZ 300.000 500.000 null 就是跳转到 (300,500) 这个坐标。第三的参数是修改放大的比例,这里用不到。

PDF 的坐标是 左上角为 (0,0),横着是 X,竖着的是 Y。

A4 纸的最大坐标是 (595.35, 841.995)。

PDFtk 导出的格式应该是 /Fit => /Dest [3 0 R /Fit] 就是直接跳转到那一页。改成 /Dest [3 0 R /XYZ 300.000 500.000 null] 就直接跳转到那页对应的坐标 (300.000, 500.000) 了(差不多中间的位置)。

这样就能跳转到具体的那个标题了,虽然要测量一下标题的坐标 :sunglasses:

我很久以前看过一点 PostScript 也就是 .ps 格式(PDF 前身)直接编程的内容 (因为 PS 是除了 forth 以外唯一能稍微有点用的 stack machine),所以直接就看明白了,不知道我解释清楚了没有。

我做尝试用的文件 cat-refs-refsalt.pdf (149.3 KB)

2赞

什么神仙楼主 hhhhh
可以可以,我再研究研究

或许有点帮助

1赞

Fish Shell 只需要一行


pdftk [input.pdf] update_info (cat [toc.txt] | toc-gen.py | psub) output [nice.pdf]
# psub -> "Process substitution" in bash.

新版 给 pdf 电子书加目录 (PyMuPDF)