[Python] 纯文本查看 复制代码
import fitz
import sys
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
import threading
import os
def split_all_facing_pdf(input_pdf: str, output_pdf: str, log_callback=None):
"""
所有对折页面对半拆分:
1. 原页面左右对半裁切
2. 左半页整体向左偏移8pt,右半页整体向右偏移8pt
3. 左在前、右在后输出单页,保留旋转矫正开关
"""
def log(msg):
if log_callback:
log_callback(msg)
else:
print(msg)
try:
src_doc = fitz.open(input_pdf)
new_doc = fitz.open()
total_page_count = len(src_doc)
log(f"检测到原PDF总页数:{total_page_count}")
# 偏移量:4字符宽度对应4pt,可按需修改
offset_pt = -8
for page_idx in range(total_page_count):
page = src_doc[page_idx]
page_rect = page.rect
page_w = page_rect.width
page_h = page_rect.height
half_w = page_w / 2
# 原始左右裁切区域
orig_clip_left = fitz.Rect(0, 0, half_w, page_h)
orig_clip_right = fitz.Rect(half_w, 0, page_w, page_h)
# ========== 左半页:向左偏移4pt ==========
# 裁切区域左移offset_pt,防止页面边缘内容被裁掉
clip_left_shift = fitz.Rect(
orig_clip_left.x0 - offset_pt,
orig_clip_left.y0,
orig_clip_left.x1 - offset_pt,
orig_clip_left.y1
)
left_single = new_doc.new_page(width=half_w, height=page_h)
# 内容向右回填offset_pt,实现视觉左移效果
left_target_rect = fitz.Rect(offset_pt, 0, half_w + offset_pt, page_h)
left_single.show_pdf_page(left_target_rect, src_doc, page_idx, clip=clip_left_shift)
# left_single.set_rotation(180) # 按需开启旋转矫正颠倒
# ========== 右半页:向右偏移4pt ==========
# 裁切区域右移offset_pt
clip_right_shift = fitz.Rect(
orig_clip_right.x0 + offset_pt,
orig_clip_right.y0,
orig_clip_right.x1 + offset_pt,
orig_clip_right.y1
)
right_single = new_doc.new_page(width=half_w, height=page_h)
# 内容向左回填offset_pt,实现视觉右移效果
right_target_rect = fitz.Rect(-offset_pt, 0, half_w - offset_pt, page_h)
right_single.show_pdf_page(right_target_rect, src_doc, page_idx, clip=clip_right_shift)
# right_single.set_rotation(180) # 按需开启旋转矫正颠倒
log(f"已拆分原第{page_idx+1}页 → 生成左(左移4pt)、右(右移4pt)2张单页")
new_doc.save(output_pdf)
new_doc.close()
src_doc.close()
log(f"\n✅ 分割偏移矫正完成!文件:{output_pdf}")
return True
except Exception as e:
log(f"❌ 错误:{str(e)}")
return False
class PDFSplitterGUI:
def __init__(self, root):
self.root = root
self.root.title("PDF对折页面拆分工具")
self.root.geometry("700x500")
# 设置样式
style = ttk.Style()
style.theme_use('clam')
# 主框架
main_frame = ttk.Frame(root, padding="20")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# 标题
title_label = ttk.Label(main_frame, text="PDF对折页面拆分工具",
font=('Arial', 16, 'bold'))
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 输入文件选择
ttk.Label(main_frame, text="输入PDF文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.input_var = tk.StringVar()
input_entry = ttk.Entry(main_frame, textvariable=self.input_var, width=50)
input_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=5)
input_btn = ttk.Button(main_frame, text="浏览...", command=self.select_input_file)
input_btn.grid(row=1, column=2, pady=5)
# 输出文件选择
ttk.Label(main_frame, text="输出PDF文件:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_var = tk.StringVar()
output_entry = ttk.Entry(main_frame, textvariable=self.output_var, width=50)
output_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=5)
output_btn = ttk.Button(main_frame, text="浏览...", command=self.select_output_file)
output_btn.grid(row=2, column=2, pady=5)
# 偏移量设置(可选,保持原功能)
ttk.Label(main_frame, text="偏移量(pt):").grid(row=3, column=0, sticky=tk.W, pady=5)
self.offset_var = tk.StringVar(value="-8")
offset_entry = ttk.Entry(main_frame, textvariable=self.offset_var, width=10)
offset_entry.grid(row=3, column=1, sticky=tk.W, pady=5)
ttk.Label(main_frame, text="负值=左偏移,正值=右偏移").grid(row=3, column=1, sticky=tk.W, padx=(120, 0), pady=5)
# 旋转矫正复选框(可选,保持原功能)
self.rotate_var = tk.BooleanVar(value=False)
rotate_cb = ttk.Checkbutton(main_frame, text="启用旋转矫正(180度)",
variable=self.rotate_var)
rotate_cb.grid(row=4, column=0, columnspan=3, sticky=tk.W, pady=10)
# 执行按钮
self.process_btn = ttk.Button(main_frame, text="开始拆分",
command=self.process_pdf, width=20)
self.process_btn.grid(row=5, column=0, columnspan=3, pady=20)
# 日志区域
ttk.Label(main_frame, text="处理日志:").grid(row=6, column=0, sticky=tk.W, pady=(10, 5))
self.log_text = scrolledtext.ScrolledText(main_frame, width=70, height=15,
font=('Courier', 9))
self.log_text.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
# 添加滚动条
main_frame.rowconfigure(7, weight=1)
# 底部状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.grid(row=1, column=0, sticky=(tk.W, tk.E))
# 绑定回车键到处理函数
self.root.bind('<Return>', lambda e: self.process_pdf())
def select_input_file(self):
"""选择输入PDF文件"""
filename = filedialog.askopenfilename(
title="选择输入PDF文件",
filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
)
if filename:
self.input_var.set(filename)
# 自动生成输出文件名
if not self.output_var.get():
input_path = Path(filename)
output_path = input_path.parent / f"{input_path.stem}_split.pdf"
self.output_var.set(str(output_path))
def select_output_file(self):
"""选择输出PDF文件"""
filename = filedialog.asksaveasfilename(
title="保存输出PDF文件",
defaultextension=".pdf",
filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
)
if filename:
self.output_var.set(filename)
def log(self, message):
"""向日志区域添加消息"""
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END) # 自动滚动到底部
self.root.update_idletasks()
def process_pdf(self):
"""处理PDF拆分"""
input_file = self.input_var.get().strip()
output_file = self.output_var.get().strip()
# 验证输入
if not input_file:
messagebox.showerror("错误", "请选择输入PDF文件")
return
if not output_file:
messagebox.showerror("错误", "请指定输出PDF文件")
return
if not os.path.exists(input_file):
messagebox.showerror("错误", f"输入文件不存在:\n{input_file}")
return
# 禁用按钮,防止重复点击
self.process_btn.config(state='disabled')
self.status_var.set("正在处理...")
# 清空日志
self.log_text.delete(1.0, tk.END)
# 在新线程中处理PDF,避免界面卡顿
def process_thread():
try:
success = split_all_facing_pdf(input_file, output_file, self.log)
if success:
self.root.after(0, lambda: messagebox.showinfo("完成",
f"PDF拆分完成!\n输出文件:{output_file}"))
self.root.after(0, lambda: self.status_var.set("处理完成"))
else:
self.root.after(0, lambda: self.status_var.set("处理失败"))
except Exception as e:
self.log(f"❌ 处理异常:{str(e)}")
self.root.after(0, lambda: messagebox.showerror("错误",
f"处理失败:{str(e)}"))
self.root.after(0, lambda: self.status_var.set("处理失败"))
finally:
self.root.after(0, lambda: self.process_btn.config(state='normal'))
# 启动处理线程
thread = threading.Thread(target=process_thread)
thread.daemon = True
thread.start()
def main():
root = tk.Tk()
app = PDFSplitterGUI(root)
# 使窗口可调整大小
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# 设置窗口图标(可选)
try:
root.iconbitmap(default='pdf_icon.ico') # 如果有图标文件的话
except:
pass
root.mainloop()
if __name__ == "__main__":
main()