python入门代码案例:pdf阅读器带图片转换
基于python代码实现的pdf文档阅读器。打开路径内pdf文件,涵盖了书签与目录功能。
从图片中看出,我们考虑了ui界面的极简性,以及上下翻页功能。
还有使用频率比较高的的pdf文件转换图片,将pdf文件中的每一页面分割为单独的图片,另外一个小细节,我们导出的图片同样是基于页面顺序。
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import fitz # PyMuPDF
from PIL import Image, ImageTk
import json
import re
import os
import platform
from tkinter import font
import threading
from typing import Optional
import ttkbootstrap as tb
class MacPDFExpert:
def __init__(self, root):
self.root = root
self.root.title("PDF阅读器『六道』")
self.root.geometry("1200x800+200+200")
self.setup_system_style()
# 文档状态
self.current_page = 0
self.pdf_document: Optional[fitz.Document] = None
self.image_list = []
self.zoom_level = 1.0
self.dpi = 96
# 用户数据
self.bookmarks = {}
self.annotations = {}
self.search_results = []
self.current_search_index = -1
# 持久化配置
self.settings_file = "pdf_expert_settings.json"
self.annotations_file = "pdf_annotations.json"
# UI组件
self.create_widgets()
self.setup_bindings()
self.load_initial_data()
def setup_system_style(self):
"""根据操作系统应用相应视觉风格"""
theme = "cosmo" if platform.system() == "Darwin" else "flatly"
self.style = tb.Style(theme=theme)
print(f"Available colors: {self.style.colors.__dict__}") # Debugging line to see available colors
self.root.configure(bg=self.style.colors.bg)
# 字体配置
system_font = "Helvetica" if platform.system() == "Darwin" else "Segoe UI"
default_font = font.nametofont("TkDefaultFont")
default_font.configure(family=system_font, size=12)
# 控件样式
self.style.configure("TButton",
padding=(10, 5),
relief="flat",
font=(system_font, 12),
anchor="center")
# 通用样式
self.style.configure("TProgressbar",
thickness=3,
troughcolor="#E5E5EA",
background="#34C759")
self.style.configure("Treeview",
background="white",
fieldbackground="white",
bordercolor="#CECED2",
font=(system_font, 11))
self.style.map("Treeview",
background=[("selected", "#007AFF")],
foreground=[("selected", "white")])
def create_widgets(self):
"""创建所有界面组件"""
# 主容器
main_container = tb.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True)
# 顶部工具栏
toolbar = tb.Frame(main_container, padding=(10, 5))
toolbar.pack(side=tk.TOP, fill=tk.X)
# 操作按钮组
btn_group = tb.Frame(toolbar)
btn_group.pack(side=tk.LEFT)
self.open_btn = tb.Button(btn_group, text="📂 打开", command=self.open_pdf, bootstyle="outline-primary")
self.open_btn.pack(side=tk.LEFT, padx=2)
self.export_menu = self.create_export_menu(btn_group)
self.export_btn = tb.Button(btn_group, text="↩️ 导出", command=lambda:
self.export_menu.post(self.export_btn.winfo_rootx(),
self.export_btn.winfo_rooty() + 30), bootstyle="outline-secondary")
self.export_btn.pack(side=tk.LEFT, padx=2)
# 导航控件组
nav_group = tb.Frame(toolbar)
nav_group.pack(side=tk.LEFT, padx=20)
self.prev_btn = tb.Button(nav_group, text="◀", width=3, command=self.prev_page, bootstyle="outline-info")
self.prev_btn.pack(side=tk.LEFT)
self.page_entry = tb.Entry(nav_group, width=5, font=("TkDefaultFont", 12))
self.page_entry.pack(side=tk.LEFT, padx=5)
self.page_entry.insert(0, "1")
self.next_btn = tb.Button(nav_group, text="▶", width=3, command=self.next_page, bootstyle="outline-success")
self.next_btn.pack(side=tk.LEFT)
# 缩放控件组
zoom_group = tb.Frame(toolbar)
zoom_group.pack(side=tk.LEFT, padx=20)
tb.Label(zoom_group, text="缩放:").pack(side=tk.LEFT)
self.zoom_scale = tb.Scale(zoom_group, from_=25, to=400,
command=lambda v: self.update_zoom(int(float(v))), orient="horizontal", length=150)
self.zoom_scale.set(100)
self.zoom_scale.pack(side=tk.LEFT, padx=5)
# 搜索组
search_group = tb.Frame(toolbar)
search_group.pack(side=tk.RIGHT)
self.search_entry = tb.Entry(search_group, width=25)
self.search_entry.pack(side=tk.LEFT)
self.search_btn = tb.Button(search_group, text="🔍 搜索", command=self.search_text, bootstyle="outline-warning")
self.search_btn.pack(side=tk.LEFT, padx=5)
# 主内容区
content_paned = tb.Panedwindow(main_container, orient=tk.HORIZONTAL)
content_paned.pack(fill=tk.BOTH, expand=True)
# 左侧导航面板
self.left_panel = tb.Frame(content_paned, width=220)
content_paned.add(self.left_panel, weight=0)
# 导航标签页
self.nav_notebook = tb.Notebook(self.left_panel)
self.nav_notebook.pack(fill=tk.BOTH, expand=True)
# 目录标签
toc_frame = tb.Frame(self.nav_notebook)
self.toc_tree = tb.Treeview(toc_frame, show="tree", selectmode="browse")
self.toc_tree_scroll = tb.Scrollbar(toc_frame, command=self.toc_tree.yview)
self.toc_tree.configure(yscrollcommand=self.toc_tree_scroll.set)
self.toc_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.toc_tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.nav_notebook.add(toc_frame, text="目录")
# 书签标签
bookmark_frame = tb.Frame(self.nav_notebook)
self.bookmark_list = tk.Listbox(bookmark_frame, bg="white", bd=0,
font=("TkDefaultFont", 11), selectbackground="#007AFF")
self.bookmark_scroll = tk.Scrollbar(bookmark_frame, command=self.bookmark_list.yview)
self.bookmark_list.configure(yscrollcommand=self.bookmark_scroll.set)
self.bookmark_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.bookmark_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.nav_notebook.add(bookmark_frame, text="书签")
# 主显示区
self.right_panel = tb.Frame(content_paned)
content_paned.add(self.right_panel, weight=1)
# PDF显示画布
self.canvas = tb.Canvas(self.right_panel, bg="white", highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
# 状态栏
self.status_bar = tb.Frame(self.root, height=24, style="StatusBar.TFrame")
self.status_label = tb.Label(self.status_bar,
text="就绪",
anchor=tk.W,
style="StatusBar.TLabel")
self.status_label.pack(side=tk.LEFT, padx=10)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 进度条
self.progress = tb.Progressbar(self.root, mode="determinate")
# 上下文菜单
self.context_menu = tb.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="添加注释", command=self.add_annotation)
self.context_menu.add_command(label="删除书签", command=self.delete_bookmark)
def create_export_menu(self, parent):
"""创建导出功能的下拉菜单"""
menu = tb.Menu(parent, tearoff=0)
menu.add_command(label="导出当前页为图片...",
command=self.export_current_page,
accelerator="Cmd+S" if platform.system() == "Darwin" else "Ctrl+S")
menu.add_command(label="导出全部页面为图片...",
command=self.export_all_pages,
accelerator="Cmd+Shift+E" if platform.system() == "Darwin" else "Ctrl+Shift+E")
menu.add_separator()
menu.add_command(label="导出选项...", command=self.show_export_settings)
return menu
def show_export_settings(self):
"""显示导出设置对话框"""
export_window = tb.Toplevel(self.root)
export_window.title("导出设置")
export_window.geometry("300x200+300+300")
# DPI 设置
dpi_frame = tb.Frame(export_window)
dpi_frame.pack(pady=10)
tb.Label(dpi_frame, text="DPI:").pack(side=tk.LEFT, padx=5)
self.dpi_var = tk.StringVar(value=str(self.dpi))
dpi_entry = tb.Entry(dpi_frame, textvariable=self.dpi_var, width=10)
dpi_entry.pack(side=tk.LEFT, padx=5)
# 应用按钮
apply_btn = tb.Button(export_window, text="应用", command=self.apply_export_settings, bootstyle="primary-outline")
apply_btn.pack(pady=10)
def apply_export_settings(self):
"""应用导出设置"""
try:
new_dpi = int(self.dpi_var.get())
if new_dpi > 0:
self.dpi = new_dpi
self.save_settings()
messagebox.showinfo("提示", "设置已保存")
else:
messagebox.showerror("错误", "请输入有效的DPI值")
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
# 文件操作功能
def open_pdf(self):
"""打开PDF文件并初始化视图"""
file_path = filedialog.askopenfilename(filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")])
if file_path:
try:
if self.pdf_document:
self.pdf_document.close()
self.pdf_document = fitz.open(file_path)
self.current_page = 0
self.zoom_level = 1.0
self.zoom_scale.set(100)
self.update_ui_state()
self.load_annotations()
self.update_toc()
self.show_page()
except Exception as e:
self.show_error_message(f"无法打开PDF文件: {str(e)}")
def update_ui_state(self):
"""更新界面状态"""
has_doc = self.pdf_document is not None
self.export_btn.state(["!disabled" if has_doc else "disabled"])
self.prev_btn.state(["!disabled" if has_doc else "disabled"])
self.next_btn.state(["!disabled" if has_doc else "disabled"])
self.search_btn.state(["!disabled" if has_doc else "disabled"])
self.zoom_scale.state(["!disabled" if has_doc else "disabled"])
# 页面导航功能
def jump_to_page(self):
"""跳转到指定页码"""
if not self.pdf_document:
return
try:
page_num = int(self.page_entry.get()) - 1
if 0 <= page_num < len(self.pdf_document):
self.current_page = page_num
self.show_page()
else:
self.show_error_message("无效的页码")
except ValueError:
self.show_error_message("请输入有效的数字")
def prev_page(self):
"""跳转到上一页"""
if self.current_page > 0:
self.current_page -= 1
self.show_page()
def next_page(self):
"""跳转到下一页"""
if self.current_page < len(self.pdf_document) - 1:
self.current_page += 1
self.show_page()
# 显示页面功能
def show_page(self):
"""显示当前页的内容"""
if not self.pdf_document:
return
page = self.pdf_document[self.current_page]
pix = page.get_pixmap(dpi=int(self.dpi * self.zoom_level)) # Ensure dpi is an integer
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
self.image_list.append(ImageTk.PhotoImage(img))
self.canvas.delete("all")
self.canvas.create_image(0, 0, image=self.image_list[-1], anchor="nw")
self.page_entry.delete(0, tk.END)
self.page_entry.insert(0, str(self.current_page + 1))
# 缩放功能
def update_zoom(self, value):
"""更新缩放级别并重新显示页面"""
self.zoom_level = float(value) / 100
self.show_page()
# 搜索功能
def search_text(self):
"""搜索文档中的文本"""
query = self.search_entry.get().strip()
if not query:
self.show_error_message("请输入搜索内容")
return
self.search_results.clear()
for i in range(len(self.pdf_document)):
text_instances = self.pdf_document[i].search_for(query)
for inst in text_instances:
self.search_results.append((i, inst))
if self.search_results:
self.current_search_index = 0
self.highlight_search_result()
else:
self.show_info_message("未找到匹配项")
def highlight_search_result(self):
"""高亮显示搜索结果"""
if not self.search_results or self.current_search_index == -1:
return
page_num, rect = self.search_results[self.current_search_index]
self.jump_to_page(page_num)
x0, y0, x1, y1 = rect.x0, rect.y0, rect.x1, rect.y1
scale_factor = self.zoom_level * self.dpi / 72
self.canvas.coords("highlight", x0 * scale_factor, y0 * scale_factor,
x1 * scale_factor, y1 * scale_factor)
self.canvas.itemconfig("highlight", outline="red", width=2)
def next_search_result(self):
"""跳转到下一个搜索结果"""
if self.current_search_index >= len(self.search_results) - 1:
self.current_search_index = 0
else:
self.current_search_index += 1
self.highlight_search_result()
def prev_search_result(self):
"""跳转到上一个搜索结果"""
if self.current_search_index <= 0:
self.current_search_index = len(self.search_results) - 1
else:
self.current_search_index -= 1
self.highlight_search_result()
# 注释和书签功能
def add_annotation(self):
"""添加注释"""
if not self.pdf_document:
return
comment = simpledialog.askstring("添加注释", "输入您的注释:")
if comment:
page = self.pdf_document[self.current_page]
annot = page.add_highlight_annot(page.rect)
annot.update(contents=comment)
self.save_annotations()
def delete_bookmark(self):
"""删除书签"""
selected_item = self.bookmark_list.curselection()
if selected_item:
bookmark_name = self.bookmark_list.get(selected_item[0])
del self.bookmarks[bookmark_name]
self.bookmark_list.delete(selected_item)
self.save_settings()
# 目录功能
def update_toc(self):
"""更新目录树"""
toc = self.pdf_document.get_toc(simple=False)
self.populate_toc(toc)
def populate_toc(self, toc):
"""填充目录树"""
for level, title, page_num, _rect in toc:
parent_node = "" if level == 1 else self.toc_tree.get_children()[level - 2]
node_id = self.toc_tree.insert(parent_node, "end", text=title, values=(level, title, page_num, _rect))
self.toc_tree.bind("<Double-1>", lambda event: self.on_toc_double_click(event))
def on_toc_double_click(self, event):
"""双击目录项时跳转到对应页面"""
item = self.toc_tree.selection()[0]
_, _, page_num, _ = self.toc_tree.item(item, "values")
self.current_page = page_num - 1
self.show_page()
# 导出功能
def export_current_page(self):
"""导出当前页为图片"""
file_path = filedialog.asksaveasfilename(defaultextension=".png",
filetypes=[("PNG文件", "*.png"), ("所有文件", "*.*")])
if file_path:
page = self.pdf_document[self.current_page]
pix = page.get_pixmap(dpi=int(self.dpi * self.zoom_level)) # Ensure dpi is an integer
pix.save(file_path)
def export_all_pages(self):
"""导出全部页面为图片"""
folder_path = filedialog.askdirectory()
if folder_path:
for i in range(len(self.pdf_document)):
page = self.pdf_document[i]
pix = page.get_pixmap(dpi=int(self.dpi * self.zoom_level)) # Ensure dpi is an integer
output_path = os.path.join(folder_path, f"page_{i + 1}.png")
pix.save(output_path)
# 设置和持久化功能
def load_initial_data(self):
"""加载初始设置和注释数据"""
self.load_settings()
self.load_annotations()
def save_settings(self):
"""保存设置数据"""
settings = {
"zoom_level": self.zoom_level,
"dpi": self.dpi
}
with open(self.settings_file, "w") as f:
json.dump(settings, f)
def load_settings(self):
"""加载设置数据"""
try:
with open(self.settings_file, "r") as f:
settings = json.load(f)
self.zoom_level = settings.get("zoom_level", 1.0)
self.dpi = settings.get("dpi", 96)
except FileNotFoundError:
pass
def save_annotations(self):
"""保存注释数据"""
annotations = {f"page_{i}": self.annotations.get(i, []) for i in range(len(self.pdf_document))}
with open(self.annotations_file, "w") as f:
json.dump(annotations, f)
def load_annotations(self):
"""加载注释数据"""
try:
with open(self.annotations_file, "r") as f:
annotations = json.load(f)
self.annotations = {int(k.replace("page_", "")): v for k, v in annotations.items()}
except FileNotFoundError:
pass
# 绑定事件
def setup_bindings(self):
"""绑定各种事件"""
self.root.bind("<Control-s>", lambda _: self.export_current_page())
self.root.bind("<Control-Shift-E>", lambda _: self.export_all_pages())
self.root.bind("<Command-s>", lambda _: self.export_current_page() if platform.system() == "Darwin" else "")
self.root.bind("<Command-Shift-E>", lambda _: self.export_all_pages() if platform.system() == "Darwin" else "")
self.root.bind("<Return>", lambda _: self.search_text())
self.root.bind("<F3>", lambda _: self.next_search_result())
self.root.bind("<Shift-F3>", lambda _: self.prev_search_result())
# 辅助函数
def show_error_message(self, message):
"""显示错误消息框"""
messagebox.showerror("错误", message)
def show_info_message(self, message):
"""显示信息消息框"""
messagebox.showinfo("提示", message)
if __name__ == "__main__":
root = tb.Window(themename="cosmo" if platform.system() == "Darwin" else "flatly")
app = MacPDFExpert(root)
root.mainloop()