python
import io
import tkinter as tk
from tkinter import filedialog, messagebox
import fitz # PyMuPDF
from PIL import Image, ImageTk
class PDFViewer:
def __init__(self, root):
self.root = root
self.root.title("PDF Viewer & Editor")
self.doc = None
self.current_page = 0
self.total_pages = 0
self.create_widgets()
def create_widgets(self):
# ---------- 顶部控制栏 ----------
top_frame = tk.Frame(self.root)
top_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
self.page_label = tk.Label(top_frame, text="Page: 0/0")
self.page_label.pack(side=tk.LEFT)
tk.Label(top_frame, text="Go to:").pack(side=tk.LEFT, padx=5)
self.go_to_entry = tk.Entry(top_frame, width=5)
self.go_to_entry.pack(side=tk.LEFT)
self.go_to_btn = tk.Button(top_frame, text="Go", command=self.go_to_page)
self.go_to_btn.pack(side=tk.LEFT)
self.open_btn = tk.Button(top_frame, text="Open PDF", command=self.open_pdf)
self.open_btn.pack(side=tk.RIGHT)
# ---------- 图像显示区域 ----------
self.canvas = tk.Canvas(self.root, width=600, height=800, bg='gray')
self.canvas.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)
# ---------- 底部操作按钮 ----------
bottom_frame = tk.Frame(self.root)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5)
self.prev_btn = tk.Button(bottom_frame, text="Previous", command=self.prev_page)
self.prev_btn.pack(side=tk.LEFT)
self.next_btn = tk.Button(bottom_frame, text="Next", command=self.next_page)
self.next_btn.pack(side=tk.LEFT)
self.delete_btn = tk.Button(bottom_frame, text="Delete", command=self.delete_page)
self.delete_btn.pack(side=tk.LEFT, padx=5)
self.rotate_btn = tk.Button(bottom_frame, text="Rotate Left", command=self.rotate_page)
self.rotate_btn.pack(side=tk.LEFT)
self.insert_btn = tk.Button(bottom_frame, text="Insert PDF", command=self.insert_pdf_after_current)
self.insert_btn.pack(side=tk.LEFT, padx=5)
self.save_btn = tk.Button(bottom_frame, text="Save As...", command=self.save_pdf)
self.save_btn.pack(side=tk.RIGHT)
self.set_buttons_state(tk.DISABLED)
def set_buttons_state(self, state):
for btn in (self.prev_btn, self.next_btn, self.delete_btn,
self.rotate_btn, self.insert_btn, self.save_btn, self.go_to_btn):
btn.config(state=state)
def open_pdf(self):
file_path = filedialog.askopenfilename(filetypes=[("PDF files", "*.pdf")])
if not file_path:
return
try:
if self.doc:
self.doc.close()
self.doc = fitz.open(file_path)
self.total_pages = len(self.doc)
self.current_page = 0
self.set_buttons_state(tk.NORMAL)
self.show_page()
except Exception as e:
messagebox.showerror("Error", f"Failed to open PDF: {e}")
def show_page(self):
if not self.doc or self.total_pages == 0:
self.canvas.delete("all")
self.page_label.config(text="Page: 0/0")
return
if self.current_page < 0:
self.current_page = 0
if self.current_page >= self.total_pages:
self.current_page = self.total_pages - 1
page = self.doc.load_page(self.current_page)
pix = page.get_pixmap(matrix=fitz.Matrix(1, 1))
img_data = pix.tobytes("png")
img = Image.open(io.BytesIO(img_data))
canvas_width = self.canvas.winfo_width() if self.canvas.winfo_width() > 1 else 600
canvas_height = self.canvas.winfo_height() if self.canvas.winfo_height() > 1 else 800
img_width, img_height = img.size
scale = min(canvas_width / img_width, canvas_height / img_height, 1.0)
new_size = (int(img_width * scale), int(img_height * scale))
img = img.resize(new_size, Image.Resampling.LANCZOS)
self.tk_image = ImageTk.PhotoImage(img)
self.canvas.delete("all")
self.canvas.create_image(canvas_width // 2, canvas_height // 2,
anchor=tk.CENTER, image=self.tk_image)
self.page_label.config(text=f"Page: {self.current_page + 1}/{self.total_pages}")
def prev_page(self):
if self.current_page > 0:
self.current_page -= 1
self.show_page()
def next_page(self):
if self.current_page < self.total_pages - 1:
self.current_page += 1
self.show_page()
def go_to_page(self):
try:
page_num = int(self.go_to_entry.get()) - 1
if 0 <= page_num < self.total_pages:
self.current_page = page_num
self.show_page()
else:
messagebox.showwarning("Invalid", f"Page number must be between 1 and {self.total_pages}")
except ValueError:
messagebox.showwarning("Invalid", "Please enter a valid number")
def delete_page(self):
if not self.doc or self.total_pages == 0:
return
if messagebox.askyesno("Delete", f"Delete page {self.current_page + 1}?"):
self.doc.delete_page(self.current_page)
self.total_pages = len(self.doc)
if self.total_pages == 0:
self.doc.close()
self.doc = None
self.set_buttons_state(tk.DISABLED)
self.show_page()
return
if self.current_page >= self.total_pages:
self.current_page = self.total_pages - 1
self.show_page()
def rotate_page(self):
if not self.doc or self.total_pages == 0:
return
page = self.doc.load_page(self.current_page)
new_rot = (page.rotation - 90) % 360
page.set_rotation(new_rot)
self.show_page()
# ========== 插入 PDF(使用图像方式,兼容所有 PyMuPDF 版本) ==========
def insert_pdf_after_current(self):
"""在当前页之后插入另一个 PDF,所有页面自动缩放至当前页尺寸"""
if not self.doc or self.total_pages == 0:
messagebox.showwarning("No document", "Please open a PDF first.")
return
file_path = filedialog.askopenfilename(filetypes=[("PDF files", "*.pdf")])
if not file_path:
return
try:
src_doc = fitz.open(file_path)
except Exception as e:
messagebox.showerror("Error", f"Failed to open insert PDF: {e}")
return
if len(src_doc) == 0:
messagebox.showwarning("Empty", "The selected PDF has no pages.")
src_doc.close()
return
# 获取当前页尺寸
current_page = self.doc.load_page(self.current_page)
target_rect = current_page.rect
target_width = target_rect.width
target_height = target_rect.height
insert_index = self.current_page + 1
new_pages = [] # 记录新页索引
# 对源文档的每一页,渲染为图像并插入到新页
for i in range(len(src_doc)):
src_page = src_doc[i]
# 提高分辨率(2倍),保证打印清晰
pix = src_page.get_pixmap(matrix=fitz.Matrix(2, 2))
img_data = pix.tobytes("png")
img = Image.open(io.BytesIO(img_data))
img_width, img_height = img.size
# 计算缩放比例(保持宽高比,居中)
scale = min(target_width / img_width, target_height / img_height)
new_w = int(img_width * scale)
new_h = int(img_height * scale)
x_offset = (target_width - new_w) / 2
y_offset = (target_height - new_h) / 2
# 调整图像大小
img_resized = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
img_bytes = io.BytesIO()
img_resized.save(img_bytes, format="PNG")
img_bytes.seek(0)
# 创建新页(尺寸为目标尺寸)
new_page = self.doc.new_page(width=target_width, height=target_height)
# 插入图像
new_page.insert_image(
fitz.Rect(x_offset, y_offset, x_offset + new_w, y_offset + new_h),
stream=img_bytes.getvalue()
)
new_pages.append(len(self.doc) - 1) # 记录末尾索引
# 将新页从末尾移动到目标位置(从后往前移动)
old_total = len(self.doc) - len(src_doc)
for i in range(len(src_doc) - 1, -1, -1):
src_index = old_total + i
dest_index = insert_index + i
self.doc.move_page(src_index, dest_index)
self.total_pages = len(self.doc)
self.show_page()
messagebox.showinfo(
"Success",
f"Inserted {len(src_doc)} page(s) after page {self.current_page + 1}.\n"
"All pages have been resized and centered."
)
src_doc.close()
# ========== 保存 PDF ==========
def save_pdf(self):
if not self.doc:
return
file_path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF files", "*.pdf")]
)
if file_path:
try:
self.doc.save(file_path)
messagebox.showinfo("Success", f"PDF saved as {file_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to save: {e}")
def on_closing(self):
if self.doc:
self.doc.close()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = PDFViewer(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
可以在某个页面插入pdf文件,和删除页面,然后可以保存。方便上班族扫描pdf文件,需要去替换一些页面。