我给自己写了一个本地 PDF 整理工具:合并、拆分、排序一次完成
目录
- 缘起:整理几页 PDF 为什么总要打开好几个软件
- 需求拆解:文档操作首先要看清页面顺序
- 技术选型:围绕 pypdf 做轻量处理
- 核心实现一:读取页面并建立可操作模型
- 核心实现二:合并、拆分与安全写入
- UI 定制:让页面顺序可以直接被确认
- 三个 PDF 整理中容易踩的坑
- 结尾:文档工具的价值是减少重复确认
1. 缘起:整理几页 PDF 为什么总要打开好几个软件
我经常要处理一些很具体的 PDF 操作:把签字页放回合同末尾,删除报告里的空白页,把扫描件按章节合成一个文件,或者从几十页资料里单独提取几页发给同事。
这些操作本身不难,麻烦的是工具和流程:
- 在线网站需要把内部文档上传到第三方;
- 桌面软件功能很多,启动和操作都很重;
- 命令行可以批处理,却无法直观看到页面顺序;
- 页面范围写错后,导出的文件要重新打开才能发现;
- 合并后原有书签、标题和元数据可能消失。
我需要的不是一个完整 PDF 编辑器,而是一个安静的本地整理工具:把文件拖进来,看到页数和缩略图,调整顺序或选择范围,最后生成一个新的 PDF,不修改原文件。
2. 需求拆解:文档操作首先要看清页面顺序
我把常见操作拆成了可以检查的目标:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 合并多个文件 | 显示文件顺序和每个文件的页数 |
| 调整章节顺序 | 支持拖拽文件或页面移动 |
| 提取指定页面 | 支持单页、连续区间和多段范围 |
| 合并前看结果 | 提供缩略图和总页数预览 |
| 原文件不能被覆盖 | 默认输出到新文件并检查同名冲突 |
| 文档结构别丢 | 尽量保留书签、旋转角度和元数据 |
第一版不做 PDF 内文修改、OCR 和电子签名。它只处理页面级操作,避免把一个整理工具扩展成编辑器。
心得:页面级工具最重要的是让“顺序”和“范围”可视化,而不是增加更多编辑按钮。
3. 技术选型:围绕 pypdf 做轻量处理
程序使用 Python,核心 PDF 操作交给 pypdf,界面使用 tkinter:
| 能力 | 方案 |
|---|---|
| 读取 PDF | pypdf.PdfReader |
| 合并页面 | pypdf.PdfWriter |
| 书签读写 | PdfReader.outline 与 add_outline_item |
| 页面缩略图 | PyMuPDF 渲染 |
| 桌面界面 | tkinter |
| 安全导出 | 临时文件 + 原子替换 |
读取和写入使用不同对象。PdfReader 负责验证源文件和访问页面,PdfWriter 只接收用户最终确认的页面列表。这样界面中的拖拽预览不会意外修改源文档。
缩略图生成是可选能力。没有安装渲染依赖时,工具仍然可以显示页面编号、尺寸和旋转角度,不会因为无法生成图片就失去基本整理功能。
4. 核心实现一:读取页面并建立可操作模型
每一页都转换成一个轻量的页面引用,记录来源文件、原始页码和旋转信息:
from dataclasses import dataclass
from pathlib import Path
from pypdf import PdfReader
@dataclass
class PageRef:
source: Path
source_index: int
width: float
height: float
rotation: int
def inspect_pdf(path: Path):
reader = PdfReader(str(path), strict=False)
pages = []
for index, page in enumerate(reader.pages):
box = page.mediabox
pages.append(PageRef(
path,
index,
float(box.width),
float(box.height),
int(page.get("/Rotate", 0) or 0),
))
return reader, pages
界面中的拖拽只改变 PageRef 列表,不直接移动 PDF 对象。用户删除某一页时,也只是把它从待输出列表中移除,源文件不会受到影响。
每个文件显示页数、文件大小、页面尺寸是否统一和是否存在加密。加密 PDF 需要密码时,工具暂停预览并提示输入;密码不会写入配置和历史记录。
5. 核心实现二:合并、拆分与安全写入
合并时按照当前页面列表把源页面加入 writer:
from pypdf import PdfWriter
def write_pages(page_refs, output_path):
writer = PdfWriter()
for ref in page_refs:
reader = PdfReader(str(ref.source), strict=False)
writer.add_page(reader.pages[ref.source_index])
with output_path.open("wb") as file:
writer.write(file)
实际导出不会直接写最终路径,而是先写到同目录临时文件,成功后再替换。写入完成后重新打开临时结果,确认页数和文件尾结构正常,再改成用户指定的名称。
拆分范围使用人类习惯的页码,从 1 开始。输入 1-3,7,10-12 会被解析成不重复的页码列表,超出页数或区间反向时立即报错。
def parse_ranges(value, page_count):
result = []
for part in value.split(","):
if "-" in part:
start, end = map(int, part.split("-", 1))
numbers = range(start, end + 1)
else:
numbers = [int(part)]
for number in numbers:
if not 1 <= number <= page_count:
raise ValueError(f"页码超出范围:{number}")
if number not in result:
result.append(number)
return result
拆分为多个文件时,程序先建立完整输出计划,检查文件名冲突和目标目录空间,再开始写入。某一份失败不会被汇报成整批成功。
6. UI 定制:让页面顺序可以直接被确认
窗口左侧是文件列表,中间是页面缩略图,右侧是当前页面信息和操作。文件列表可以拖动排序,页面缩略图也可以跨文件移动。
顶部状态栏持续显示:当前页面总数、预计输出大小、已选择页面数和加密文件数量。用户执行删除或拆分时,按钮文字直接写明影响范围,例如“导出 18 页”,而不是只写一个模糊的“确定”。
缩略图加载采用分批方式,先显示占位符,再在后台生成可见区域的页面。用户快速调整顺序时不会因为等待几十张预览图而卡住。
书签处理提供两个策略:保留原文件书签并按页面映射,或导出后重新生成简单章节书签。无法可靠映射的书签会列在报告中,不会悄悄丢失后假装全部保留。
7. 三个 PDF 整理中容易踩的坑
坑一:页面顺序和书签层级不是一回事。 移动页面后,原书签指向的页码可能已经变化。简单复制书签树会让标题跳到错误页面,因此工具要么重新映射目标页,要么明确提示书签需要重新整理。
坑二:旋转角度不等于页面尺寸旋转。 扫描 PDF 可能通过 /Rotate 表示横竖方向。只看 MediaBox 会让缩略图方向错误,导出时也可能把横向页面显示成竖向。页面模型必须同时保存尺寸和旋转信息。
坑三:加密、表单和外部资源可能影响合并结果。 某些 PDF 含有表单字段、注释、嵌入字体或外部引用,简单复制页面不一定完整保留所有交互能力。工具在检测到特殊结构时给出提示,并把“页面内容成功合并”和“所有高级特性完整保留”区分开。
另外,PDF 只写出了文件并不代表结果可用。导出后至少要重新读取页数、检查每页尺寸,并在用户需要时打开系统默认查看器做人工抽检。
8. 结尾:文档工具的价值是减少重复确认
做完这个 PDF 整理工具后,我处理文档时不再靠文件名和记忆猜顺序。把文件拖进来、查看缩略图、调整页面、确认输出范围,再生成新文件,整个过程可见且可回退。
一个可靠的本地 PDF 工具应该做到:
- 操作页面前先显示页数和来源;
- 所有修改都发生在新输出文件上;
- 页码区间按用户习惯解析并严格校验;
- 对书签、旋转和加密等特殊结构给出提示;
- 导出后重新读取并验证结果。
如果你也想做类似工具,可以先实现合并和页码拆分,再加入缩略图、拖拽排序与书签映射。文档处理最怕“看起来完成了”,所以每一次导出都应该有明确的页数和结构检查。
当页面顺序、输出范围和源文件边界都清楚,整理 PDF 就不再是一连串小心翼翼的手工操作。
- 点赞
- 收藏
- 关注作者
评论(0)