feat: GUI multi-formats + fichier unique + textes mis à jour

- Titre : "Pseudonymisation de vos documents"
- Sous-titre, étape 1, paramètres, bouton : textes adaptés
- Choix fichier unique : clic → menu "Dossier / Fichier"
  avec filedialog filtré par formats supportés
- 14 formats supportés : PDF, DOCX, ODT, RTF, TXT, HTML,
  JPEG, PNG, TIFF, BMP

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 09:39:06 +01:00
parent 437877e1c8
commit 63a4a013a2

View File

@@ -85,7 +85,7 @@ except ImportError:
# ---------------------------------------------------------------------------
# Constantes
# ---------------------------------------------------------------------------
APP_TITLE = "Pseudonymisation de PDF"
APP_TITLE = "Pseudonymisation de vos documents"
APP_VERSION = "v5.0"
def _app_dir() -> Path:
@@ -305,6 +305,8 @@ class App:
# --- Contrôle d'arrêt ---
self._stop_requested = False
# --- Fichier unique (None = mode dossier) ---
self._single_file: Optional[Path] = None
# --- Construction UI ---
self._build_ui()
@@ -382,7 +384,7 @@ class App:
tk.Label(
main,
text="Masquez automatiquement les données personnelles de vos documents PDF.",
text="Masquez automatiquement les données personnelles de vos documents.",
font=self._f_body, bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
).pack(fill=tk.X, padx=pad_x, pady=(0, 18))
@@ -414,7 +416,7 @@ class App:
self._folder_text_lbl = tk.Label(
self._folder_inner,
text="Cliquez pour choisir un dossier (tous les PDF seront recherchés récursivement)",
text="Cliquez pour choisir un dossier ou un fichier",
font=self._f_body, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY,
)
self._folder_text_lbl.pack(pady=(4, 0))
@@ -448,7 +450,7 @@ class App:
tk.Label(
info_inner,
text=("\u2022 Recherche récursive de tous les PDF dans les sous-dossiers\n"
text=("\u2022 Recherche récursive de tous les documents dans les sous-dossiers\n"
"\u2022 Sortie PDF Image (raster) — sécurité maximale, aucun texte résiduel\n"
"\u2022 Résultats dans le dossier « anonymise/ » à la racine"),
font=self._f_card_desc, bg=CLR_BLUE_LIGHT, fg=CLR_TEXT_SECONDARY,
@@ -480,7 +482,7 @@ class App:
buttons_frame.pack(fill=tk.X, padx=pad_x, pady=(0, 4))
self.btn_run = tk.Button(
buttons_frame, text="Lancer la pseudonymisation",
buttons_frame, text="Lancer l'anonymisation",
font=self._f_button, bg=CLR_PRIMARY, fg="white",
activebackground="#1d4ed8", activeforeground="white",
relief=tk.FLAT, cursor="hand2", pady=10,
@@ -648,29 +650,71 @@ class App:
# Actions dossier
# ---------------------------------------------------------------
def _browse(self):
"""Propose le choix entre dossier et fichier unique via un menu contextuel."""
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="Choisir un dossier", command=self._browse_folder)
menu.add_command(label="Choisir un fichier", command=self._browse_file)
# Afficher le menu sous le curseur
try:
menu.tk_popup(self.root.winfo_pointerx(), self.root.winfo_pointery())
finally:
menu.grab_release()
def _browse_folder(self):
d = filedialog.askdirectory()
if d:
self._single_file = None
self.dir_var.set(d)
self._update_folder_display()
def _browse_file(self):
try:
from format_converter import SUPPORTED_EXTENSIONS
except ImportError:
SUPPORTED_EXTENSIONS = {".pdf"}
# Construire les filtres pour le dialogue
ext_list = " ".join(f"*{e}" for e in sorted(SUPPORTED_EXTENSIONS))
f = filedialog.askopenfilename(
title="Choisir un document à anonymiser",
filetypes=[
("Documents supportés", ext_list),
("PDF", "*.pdf"),
("Word", "*.docx"),
("Images", "*.jpg *.jpeg *.png *.tiff *.tif *.bmp"),
("Texte", "*.txt *.rtf *.odt *.html *.htm"),
("Tous", "*.*"),
],
)
if f:
self._single_file = Path(f)
self.dir_var.set(str(self._single_file.parent))
self._update_folder_display()
def _update_folder_display(self):
folder = self.dir_var.get()
if not folder:
return
# Compter les documents supportés (récursif)
try:
from format_converter import SUPPORTED_EXTENSIONS
except ImportError:
SUPPORTED_EXTENSIONS = {".pdf"}
pdf_count = 0
try:
pdf_count = len([
p for p in Path(folder).rglob("*")
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
])
except Exception:
pass
is_single = getattr(self, '_single_file', None) is not None
if is_single:
doc_count = 1
display_label = self._single_file.name
else:
# Compter les documents supportés (récursif)
try:
from format_converter import SUPPORTED_EXTENSIONS
except ImportError:
SUPPORTED_EXTENSIONS = {".pdf"}
doc_count = 0
try:
doc_count = len([
p for p in Path(folder).rglob("*")
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
])
except Exception:
pass
display_label = folder
# Vider et reconstruire l'intérieur
for w in self._folder_inner.winfo_children():
@@ -679,8 +723,9 @@ class App:
row = tk.Frame(self._folder_inner, bg=CLR_CARD_BG)
row.pack(fill=tk.X)
icon = "\U0001f4c4" if is_single else "\U0001f4c2" # 📄 ou 📂
tk.Label(
row, text="\U0001f4c2", font=(self._font_family, 16),
row, text=icon, font=(self._font_family, 16),
bg=CLR_CARD_BG,
).pack(side=tk.LEFT, padx=(0, 8))
@@ -688,7 +733,7 @@ class App:
info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Chemin (tronqué si trop long)
display_path = folder
display_path = display_label
if len(display_path) > 60:
display_path = "..." + display_path[-57:]
tk.Label(
@@ -696,9 +741,13 @@ class App:
bg=CLR_CARD_BG, fg=CLR_TEXT, anchor="w",
).pack(fill=tk.X)
suffix = "document trouvé (récursif)" if pdf_count <= 1 else "documents trouvés (récursif)"
if is_single:
subtitle = f"Fichier unique — {self._single_file.suffix.upper().lstrip('.')}"
else:
suffix = "document trouvé (récursif)" if doc_count <= 1 else "documents trouvés (récursif)"
subtitle = f"{doc_count} {suffix}"
tk.Label(
info_frame, text=f"{pdf_count} {suffix}",
info_frame, text=subtitle,
font=self._f_small, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
).pack(fill=tk.X)
@@ -716,31 +765,41 @@ class App:
# Lancement
# ---------------------------------------------------------------
def _run(self):
folder = Path(self.dir_var.get().strip())
if not folder.is_dir():
messagebox.showwarning(
"Dossier invalide",
"Choisissez un dossier contenant des documents.",
)
return
is_single = getattr(self, '_single_file', None) is not None
try:
from format_converter import SUPPORTED_EXTENSIONS
except ImportError:
SUPPORTED_EXTENSIONS = {".pdf"}
pdfs = sorted([
p for p in folder.rglob("*")
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
])
if not pdfs:
exts = ", ".join(sorted(SUPPORTED_EXTENSIONS))
messagebox.showwarning(
"Aucun document",
f"Aucun fichier supporté trouvé.\n"
f"Formats acceptés : {exts}\n"
f"(recherche récursive dans les sous-dossiers)",
)
return
if is_single:
# Mode fichier unique
if not self._single_file.is_file():
messagebox.showwarning("Fichier introuvable", f"{self._single_file}")
return
folder = self._single_file.parent
pdfs = [self._single_file]
else:
# Mode dossier
folder = Path(self.dir_var.get().strip())
if not folder.is_dir():
messagebox.showwarning(
"Dossier invalide",
"Choisissez un dossier ou un fichier.",
)
return
try:
from format_converter import SUPPORTED_EXTENSIONS
except ImportError:
SUPPORTED_EXTENSIONS = {".pdf"}
pdfs = sorted([
p for p in folder.rglob("*")
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
])
if not pdfs:
exts = ", ".join(sorted(SUPPORTED_EXTENSIONS))
messagebox.showwarning(
"Aucun document",
f"Aucun fichier supporté trouvé.\n"
f"Formats acceptés : {exts}\n"
f"(recherche récursive dans les sous-dossiers)",
)
return
self._stop_requested = False
self.btn_run.pack_forget()