Résolu le 1-04-25 Interface graphique en python pour pdftk-java à disposition

Postez ici vos scripts Bash, Python, C++, etc...
Répondre
Avatar du membre
herve
Messages : 35
Enregistré le : mer. 3 oct. 2018 20:57

Interface graphique en python pour pdftk-java à disposition

Message par herve »

Bonjour,

J'ai voulu m'amuser à tester les IA Gemini 2.5 Pro et Grok3 au codage. Pour cela, j'ai demandé une GUI pour pdftk :P ...
Il n'y a eu beaucoup de rectifications à faire faire, les IA se sont permis de bonne initiatives... Impressionnant !
Le programme m'a plu, je le met à dispo, desfois que ça aide quelqu'un ou que cela donne des idées ou que quelqu'un ait envie d'y apporter des améliorations... :)
Voici les recommandations pour qu'il fonctionne :
Tkinter: Généralement inclus avec Python, mais si ce n'est pas le cas :

Code : Tout sélectionner

sudo apt update && sudo apt install python3-tk
pdftk: L'outil en ligne de commande. Selon votre version d'Ubuntu, l'installation peut varier. Une version courante est pdftk-java:

Code : Tout sélectionner

sudo apt update && sudo apt install pdftk-java 
(Si ce paquet n'est pas disponible, vous pourriez avoir besoin de trouver une autre source ou une alternative comme pdftk s'il est disponible dans vos dépôts, ou de le compiler). Vérifiez que la commande pdftk fonctionne dans votre terminal.

Bonne soirée à tous :)
Hervé

Voilà le code du programme :

Code : Tout sélectionner

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import subprocess
import shlex
import os
import platform
import webbrowser

# --- Fonctions Utilitaires ---
def check_pdftk():
    try:
        cmd = "where" if platform.system() == "Windows" else "command -v"
        subprocess.run(f"{cmd} pdftk", shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        messagebox.showerror(
            "Erreur pdftk",
            "L'exécutable 'pdftk' est introuvable.\nAssurez-vous qu'il est installé et accessible dans le PATH système.\nSur Ubuntu/Debian : sudo apt install pdftk-java"
        )
        return False

def run_pdftk_command(command_list, status_label):
    if not command_list:
        status_label.config(text="Erreur: Commande vide.", foreground="red")
        return False, None
    full_command = ["pdftk"] + command_list
    print("Commande exécutée :", " ".join(map(shlex.quote, full_command)))  # Debugging
    status_label.config(text=f"Exécution: {' '.join(map(str, full_command))}", foreground="blue")
    status_label.update_idletasks()

    output_path = None
    try:
        output_index = full_command.index("output")
        if output_index + 1 < len(full_command):
            if full_command[-1].lower() == 'compress' and output_index + 2 == len(full_command) - 1:
                output_path = full_command[output_index + 1]
            elif full_command[output_index + 1].endswith('.pdf') or '.' in os.path.basename(full_command[output_index + 1]):
                output_path = full_command[output_index + 1]
    except ValueError:
        pass

    try:
        result = subprocess.run(full_command, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
        if result.returncode == 0:
            status_label.config(text="Succès!", foreground="green")
            if result.stdout and len(result.stdout.strip()) > 0:
                if "dump_data" in full_command:
                    messagebox.showinfo("Sortie pdftk (dump_data)", result.stdout.strip())
                else:
                    print("Sortie pdftk:", result.stdout.strip())
            return True, output_path
        else:
            error_message = f"Erreur pdftk (code {result.returncode}):\n{result.stderr.strip()}"
            print("--- ERREUR PDFTK ---", error_message, "Commande:", full_command, "--------------------", sep="\n")
            status_label.config(text="Échec pdftk (voir console).", foreground="red")
            messagebox.showerror("Erreur d'exécution pdftk", error_message)
            return False, None
    except FileNotFoundError:
        messagebox.showerror("Erreur", "pdftk non trouvé. Vérifiez l'installation.")
        status_label.config(text="Erreur: pdftk non trouvé.", foreground="red")
        return False, None
    except subprocess.CalledProcessError as e:
        error_message = f"Erreur pdftk (code {e.returncode}):\n{e.stderr.strip()}"
        print("--- ERREUR PDFTK ---", error_message, "Commande:", full_command, "--------------------", sep="\n")
        status_label.config(text="Échec pdftk (voir console).", foreground="red")
        messagebox.showerror("Erreur d'exécution pdftk", error_message)
        return False, None
    except Exception as e:
        messagebox.showerror("Erreur Inattendue", f"Erreur Python: {str(e)}")
        status_label.config(text=f"Erreur: {str(e)}", foreground="red")
        return False, None

# --- Classe Principale ---
class PDFTkApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Interface Graphique pour pdftk v2")
        self.geometry("2300x1500")
        self.configure(bg="#f5f5f5")

        if not check_pdftk():
            self.destroy()
            return

        self.input_files = []
        self.output_file = tk.StringVar()
        self.operation = tk.StringVar(value="merge")
        self.compress_var = tk.BooleanVar(value=False)
        self.last_successful_output = None

        # --- Style personnalisé ---
        style = ttk.Style(self)
        style.theme_use('clam')
        
        style.configure("Large.TRadiobutton", 
                       font=("Helvetica", 14),
                       padding=10,
                       background="#f5f5f5")
        style.map("Large.TRadiobutton",
                 background=[('selected', '#4CAF50'), ('!selected', '#f5f5f5')],
                 foreground=[('selected', 'white'), ('!selected', 'black')])
        
        style.configure("TLabel", padding=6, background="#f5f5f5", font=("Helvetica", 11))
        style.configure("TButton", padding=8, font=("Helvetica", 11))
        style.configure("TFrame", padding=15, background="#f5f5f5")
        style.configure("TLabelframe", padding=15, background="#f5f5f5")
        style.configure("TLabelframe.Label", font=("Helvetica", 12, "bold"), foreground="#2c3e50")
        style.configure("Status.TLabel", background="#e8e8e8", relief=tk.SUNKEN, padding=6, font=("Helvetica", 10))

        # --- Cadre Principal ---
        main_frame = ttk.Frame(self)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)

        # --- Cadre Fichiers d'Entrée ---
        input_frame = ttk.LabelFrame(main_frame, text="Fichiers d'Entrée PDF")
        input_frame.pack(fill=tk.BOTH, pady=(0, 15), expand=True)

        self.input_listbox = tk.Listbox(input_frame, selectmode=tk.EXTENDED, height=10, bg="white", fg="#333333", font=("Helvetica", 11))
        self.input_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))

        scrollbar = ttk.Scrollbar(input_frame, orient=tk.VERTICAL, command=self.input_listbox.yview)
        scrollbar.pack(side=tk.LEFT, fill=tk.Y)
        self.input_listbox.config(yscrollcommand=scrollbar.set)

        input_buttons_frame = ttk.Frame(input_frame)
        input_buttons_frame.pack(side=tk.LEFT, padx=10)
        buttons = [
            ("Ajouter...", self.add_input_files),
            ("Supprimer Sel.", self.remove_selected_files),
            ("Vider Liste", self.clear_input_files),
            ("Monter", self.move_up),
            ("Descendre", self.move_down)
        ]
        for text, cmd in buttons:
            ttk.Button(input_buttons_frame, text=text, command=cmd).pack(fill=tk.X, pady=4, padx=2)

        # --- Cadre Opérations ---
        op_frame = ttk.LabelFrame(main_frame, text="Opérations pdftk")
        op_frame.pack(fill=tk.BOTH, pady=15)

        operations = [
            ("Fusionner (merge)", "merge"), ("Diviser (burst)", "burst"),
            ("Supprimer/Réorganiser Pages (cat)", "cat_modify"), ("Rotation pages (rotate)", "rotate"),
            ("Ajouter Filigrane/Fond (background)", "background"), ("Ajouter Tampon (stamp)", "stamp"),
            ("Décrypter (decrypt)", "decrypt"), ("Crypter (encrypt)", "encrypt"),
            ("Extraire métadonnées (dump_data)", "dump_data"), ("Mettre à jour métadonnées (update_info)", "update_info"),
            ("Attacher fichiers (attach)", "attach"), ("Extraire fichiers attachés (unpack_files)", "unpack_files")
        ]
        for i, (text, value) in enumerate(operations):
            ttk.Radiobutton(op_frame, text=text, variable=self.operation, value=value, 
                          command=self.update_options_ui, style="Large.TRadiobutton").grid(
                          row=i//3, column=i%3, sticky="w", padx=10, pady=6)

        # --- Cadre Options Spécifiques ---
        self.options_frame = ttk.LabelFrame(main_frame, text="Options de l'Opération")
        self.options_frame.pack(fill=tk.BOTH, pady=15, expand=True)

        # --- Cadre Sortie ---
        output_frame = ttk.LabelFrame(main_frame, text="Sortie & Options Générales")
        output_frame.pack(fill=tk.X, pady=15)
        ttk.Entry(output_frame, textvariable=self.output_file, width=60, font=("Helvetica", 11)).grid(row=0, column=0, sticky="ew", padx=5, pady=5)
        ttk.Button(output_frame, text="Parcourir...", command=self.select_output).grid(row=0, column=1, padx=5, pady=5)
        ttk.Radiobutton(output_frame, text="Compresser le PDF (si applicable)", variable=self.compress_var, value=True,
                       style="Large.TRadiobutton").grid(row=1, column=0, sticky="w", padx=5, pady=5)
        ttk.Radiobutton(output_frame, text="Ne pas compresser", variable=self.compress_var, value=False,
                       style="Large.TRadiobutton").grid(row=1, column=1, sticky="w", padx=5, pady=5)
        output_frame.columnconfigure(0, weight=1)

        # --- Cadre Exécution ---
        exec_frame = ttk.Frame(main_frame)
        exec_frame.pack(fill=tk.X, pady=15)
        self.status_label = ttk.Label(exec_frame, text="Prêt.", style="Status.TLabel", anchor=tk.W)
        self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
        self.run_button = ttk.Button(exec_frame, text="Exécuter pdftk", command=self.execute_pdftk_operation)
        self.run_button.pack(side=tk.RIGHT, padx=5)
        self.open_button = ttk.Button(exec_frame, text="Ouvrir Résultat", state=tk.DISABLED, command=self.open_result_file)
        self.open_button.pack(side=tk.RIGHT, padx=5)

        self.update_options_ui()
        self.reset_status()

    # --- Méthodes de gestion de la liste ---
    def move_up(self):
        selected = self.input_listbox.curselection()
        if not selected or selected[0] == 0:
            return
        for i in selected:
            item = self.input_listbox.get(i)
            self.input_listbox.delete(i)
            self.input_listbox.insert(i-1, item)
            self.input_listbox.selection_set(i-1)

    def move_down(self):
        selected = self.input_listbox.curselection()
        if not selected or selected[-1] == self.input_listbox.size() - 1:
            return
        for i in reversed(selected):
            item = self.input_listbox.get(i)
            self.input_listbox.delete(i)
            self.input_listbox.insert(i+1, item)
            self.input_listbox.selection_set(i+1)

    def reset_status(self):
        self.status_label.config(text="Prêt.", foreground="grey")
        self.open_button.config(state=tk.DISABLED)
        self.last_successful_output = None

    def update_status(self, message, color):
        self.status_label.config(text=message, foreground=color)
        if color != "green":
            self.open_button.config(state=tk.DISABLED)
            self.last_successful_output = None

    def add_input_files(self):
        files = filedialog.askopenfilenames(
            title="Sélectionner PDFs",
            filetypes=[("Fichiers PDF", "*.pdf"), ("Tous", "*.*")],
            parent=self
        )
        if files:
            current_files = set(self.input_listbox.get(0, tk.END))
            new_files_added = 0
            for f in files:
                if f not in current_files:
                    self.input_listbox.insert(tk.END, f)
                    new_files_added += 1
            self.update_status(f"{new_files_added} fichier(s) ajouté(s).", "black")
            if self.input_listbox.size() == 1 and not self.output_file.get():
                base, _ = os.path.splitext(files[0])
                suffix = "_fusion" if self.operation.get() == "merge" else f"_{self.operation.get()}"
                self.output_file.set(f"{base}{suffix}.pdf")

    def remove_selected_files(self):
        selected = self.input_listbox.curselection()
        if not selected:
            return
        for i in reversed(selected):
            self.input_listbox.delete(i)
        self.update_status(f"{len(selected)} fichier(s) supprimé(s).", "black")
        if not self.input_listbox.size():
            self.output_file.set("")
            self.reset_status()

    def clear_input_files(self):
        if self.input_listbox.size():
            self.input_listbox.delete(0, tk.END)
            self.output_file.set("")
            self.update_status("Liste vidée.", "black")
            self.reset_status()

    def select_output(self):
        op = self.operation.get()
        initial_dir = os.path.dirname(self.input_listbox.get(0)) if self.input_listbox.size() else os.getcwd()
        initial_file = "resultat.pdf"
        if self.input_listbox.size():
            base, _ = os.path.splitext(os.path.basename(self.input_listbox.get(0)))
            initial_file = f"{base}_{op}.pdf" if op != "merge" else f"{base}_fusion.pdf"
        if op in ["burst", "unpack_files"]:
            output = filedialog.askdirectory(title=f"Sélectionner dossier pour {op}", initialdir=initial_dir)
            if output and op == "burst":
                output = os.path.join(output, "page_")
        elif op == "dump_data":
            output = filedialog.asksaveasfilename(title="Fichier métadonnées", defaultextension=".txt",
                                                filetypes=[("Texte", "*.txt")], initialdir=initial_dir,
                                                initialfile=f"{base}_meta.txt")
        else:
            output = filedialog.asksaveasfilename(title="Fichier PDF sortie", defaultextension=".pdf",
                                                filetypes=[("PDF", "*.pdf")], initialdir=initial_dir,
                                                initialfile=initial_file)
        if output:
            self.output_file.set(output)
            self.update_status("Sortie définie.", "black")
            self.open_button.config(state=tk.DISABLED)

    def select_aux_file(self, target_var, title, filetypes):
        initial_dir = os.path.dirname(self.input_listbox.get(0)) if self.input_listbox.size() else os.getcwd()
        filename = filedialog.askopenfilename(title=title, filetypes=filetypes, initialdir=initial_dir)
        if filename:
            target_var.set(filename)

    def update_options_ui(self):
        for widget in self.options_frame.winfo_children():
            widget.destroy()
        op = self.operation.get()
        self.run_button.config(state=tk.NORMAL)
        self.page_range_var = tk.StringVar(value="1-end")
        self.owner_password_var = tk.StringVar()
        self.user_password_var = tk.StringVar()
        self.aux_file_var = tk.StringVar()
        self.attach_files_var = tk.StringVar()
        self.rotation_var = tk.StringVar(value="Est")
        self.encrypt_strength = tk.StringVar(value="128")
        self.cat_pages_var = tk.StringVar(value="1-end")
        row_num = 0

        if op in ["cat_modify", "rotate", "background", "stamp"]:
            label_text = "Pages à garder/ordonner (ex: 1 3 5-end odd):" if op == "cat_modify" else "Plage de pages (ex: 1-end, 1 3 5-7):"
            ttk.Label(self.options_frame, text=label_text).grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            entry_var = self.cat_pages_var if op == "cat_modify" else self.page_range_var
            ttk.Entry(self.options_frame, textvariable=entry_var, width=40).grid(row=row_num, column=1, sticky="ew", padx=5, pady=5)
            row_num += 1

        if op == "rotate":
            ttk.Label(self.options_frame, text="Direction de rotation:").grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            rot_combo = ttk.Combobox(self.options_frame, textvariable=self.rotation_var, 
                                   values=["Nord", "Sud", "Est", "Ouest"], state="readonly", width=15)
            rot_combo.grid(row=row_num, column=1, sticky="w", padx=5, pady=5)
            row_num += 1

        if op in ["background", "stamp"]:
            file_label = "Fichier Filigrane/Fond PDF:" if op == "background" else "Fichier Tampon PDF:"
            ttk.Label(self.options_frame, text=file_label).grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            ttk.Entry(self.options_frame, textvariable=self.aux_file_var, width=50).grid(row=row_num, column=1, sticky="ew", padx=5, pady=5)
            ttk.Button(self.options_frame, text="Parcourir...", 
                      command=lambda: self.select_aux_file(self.aux_file_var, f"Sélectionner {file_label}", [("PDF", "*.pdf")])).grid(
                      row=row_num, column=2, sticky="e", padx=5, pady=5)
            row_num += 1

        if op == "decrypt":
            ttk.Label(self.options_frame, text="Mot de passe:").grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            ttk.Entry(self.options_frame, textvariable=self.user_password_var, show="*", width=40).grid(row=row_num, column=1, sticky="ew", padx=5, pady=5)
            row_num += 1

        if op == "encrypt":
            ttk.Label(self.options_frame, text="Mot de passe Utilisateur:").grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            ttk.Entry(self.options_frame, textvariable=self.user_password_var, show="*", width=40).grid(row=row_num, column=1, sticky="ew", padx=5, pady=5)
            row_num += 1
            ttk.Label(self.options_frame, text="Mot de passe Propriétaire:").grid(row=row_num, column=0, sticky="w", padx=5, pady=5)
            ttk.Entry(self.options_frame, textvariable=self.owner_password_var, show="*", width=40).grid(row=row_num, column=1, sticky="ew", padx=5, pady=5)
            row_num += 1

        if row_num == 0:
            ttk.Label(self.options_frame, text="Aucune option spécifique.").grid(row=0, column=0, sticky="w", padx=5)
        self.options_frame.columnconfigure(1, weight=1)

    def execute_pdftk_operation(self):
        op = self.operation.get()
        inputs = list(self.input_listbox.get(0, tk.END))
        output = self.output_file.get()
        should_compress = self.compress_var.get()

        if not inputs or not output:
            messagebox.showerror("Erreur", "Ajoutez un fichier d'entrée et spécifiez une sortie.")
            return

        command = []
        input_handles = {filename: chr(ord('A') + i) for i, filename in enumerate(inputs)}
        for filename, handle in input_handles.items():
            command.append(f"{handle}={filename}")

        if op == "merge":
            command.append("cat")
            command.extend(input_handles.values())
            command.append("output")
            command.append(output)
        elif op == "rotate":
            pages = self.page_range_var.get() or "1-end"
            direction_map = {"Nord": "north", "Sud": "south", "Est": "east", "Ouest": "west"}
            direction = direction_map.get(self.rotation_var.get(), "east")
            command = [inputs[0], "cat", f"{pages}{direction}", "output", output]
        elif op == "encrypt":
            if not self.user_password_var.get() and not self.owner_password_var.get():
                messagebox.showerror("Erreur", "Un mot de passe (utilisateur ou propriétaire) est requis pour le cryptage.")
                return
            command = [inputs[0], "output", output]
            if self.user_password_var.get():
                command.extend(["user_pw", shlex.quote(self.user_password_var.get())])
            if self.owner_password_var.get():
                command.extend(["owner_pw", shlex.quote(self.owner_password_var.get())])
            command.append("encrypt_128bit")

        if should_compress and op not in ["burst", "dump_data", "unpack_files"]:
            command.append("compress")

        self.run_button.config(state=tk.DISABLED)
        success, final_output_path = run_pdftk_command(command, self.status_label)
        self.run_button.config(state=tk.NORMAL)
        if success:
            self.last_successful_output = final_output_path or output
            self.open_button.config(state=tk.NORMAL)
            if op != "dump_data":
                messagebox.showinfo("Succès", f"Opération terminée.\nRésultat: {self.last_successful_output}")

    def open_result_file(self):
        if self.last_successful_output and os.path.exists(self.last_successful_output):
            webbrowser.open(self.last_successful_output)
        else:
            messagebox.showwarning("Erreur", "Fichier introuvable.")

if __name__ == "__main__":
    app = PDFTkApp()
    if app.winfo_exists():
        app.mainloop()
OS: Linux Mint 21.3 Virginia, Cinnamon 6Host: ThinkPad T14 Gen 2, Disk: 512G
CPU: Intel i5-1135G7 (8) @ 2.40GHz, GPU: TigerLake-LP GT2 (Iris Xe)
RAM: 32 GiB
Répondre