[Python] 纯文本查看 复制代码pip install PyMuPDF
python脚本:
[Python] 纯文本查看 复制代码import fitz # PyMuPDF
import tkinter as tk
from tkinter import filedialog, ttk, messagebox, simpledialog
import os
class PDFRectangleDrawer:
def __init__(self, root):
self.root = root
self.root.title("PDF 矩形绘制工具")
self.root.geometry("1000x700")
# 初始化变量
self.pdf_path = None
self.doc = None
self.current_page_num = 0
self.total_pages = 0
self.rectangles = [] # 存储所有绘制的矩形 [(page_num, rect_coords), ...]
self.start_x = None
self.start_y = None
self.rect = None
self.zoom = 1.5
self.page_image = None # 存储当前页面的图像对象
# 创建主框架
self.main_frame = ttk.Frame(root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建顶部控制区域
self.control_frame = ttk.Frame(self.main_frame)
self.control_frame.pack(fill=tk.X, pady=(0, 10))
# 打开PDF按钮
self.open_btn = ttk.Button(self.control_frame, text="打开PDF", command=self.open_pdf)
self.open_btn.pack(side=tk.LEFT, padx=5)
# 页面导航区域
self.nav_frame = ttk.Frame(self.control_frame)
self.nav_frame.pack(side=tk.LEFT, padx=20)
self.prev_btn = ttk.Button(self.nav_frame, text="上一页", command=self.prev_page, state=tk.DISABLED)
self.prev_btn.pack(side=tk.LEFT, padx=5)
self.page_label = ttk.Label(self.nav_frame, text="0/0")
self.page_label.pack(side=tk.LEFT, padx=5)
self.next_btn = ttk.Button(self.nav_frame, text="下一页", command=self.next_page, state=tk.DISABLED)
self.next_btn.pack(side=tk.LEFT, padx=5)
self.goto_btn = ttk.Button(self.nav_frame, text="跳转到", command=self.goto_page, state=tk.DISABLED)
self.goto_btn.pack(side=tk.LEFT, padx=5)
# 处理按钮
self.process_frame = ttk.Frame(self.control_frame)
self.process_frame.pack(side=tk.RIGHT)
self.clear_btn = ttk.Button(self.process_frame, text="清除矩形", command=self.clear_rectangles, state=tk.DISABLED)
self.clear_btn.pack(side=tk.LEFT, padx=5)
self.process_btn = ttk.Button(self.process_frame, text="处理PDF", command=self.process_pdf, state=tk.DISABLED)
self.process_btn.pack(side=tk.LEFT, padx=5)
# 创建画布区域
self.canvas_frame = ttk.Frame(self.main_frame)
self.canvas_frame.pack(fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(self.canvas_frame, bg="lightgray")
self.h_scroll = ttk.Scrollbar(self.canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview)
self.v_scroll = ttk.Scrollbar(self.canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview)
self.canvas.configure(xscrollcommand=self.h_scroll.set, yscrollcommand=self.v_scroll.set)
self.h_scroll.pack(side=tk.BOTTOM, fill=tk.X)
self.v_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 底部状态栏
self.status_frame = ttk.Frame(self.main_frame)
self.status_frame.pack(fill=tk.X, pady=(10, 0))
self.status_label = ttk.Label(self.status_frame, text="就绪。请打开PDF文件...")
self.status_label.pack(side=tk.LEFT)
self.rect_count_label = ttk.Label(self.status_frame, text="矩形数量: 0")
self.rect_count_label.pack(side=tk.RIGHT)
# 页面范围选择框架
self.range_frame = ttk.LabelFrame(self.main_frame, text="处理页面范围")
self.range_frame.pack(fill=tk.X, pady=(10, 0))
self.range_var = tk.StringVar(value="all")
self.range_all = ttk.Radiobutton(self.range_frame, text="所有页面", variable=self.range_var, value="all")
self.range_all.pack(side=tk.LEFT, padx=10)
self.range_current = ttk.Radiobutton(self.range_frame, text="当前页面", variable=self.range_var, value="current")
self.range_current.pack(side=tk.LEFT, padx=10)
# 自定义范围按钮(替代原来的单选按钮和输入框)
self.custom_range_btn = ttk.Button(self.range_frame, text="自定义范围",
command=self.apply_current_rectangles_to_range,
state=tk.DISABLED)
self.custom_range_btn.pack(side=tk.LEFT, padx=10)
# 绑定鼠标事件
self.canvas.bind("[B]", self.on_button_press)
self.canvas.bind("[B]", self.on_mouse_drag)
self.canvas.bind("[B]", self.on_button_release)
# 绑定鼠标滚轮事件用于滚动和缩放
self.canvas.bind("", self.on_mouse_wheel_scroll) # Windows
self.canvas.bind("[B]", self.on_mouse_wheel_scroll) # Linux上滚
self.canvas.bind("[B]", self.on_mouse_wheel_scroll) # Linux下滚
self.canvas.bind("", self.on_mouse_wheel_zoom) # Windows缩放
self.canvas.bind("", self.on_mouse_wheel_zoom) # Linux缩放
self.canvas.bind("", self.on_mouse_wheel_zoom) # Linux缩放
# 绑定窗口大小变化事件
self.canvas.bind("", self.on_canvas_configure)
def open_pdf(self):
"""打开PDF文件"""
pdf_path = filedialog.askopenfilename(title="选择 PDF 文件", filetypes=[("PDF 文件", "*.pdf")])
if not pdf_path:
return
self.pdf_path = pdf_path
self.doc = fitz.open(self.pdf_path)
self.total_pages = len(self.doc)
self.current_page_num = 0
# 更新状态
self.status_label.config(text=f"已加载: {os.path.basename(self.pdf_path)}")
# 启用按钮
self.prev_btn.config(state=tk.NORMAL)
self.next_btn.config(state=tk.NORMAL)
self.goto_btn.config(state=tk.NORMAL)
self.clear_btn.config(state=tk.NORMAL)
self.process_btn.config(state=tk.NORMAL)
self.custom_range_btn.config(state=tk.NORMAL)
# 加载第一页
self.load_page(0)
def load_page(self, page_num):
"""加载指定页面"""
if not self.doc or page_num = self.total_pages:
return
self.current_page_num = page_num
self.page = self.doc[page_num]
# 更新页面标签
self.page_label.config(text=f"{page_num + 1}/{self.total_pages}")
# 渲染页面
self.render_page()
# 绘制已有的矩形
self.draw_existing_rectangles()
def render_page(self):
"""渲染当前页面到画布"""
# 清除画布
self.canvas.delete("all")
# 设置缩放
mat = fitz.Matrix(self.zoom, self.zoom)
pix = self.page.get_pixmap(matrix=mat)
# 创建图像
img_data = pix.samples
img_mode = "RGBA" if pix.alpha else "RGB"
img_size = (pix.width, pix.height)
from PIL import Image, ImageTk
img = Image.frombytes(img_mode, img_size, img_data)
self.page_image = ImageTk.PhotoImage(image=img)
# 计算画布中心位置
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# 计算图像位置,使其居中
x_pos = max(0, (canvas_width - pix.width) // 2)
y_pos = max(0, (canvas_height - pix.height) // 2)
# 设置画布滚动区域,确保足够大以容纳图像和居中的空间
scroll_width = max(canvas_width, pix.width + x_pos * 2)
scroll_height = max(canvas_height, pix.height + y_pos * 2)
self.canvas.config(scrollregion=(0, 0, scroll_width, scroll_height))
# 在计算的位置显示图像
self.canvas.create_image(x_pos, y_pos, image=self.page_image, tags="page", anchor=tk.NW)
# 初始滚动位置
if pix.width > canvas_width:
self.canvas.xview_moveto(x_pos / scroll_width)
else:
self.canvas.xview_moveto(0)
if pix.height > canvas_height:
self.canvas.yview_moveto(y_pos / scroll_height)
else:
self.canvas.yview_moveto(0)
def on_canvas_configure(self, event):
"""当画布大小改变时重新居中图像"""
if hasattr(self, 'page') and self.page_image:
# 重新渲染页面以更新居中位置
self.render_page()
self.draw_existing_rectangles()
def draw_existing_rectangles(self):
"""在当前页面上绘制已存在的矩形"""
for page_num, rect_coords in self.rectangles:
if page_num == self.current_page_num:
# 将PDF坐标转换为画布坐标
x0, y0, x1, y1 = rect_coords
canvas_x0 = x0 * self.zoom
canvas_y0 = y0 * self.zoom
canvas_x1 = x1 * self.zoom
canvas_y1 = y1 * self.zoom
# 获取图像位置偏移
image_item = self.canvas.find_withtag("page")
if image_item:
image_coords = self.canvas.coords(image_item[0])
if image_coords:
offset_x, offset_y = image_coords[0], image_coords[1]
# 绘制矩形 - 半透明白色,考虑图像偏移
self.canvas.create_rectangle(
offset_x + canvas_x0, offset_y + canvas_y0,
offset_x + canvas_x1, offset_y + canvas_y1,
outline="black", width=1, fill="white", stipple="gray50", tags="rect"
)
def prev_page(self):
"""显示上一页"""
if self.current_page_num > 0:
self.load_page(self.current_page_num - 1)
def next_page(self):
"""显示下一页"""
if self.current_page_num 0 else -0.1
new_zoom = max(0.5, min(3.0, self.zoom + factor))
if new_zoom != self.zoom:
self.zoom = new_zoom
self.render_page()
self.draw_existing_rectangles()
self.status_label.config(text=f"缩放: {self.zoom:.1f}x")
def clear_rectangles(self):
"""清除所有矩形"""
if not self.rectangles:
return
if messagebox.askyesno("确认", "确定要清除所有矩形吗?"):
self.rectangles = []
self.rect_count_label.config(text="矩形数量: 0")
self.render_page() # 重新渲染页面,清除所有矩形
self.status_label.config(text="已清除所有矩形")
def parse_page_range(self, range_str, max_pages):
"""解析页面范围字符串,返回页码列表"""
pages = []
if not range_str:
return pages
parts = range_str.split(',')
for part in parts:
if '-' in part:
start, end = part.split('-')
try:
start_num = int(start.strip())
end_num = int(end.strip())
if 1