# add_pdf_bookmarker **Repository Path**: iam002/add_pdf_bookmarker ## Basic Information - **Project Name**: add_pdf_bookmarker - **Description**: 使用 pypdf2 给 pdf 添加目录书签 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 1 - **Created**: 2022-02-02 - **Last Updated**: 2024-07-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: Python ## README - [1. 使用 python 给 PDF 添加书签](#1-使用-python-给-pdf-添加书签) - [1.1. 安装 `PyPDF2`](#11-安装-pypdf2) - [1.2. 提取 PDF 的目录信息并保存在 txt](#12-提取-pdf-的目录信息并保存在-txt) - [1.3. 编程实现](#13-编程实现) - [1.4. 可能遇到的错误](#14-可能遇到的错误) - [1.4.1. 问题一:ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list](#141-问题一valueerror-type-outlines-count-0-is-not-in-list) - [1.4.2. 问题二:RuntimeError: generator raised StopIteration](#142-问题二runtimeerror-generator-raised-stopiteration) - [1.5. 代码下载](#15-代码下载) - [1.6. 参考](#16-参考) --- # 1. 使用 python 给 PDF 添加书签 有时下载到扫描版的 PDF 是不带书签目录的,这样阅读起来很不方便。下面通过 python 实现一个半自动化添加书签目录的脚本。 ## 1.1. 安装 `PyPDF2` ```shell pip install pypdf2 ``` 未避免后续运行程序报错,`python` 版本必须是 3.7 之前的(3.6)。 ## 1.2. 提取 PDF 的目录信息并保存在 txt 这一步是比较麻烦的,需要手动实现。一般可以使用一些 OCR 文字识别工具,或者将目录页转化为 word 来操作。然后整理为如下的 txt 格式: * 每一行包含三项:级别 `level`、 标题 `title`、 页数 `page`,用空格隔开 * 使用“`.`”来判断书签的级别,例如: * “第1章” 包含 0 个 “`.`” 是一级标题 * “1.1” 包含 1 个 “`.`” 是二级标题 * “1.1.1” 包含 2 个 “`.`” 是三级标题 * ……(以此类推) * 请不要有多余的空行 这里是我整理后的 txt: ```txt 第1章 绪论 1 1.1 本书的目的 1 1.2 信息融合的主要挑战 5 1.3 为什么需要随机集或FISST 5 1.3.1 多目标滤波的复杂性 6 1.3.2 超越启发式 7 1.3.3 单目标与多目标统计学的区别 7 1.3.4 常规数据与模糊数据的区别 7 1.3.5 形式化贝叶斯建模 8 1.3.6 模糊信息建模 8 1.3.7 多源多目标形式化建模 9 ``` ## 1.3. 编程实现 ```python import PyPDF2 import sys class PdfDirGenerator: def __init__(self, pdf_path:str, txt_path:str, offset:int, out_path:str=None, levelmark:str='.'): self.pdf_path = pdf_path # pdf路径 self.txt_path = txt_path # 包含pdf目录信息的txt self.offset = offset # 目录页数偏移量 self.out_path = out_path # 输出路径 self.levelmark = levelmark # 用于判断书签级别的标志符 self.dir_parent = [None] def getLevelId(self, level): """计算书签的级数(级数的标志符号为“.”) 一级目录: 0 个“.”,例如: 第1章、附录A等 二级目录: 1个“.”,例如: 1.1、A.1 三级目录: 2个“.”,例如: 2.1.3 """ mark_num = 0 for c in level: if c == self.levelmark: mark_num += 1 return mark_num + 1 def run(self): print("--------------------------- Adding the bookmark ---------------------------") print(" * PDF Source: %s" % self.pdf_path) print(" * TXT Source: %s" % self.txt_path) print(" * Offset: %d" % self.offset) print("---------------------------------------------------------------------------") with open(self.txt_path, 'r', encoding='utf-8') as txt: pdf_reader = PyPDF2.PdfFileReader(self.pdf_path) pdf_writer = PyPDF2.PdfFileWriter() pdf_writer.cloneDocumentFromReader(pdf_reader) # BUG: ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list # 修改代码 ${PYTHON_PATH}/site-packages/PyPDF2/pdf.py): getOutlineRoot 函数 # 参考:https://www.codetd.com/en/article/11823498 lines = txt.readlines() num_all_lines = len(lines) for i, line in enumerate(lines): # pline = line.split(' ') # 要求level title page之间只能有一个空格且不能是\t, 比较麻烦换成下面这个 pline = line.split(None, maxsplit=-1) # python 3.6.13, 支持删除\t\n 空格, 且不限次数 level = pline[0]; title = pline[1]; page = int(pline[2]) + self.offset # 1. 计算当前的 level 的级数 id # 2. 当前书签的父结点存放在 dir_parent[id-1] 上 # 3. 更新/插入 dir_parent[id] id = self.getLevelId(level) if id >= len(self.dir_parent): self.dir_parent.append(None) self.dir_parent[id] = pdf_writer.addBookmark(level+' '+title, page-1, self.dir_parent[id-1]) print(" * [%d/%d finished] level: %s(%d), title: %s, page: %d" % (i+1, num_all_lines, level, id, title, page)) if self.out_path is None: self.out_path = self.pdf_path[:-4] + '(书签).pdf' with open(self.out_path, 'wb') as out_pdf: pdf_writer.write(out_pdf) print("---------------------------------------------------------------------------") print(" * Save: %s" % self.out_path) print("---------------------------------- Done! ----------------------------------") if __name__ == '__main__': input_num = len(sys.argv) assert(input_num > 3) opath = None if input_num > 4: opath = sys.argv[4] mark='.' if input_num > 5: mark = sys.argv[5] pdg = PdfDirGenerator( pdf_path=sys.argv[1], txt_path=sys.argv[2], offset=int(sys.argv[3]), # 一般是目录结束页的页数 out_path=opath, levelmark=mark ) pdg.run() ``` 上述代码保存在 `PdfDirGenerator.py`中,其中有3个参数和2个可选参数: - 第1个参数:待插入书签的 PDF 的路径 - 第2个参数:包含目录信息的 txt 的路径 - 第3个参数:正文内容的偏移页数(一般填目录结束页的页数) - 第4个参数(可选):输出路径 - 第5个参数(可选):级数标志,默认为“.” 例如,在命令行中输入: ```shell python .\PdfDirGenerator.py .\多源多目标统计信息融合Mahler.pdf .\dir.txt 27 ``` 运行效果: ![res](res.gif) ## 1.4. 可能遇到的错误 这里主要参考 [https://www.codetd.com/en/article/11823498](https://www.codetd.com/en/article/11823498) ### 1.4.1. 问题一:ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list 如果 PDF 之前被其他软件修改过(删除之前的目录),可能会有如下错误: ```shell Traceback (most recent call last): File ".\PDFbookmark.py", line 70, in print(addBookmark(args[1], args[2], int(args[3]))) File ".\PDFbookmark.py", line 55, in addBookmark new_bookmark = writer.addBookmark(title, page + page_offset, parent=parent) File "C:\Anaconda3\lib\site-packages\PyPDF2\pdf.py", line 732, in addBookmark outlineRef = self.getOutlineRoot() File "C:\Anaconda3\lib\site-packages\PyPDF2\pdf.py", line 607, in getOutlineRoot idnum = self._objects.index(outline) + 1 ValueError: { '/Type': '/Outlines', '/Count': 0} is not in list ``` 解决方法:修改 `pdf.py` 的 `getOutlineRoot()` 函数(`pdf.py` 的路径为 [${PYTHON_PATH}/site-packages/PyPDF2/pdf.py]()) ```python def getOutlineRoot(self): if '/Outlines' in self._root_object: outline = self._root_object['/Outlines'] try: idnum = self._objects.index(outline) + 1 except ValueError: if not isinstance(outline, TreeObject): def _walk(node): node.__class__ = TreeObject for child in node.children(): _walk(child) _walk(outline) outlineRef = self._addObject(outline) self._addObject(outlineRef.getObject()) self._root_object[NameObject('/Outlines')] = outlineRef idnum = self._objects.index(outline) + 1 outlineRef = IndirectObject(idnum, 0, self) assert outlineRef.getObject() == outline else: outline = TreeObject() outline.update({ }) outlineRef = self._addObject(outline) self._root_object[NameObject('/Outlines')] = outlineRef return outline ``` --- > 更新于 2022年9月13日 如果 `PyPDF2` 的版本是 2.8.0 / 2.11.0 的,报错信息有点不同,如下所示。可见,我们需要修改的是 `_writer.py` 的 `get_outline_roor()` 函数。 ```bash File "C:\Users\q2799\.conda\envs\py36\lib\site-packages\PyPDF2\_writer.py", line 1195, in addBookmark title, pagenum, parent, color, bold, italic, fit, *args File "C:\Users\q2799\.conda\envs\py36\lib\site-packages\PyPDF2\_writer.py", line 1174, in add_bookmark parent = self.get_outline_root() File "C:\Users\q2799\.conda\envs\py36\lib\site-packages\PyPDF2\_writer.py", line 1008, in get_outline_root idnum = self._objects.index(outline) + 1 ValueError: {'/Type': '/Outlines', '/First': IndirectObject(1006, 0, 3019818315728), '/Count': 493, '/Last': IndirectObject(1498, 0, 3019818315728)} is not in list ``` `get_outline_root` 修改为: ```python def get_outline_root(self) -> TreeObject: if CO.OUTLINES in self._root_object: # TABLE 3.25 Entries in the catalog dictionary outline = cast(TreeObject, self._root_object[CO.OUTLINES]) try: idnum = self._objects.index(outline) + 1 except ValueError: if not isinstance(outline, TreeObject): def _walk(node): node.__class__ = TreeObject for child in node.children(): _walk(child) _walk(outline) outline_ref = self._add_object(outline) self._add_object(outline_ref.get_object()) self._root_object[NameObject('/Outlines')] = outline_ref idnum = self._objects.index(outline) + 1 outline_ref = IndirectObject(idnum, 0, self) assert outline_ref.get_object() == outline else: outline = TreeObject() outline.update({}) outline_ref = self._add_object(outline) self._root_object[NameObject(CO.OUTLINES)] = outline_ref return outline ``` ![修改后](asset/pypdf2-repair.png.png) ### 1.4.2. 问题二:RuntimeError: generator raised StopIteration 如果在你做了上面的修改后,在运行脚本时报错:`untimeError: generator raised StopIteration`,请检查使用 Python 版本是不是 3.7或者更高版本(从版本v3.7之后,Python终止迭代过程发生了变化,细节可以参考PEP 479)。为避免报错,请使用低于3.7版本的 python,例如 3.6 版本。 ## 1.5. 代码下载 - [https://gitee.com/iam002/add_pdf_bookmarker](https://gitee.com/iam002/add_pdf_bookmarker) - 这里用的 PDF 是 [多源多目标统计信息融合 by Mahler (z-lib.org).pdf](https://z-lib.org/) ,有需要的同学可点击 [阿里云盘](https://www.aliyundrive.com/s/94x2Taq7Jpj) 下载。 ## 1.6. 参考 - [https://www.codetd.com/en/article/11823498](https://www.codetd.com/en/article/11823498) - [https://www.cnblogs.com/1blog/p/15186521.html](https://www.cnblogs.com/1blog/p/15186521.html) - [https://www.jianshu.com/p/1aac3ae4d620?tdsourcetag=s_pcqq_aiomsg](https://www.jianshu.com/p/1aac3ae4d620?tdsourcetag=s_pcqq_aiomsg)