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()
相关推荐
计算机视觉-Archer1 小时前
Office Word高质量导出pdf(Word 2010版本)
pdf·word
AESA相控阵3 小时前
texstudio: 编辑器显示行号+给PDF增加行号
pdf·编辑器·latex技巧
心灵宝贝6 小时前
用ABBYY PDF Transformer+对PDF的创建编辑转换和注释等操作
pdf
2501_9057013020 小时前
清华大学出品《DeepSeek从入门到精通》超详细使用手册pdf
人工智能·pdf
Nine eight seven four1 天前
pdf修改内容:分享5款好用的工具
pdf
oh,huoyuyan1 天前
火语言RPA--PDF转Word
pdf·word·rpa
CodeCraft Studio1 天前
PDF处理控件Aspose.PDF,如何实现企业级PDF处理
java·python·pdf
inxunoffice1 天前
批量将 Excel 转换 PDF/Word/CSV以及图片等其它格式
pdf·word·excel
开开心心就好1 天前
能一站式搞定远程操作需求的实用工具
java·服务器·python·spring·pdf·电脑·软件