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()
相关推荐
cosinmz14 小时前
PDF 发票合并经验分享:月初高效整理发票的实用方法
经验分享·小程序·pdf·pdf转换·pdf发票合并·发票合并打印
一个博客14 小时前
pdf-viewer 实现预览pdf文件
开发语言·javascript·pdf
代码小库14 小时前
免费在线简历工具「面试帮」——18 款模板 + PDF 导出
面试·职场和发展·pdf
庖丁AI14 小时前
文档比对工具怎么选?Word、PDF、扫描件差异检测思路
pdf·word·扫描件·文档比对
asdzx671 天前
使用 Python 快速提取 PDF 中的表格
python·pdf
南风微微吹1 天前
2026英语六级作文模版万能句子PDF电子版
pdf·英语六级
又是被bug折磨的一天1 天前
对多个pdf合同文件批量命名
pdf
南风微微吹1 天前
2026年英语四级作文模版万能句子PDF电子版
pdf·英语四级
这是个假程序员1 天前
PDF分色、智能PDF黑彩识别工具
pdf
夜勤月1 天前
HarmonyOS 6.0 ArkWeb实战:PDF背景色自定义功能全解析(附完整代码+避坑指南)
华为·pdf·harmonyos