一键为扫描版/文字版 PDF 添加目录书签的跨平台桌面工具。
版本: v1.2.0 | 平台: macOS / Windows | 语言: Python 3.10+ | 许可: MIT
使用Claude Code构建
- 便于使用: 选择 PDF → 编辑目录 → 生成书签
- 自动识别目录: 文字版 PDF 直接提取文字识别;扫描版 PDF 通过 MinerU OCR 识别
- 灵活编辑: 支持层级目录(Tab 缩进)、撤销/重做、查找替换、语法高亮
- 页码偏移: 识别到的目录页码可自动匹配 PDF 实际页码
- 多级书签: 支持无限层级嵌套书签
- 暗色/亮色主题: 自动跟随系统主题,可手动切换
- 加密 PDF 处理支持: 输入密码后可处理加密文件(不修改原密码)
- 跨平台: macOS 和 Windows 原生体验
# 1. 克隆项目
git clone <repo-url>
cd Bookmarkify-main
# 2. 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate # macOS
# .venv\Scripts\activate # Windows
# 3. 安装依赖
pip install -r requirements.txt
# 4. 运行
python main.py
# 可选:直接打开指定 PDF
python main.py /path/to/file.pdf下载 Release 页面中的 .dmg(macOS)或 .exe(Windows),直接安装/运行。
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 第一步 │ │ 第二步 │ │ 第三步 │
│ 选择 PDF 文件 │ -> │ 编辑目录 │ -> │ 设置偏移并生成 │
│ │ │ │ │ │
│ · 浏览文件 │ │ · 编辑书签条目 │ │ · 页码偏移 │
│ · 设置目录页范围 │ │ · 调整层级 │ │ · 输出路径 │
│ · 选择识别方式 │ │ · 查找替换 │ │ · 开始生成 │
│ · 自动/手动输入 │ │ · 导入/导出 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
第一章 绪论@@1
第一节 研究背景@@3
第二节 研究方法@@8
第二章 理论基础@@12
第一节 基本概念@@12
一、定义与分类@@15
- 使用 Tab 缩进表示层级关系
- 标题与页码之间用
@@分隔 - 支持无限层级嵌套
- 选择 PDF 文件后,工具会自动检测文件类型(文字版/扫描版)
- 设置目录所在页码范围
- 点击「开始识别」
- 文字版 PDF: 直接从 PDF 中提取文字并解析
- 扫描版 PDF: 调用 MinerU Cloud API 进行 OCR 识别
- Flash 模式: 免费,适用于 ≤20 页、≤10MB 的文件
- 精准模式: 需要从 mineru.net 获取 API Token
⚠️ 注意: 使用 OCR 识别前请关闭代理工具(如 Clash、V2Ray 等)并关闭系统代理,避免 SSL 连接错误。
main/
├── main.py # 应用入口
├── requirements.txt # Python 依赖
├── build.spec # PyInstaller 打包配置
├── build.sh # macOS 构建脚本
├── build.bat # Windows 构建脚本
├── scripts/
│ └── generate_icon.py # 应用图标生成脚本
└── app/
├── assets/ # 图标资源(PNG / ICNS / ICO)
├── core/ # 核心业务逻辑
│ ├── toc_parser.py # 目录解析与序列化
│ ├── pdf_ops.py # PDF 操作封装(PyMuPDF)
│ ├── mineru_client.py # MinerU OCR API 客户端
│ └── toc_auto.py # 自动目录识别
├── gui/ # 图形界面组件
│ ├── main_window.py # 主窗口(工作流协调器)
│ ├── file_panel.py # 第一步:文件选择面板
│ ├── toc_editor.py # 第二步:目录编辑器
│ ├── offset_panel.py # 第三步:偏移与生成面板
│ ├── find_replace.py # 查找/替换对话框
│ └── progress_dialog.py # OCR 进度对话框
└── utils/ # 工具模块
├── constants.py # UI 字符串常量(中文)
├── config.py # 持久化配置管理
└── theme_manager.py # 主题管理器(暗色/亮色)
应用采用 三层架构,清晰分离界面、业务逻辑和工具模块:
┌──────────────────────────────────────────────────┐
│ GUI (app/gui/) │
│ MainWindow · FilePanel · TOCEditor · OffsetPanel│
├──────────────────────────────────────────────────┤
│ Core (app/core/) │
│ toc_parser · pdf_ops · mineru_client · toc_auto │
├──────────────────────────────────────────────────┤
│ Utils (app/utils/) │
│ constants · config · theme_manager │
└──────────────────────────────────────────────────┘
- GUI 层不直接调用
fitz/mineru_sdk,全部通过 Core 层中转 - Core 层为纯逻辑,无 GUI 依赖,可独立测试
- Utils 层提供跨模块共享的配置、主题和常量
| 文件 | 入口/关键函数 | 职责 |
|---|---|---|
| main.py | main() |
应用启动入口:日志初始化、主题检测、窗口创建、事件循环 |
| main.py | get_asset_path() |
资源路径解析(兼容开发与 PyInstaller 打包) |
| app/gui/main_window.py | MainWindow.__init__() |
主窗口初始化:三步面板创建、菜单栏、状态栏、信号连接 |
| app/gui/main_window.py | MainWindow._open_pdf() |
PDF 打开流程:检测加密、判断类型、提取已有目录 |
| app/gui/main_window.py | MainWindow._on_recognize() |
识别触发:启动后台 OCR 线程 |
| app/gui/main_window.py | MainWindow._on_generate() |
生成触发:应用偏移、写入书签、保存 PDF |
| app/gui/main_window.py | MainWindow._auto_detect_offset() |
自动偏移:搜索第一条目录标题在 PDF 中的位置 |
toc_parser.py — 目录解析器
text ──► parse_toc_text() ──► [TOCEntry, ...] ──► toc_to_text() ──► text
│
validate_toc() ──► 警告列表
│
format_toc_with_offset() ──► PyMuPDF TOC 格式
| 函数 | 说明 |
|---|---|
parse_toc_text(text) |
将 Tab 缩进文本解析为 TOCEntry 列表 |
toc_to_text(entries) |
将 TOCEntry 列表序列化为文本 |
validate_toc(entries) |
验证目录合法性(空标题、无效页码) |
format_toc_with_offset(entries, offset, total) |
应用页码偏移,输出 PyMuPDF 兼容格式 |
pdf_ops.py — PDF 操作封装
| 函数 | 说明 |
|---|---|
open_pdf(path, password) |
打开 PDF(自动处理加密) |
create_decrypted_copy(doc) |
创建解密临时副本 |
get_page_count(doc) |
获取总页数 |
detect_pdf_type(doc) |
检测 PDF 类型("scanned" / "text") |
get_existing_toc(doc) |
读取已有书签 |
add_bookmarks(doc, toc_list, output_path) |
写入书签并保存 |
search_text_in_pages(doc, text, ...) |
在页面范围内搜索文字 |
mineru_client.py — MinerU OCR 客户端
| 函数 | 说明 |
|---|---|
check_mineru_available() |
检查 SDK 是否已安装 |
flash_extract(pdf_path) |
免费 OCR(≤20 页,≤10MB),自动重试 |
precision_extract(pdf_path, api_token) |
付费 OCR(需 API Token) |
extract_toc_pages(pdf_path, api_token, start, end) |
提取目录页 → OCR 识别 |
toc_auto.py — 自动目录识别
| 函数 | 说明 |
|---|---|
auto_recognize(doc, ...) |
调度入口:根据 PDF 类型分发 |
_recognize_text_pdf(doc, start, end) |
文字版 PDF 目录提取 |
_recognize_scanned_pdf(pdf_path, ...) |
扫描版 PDF 调用 OCR |
_parse_markdown_toc(markdown) |
OCR 结果 → 目录文本 |
_parse_line(content) |
单行解析(支持多种格式) |
main_window.py — 主窗口(工作流协调器)
MainWindow 是所有 GUI 组件的容器和协调器。它:
- 拥有
fitz.Document对象,控制 PDF 生命周期 - 管理三个步骤面板的创建和信号连接
- 通过线程 + 操作 ID 机制管理后台 OCR,防止过期回调
- 协调自动偏移检测(识别完成后自动触发)
- 提供菜单栏(文件、设置、帮助)和状态栏
关键属性:
doc— 当前打开的fitz.Document(None 表示未打开)_ocr_op_id— OCR 操作 ID,每次新识别递增,回调中校验以丢弃过期结果_decrypted_temp_path— 加密 PDF 解密后的临时文件路径
关键设计:
- 后台 OCR 使用
threading.Thread(target=..., daemon=True)启动 - 回调通过
root.after(0, callback)回到主线程更新 UI - 操作 ID 机制确保多次快速点击识别时,只有最后一次的结果生效
file_panel.py — 文件选择面板
- 可折叠的
ttk.LabelFrame,折叠时显示摘要 - 文件浏览(记忆上次目录)
- PDF 类型自动检测 + 用户可覆盖
- 页码范围选择(Spinbox)
- 手动/自动模式切换
- OCR 模式选择(Flash / 精准)
- API Token 输入(带显示/隐藏切换)
toc_editor.py — 目录编辑器
核心编辑组件,提供:
- 工具栏: 提升/降低层级、撤销/重做、查找替换、导入/导出
- 文本编辑器: 基于
tk.Text,支持撤销/重做栈、等宽字体 - 语法高亮: 4 级标题颜色区分 + 页码高亮,200ms 防抖更新
- 状态栏: 书签计数、最大层级、验证警告
- 主题感知: 注册为
ThemeManager观察者,主题切换时实时更新配色 - 占位文本: 未聚焦时显示格式提示
offset_panel.py — 偏移与生成面板
- 联动 Spinbox:左侧为目录页码,右侧为 PDF 实际页码
- 实时计算并显示偏移量
- 输出路径浏览
- 「覆盖原文件」复选框
- 「开始添加书签」按钮
find_replace.py — 查找替换对话框
- 支持区分大小写和正则表达式
- 查找下一个 / 替换 / 全部替换
- 到末尾自动回到开头继续搜索
- 关闭时清除高亮
progress_dialog.py — OCR 进度对话框
- 模态对话框,OCR 期间阻止交互
- 5 阶段进度:初始化 → 上传 → 处理 → 下载 → 解析
- Unicode 盲文字符旋转动画
- 取消按钮可终止 OCR
- 完成后 800ms 自动关闭
constants.py — UI 字符串常量
所有用户可见的中文字符串集中定义在此,便于:
- 国际化改造(替换常量文件即可)
- 文案统一修改
- 避免硬编码字符串散落各处
config.py — 持久化配置
- 存储位置:
~/.pdf_bookmark_config.json - 当前配置项:
last_directory,mineru_api_token,ocr_mode,window_width,window_height,default_offset - 每次
set()自动写入磁盘 - 损坏文件自动降级(重置为默认值)
theme_manager.py — 主题管理器
- 单例模式: 全局唯一实例
- 系统检测: 使用
darkdetect监听系统主题变化 - 配色方案: Catppuccin Mocha 风格暗色主题 + 专业亮色主题
- 配色项目: 文本背景/前景、4 级标题色、页码色、警告色、匹配高亮色
- 观察者模式: 组件可注册回调,主题切换时自动通知更新
使用方式:
tm = ThemeManager()
tm.register_observer(my_callback) # 注册观察者
tm.toggle_theme() # 切换主题- Python 3.10+
- 依赖见 requirements.txt
# 开发模式运行
python main.py
# 查看日志
tail -f ~/.pdf_bookmark_tool.log
# 查看配置文件
cat ~/.pdf_bookmark_config.json日志文件位于 ~/.pdf_bookmark_tool.log,记录 DEBUG 级别日志。控制台仅输出 WARNING 及以上级别。
bash build.sh # 基础打包
bash build.sh --sign "Developer ID" # 带签名
bash build.sh --dmg # 打包 DMG
bash build.sh --sign "ID" --dmg # 签名 + DMGbuild.bat打包配置参见 build.spec,已排除 numpy、scipy、pandas、PyQt、torch 等大型库以减小体积。
| 决策 | 理由 |
|---|---|
@@ 作为目录分隔符 |
比传统 PdgCntEditor 的 Tab 分隔格式更直观、不易混淆 |
| PyMuPDF (fitz) 而非 PyPDF2 | 更好的性能、加密 PDF 支持、书签读写 API |
| ttkbootstrap 而非 PyQt | 更轻量、打包体积小、对 tkinter 生态友好 |
| MinerU Cloud API 而非本地 OCR | 免去大型 OCR 模型部署,降低使用门槛 |
| 操作 ID 机制 | 防止快速点击识别时过期结果覆盖新结果 |
| 加密 PDF 解密副本 | 避免要求用户提供原始密码进行保存操作 |
| 单例 ThemeManager + 观察者 | 全局主题统一管理,组件自动响应切换 |
- 添加新的 UI 字符串: 在 constants.py 中添加,不要硬编码到 GUI 文件
- 添加新的 PDF 操作: 在 pdf_ops.py 中添加,不要直接在 GUI 层调用
fitz - 添加新配置项: 在 config.py 的
DEFAULTS字典中添加 - 添加新主题颜色: 在 theme_manager.py 的
dark_colors/light_colors中添加 - GUI 组件开发: 继承 ttk 控件,通过
ThemeManager获取主题色,通过Config读写持久化设置
Q: OCR 识别失败,提示 SSL 错误?
A: 关闭系统代理和代理工具(Clash、V2Ray 等)。这是 macOS + Python 3.14 + OpenSSL 3.6 0-RTT 模式下的已知兼容性问题。
Q: 识别结果不准确?
A: 尝试缩小目录页码范围,确保只包含目录页。扫描版 PDF 的质量也会影响 OCR 准确度。
Q: 加密 PDF 如何处理?
A: 工具会弹出密码输入框,输入正确密码后会创建解密临时副本进行操作,原文件不会被修改。
| 组件 | 技术 |
|---|---|
| 语言 | Python 3.10+ |
| GUI 框架 | tkinter + ttkbootstrap |
| PDF 处理 | PyMuPDF (fitz) |
| OCR 引擎 | MinerU Cloud API |
| 图片处理 | Pillow (PIL) |
| 系统主题检测 | darkdetect |
| 打包 | PyInstaller |