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
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-tkCode : Tout sélectionner
sudo apt update && sudo apt install pdftk-java 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()