Standardisation 6

This commit is contained in:
2026-08-20 15:01:17 +02:00
parent 644e287586
commit aa40e58dd1
4 changed files with 419 additions and 191 deletions
+171 -110
View File
@@ -1,9 +1,15 @@
from __future__ import annotations
import argparse
import glob
import os
import re
import shutil
import sys
import tempfile
import tkinter as tk
import uuid
from collections.abc import Sequence
from pathlib import Path
from tkinter import messagebox
import fitz # PyMuPDF
@@ -11,16 +17,78 @@ from PIL import Image, ImageDraw, ImageTk
from pypdf import PdfReader, PdfWriter
from config import PAGE_SPLITTER_KB
from copienator import (
CliError,
EvaluationWorkspace,
ExitCode,
execute,
target_parser,
workspace_from_target,
)
from platform_utils import launch_pdf_arranger
# --- Constants ---
# Conversion factor: 1 cm to points (1 inch = 2.54 cm, 72 points = 1 inch)
CM_TO_POINTS = (1 / 2.54) * 72
def list_pdf_files(directory):
l = list(reversed(sorted(glob.glob(os.path.join(directory, "*.pdf")))))
return [u for u in l if "enonce" not in u]
def list_pdf_files(directory: str | Path) -> list[Path]:
paths = sorted(Path(directory).glob("*.pdf"), key=lambda path: path.name.casefold())
return [path for path in paths if "enonce" not in path.name.casefold()]
def _temporary_sibling(path: Path, purpose: str) -> Path:
return path.with_name(f".{path.name}.{purpose}.{uuid.uuid4().hex}.tmp")
def commit_processed_pdf(
workspace: EvaluationWorkspace,
original_path: Path,
generated_path: Path,
) -> Path:
"""Commit a processed copy and its original backup with rollback."""
backup_path = workspace.original_copies_dir / original_path.name
output_path = workspace.copies_dir / original_path.name
workspace.original_copies_dir.mkdir(parents=True, exist_ok=True)
workspace.copies_dir.mkdir(parents=True, exist_ok=True)
staged_backup = None
if original_path.resolve() != backup_path.resolve():
staged_backup = _temporary_sibling(backup_path, "new-original")
shutil.copy2(original_path, staged_backup)
saved_backup = _temporary_sibling(backup_path, "old-original")
saved_output = _temporary_sibling(output_path, "old-output")
backup_replaced = False
output_replaced = False
try:
if staged_backup is not None:
if backup_path.exists():
backup_path.replace(saved_backup)
staged_backup.replace(backup_path)
backup_replaced = True
if output_path.exists():
output_path.replace(saved_output)
generated_path.replace(output_path)
output_replaced = True
if original_path.resolve() not in {
backup_path.resolve(),
output_path.resolve(),
}:
original_path.unlink()
except Exception:
if output_replaced and output_path.exists():
output_path.unlink()
if saved_output.exists():
saved_output.replace(output_path)
if backup_replaced and backup_path.exists():
backup_path.unlink()
if saved_backup.exists():
saved_backup.replace(backup_path)
raise
finally:
for temporary in (staged_backup, saved_backup, saved_output):
if temporary is not None and temporary.exists():
temporary.unlink()
return output_path
class PDFPreviewer:
@@ -30,26 +98,36 @@ class PDFPreviewer:
return False
self.pdf_path = self.inputs.pop()
self.file_rotation = 0
self.base_name = os.path.splitext(os.path.basename(self.pdf_path))[0]
self.split_dir = f"{self.base_name}_split"
self.reorder_dir = f"{self.base_name}_reorder"
# Create a temporary output file
self.final_file = f"{self.base_name}_temp.pdf"
self.base_name = self.pdf_path.stem
self._temporary_directory = tempfile.TemporaryDirectory(
prefix=f".{self.base_name}.page-splitter.",
dir=self.workspace.root,
)
working_dir = Path(self._temporary_directory.name)
self.split_dir = working_dir / "split"
self.reorder_dir = working_dir / "reorder"
self.final_file = working_dir / f"{self.base_name}.pdf"
self.current_page_index = 0
self.page_settings = []
self.processing = False # Flag to prevent multiple finish calls
try:
self.doc = fitz.open(self.pdf_path)
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
self.failed = True
self._temporary_directory.cleanup()
messagebox.showerror("Error", f"Failed to open PDF file: {e}")
self.master.destroy()
return
self.master.title(f"PDF Splitter - {os.path.basename(self.pdf_path)}")
self.master.title(f"PDF Splitter - {self.pdf_path.name}")
return True
def __init__(self, master, path):
def __init__(
self,
master: tk.Tk,
workspace: EvaluationWorkspace,
inputs: list[Path],
) -> None:
"""
Initializes the application.
@@ -57,40 +135,16 @@ class PDFPreviewer:
master (tk.Tk): The root Tkinter window.
pdf_path (str): The path to the input PDF file.
"""
if not os.path.exists(path):
messagebox.showerror("Error", f"File not found: {path}")
master.destroy()
return
if os.path.isdir(path):
self.inputs = list_pdf_files(path)
else:
# Check for existing original in backup and restore if found
dir_name = os.path.dirname(os.path.abspath(path))
file_name = os.path.basename(path)
if os.path.basename(dir_name) == "Copies":
dir_name = os.path.dirname(dir_name)
path = os.path.join(dir_name, file_name)
backup_path = os.path.join(dir_name, "Copies Originales", file_name)
if os.path.exists(backup_path):
try:
shutil.move(backup_path, path)
print(f"Restored original file from: {backup_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to restore original file: {e}")
master.destroy()
return
self.inputs = [path]
self.workspace = workspace
self.inputs = inputs
self.output_dir = None
self.master = master
self.num = 0
self.global_rotation = 0 # Rotation appliquée à tous les fichiers
self.history = []
self.failed = False
if not self.setup_next_file():
print(f"Aucun fichier PDF valide trouvé dans : {path}")
print(f"No PDF files found in {workspace.root}")
master.destroy()
return
@@ -249,7 +303,7 @@ class PDFPreviewer:
# Re-open the file from disk to reset changes (like moved pages)
try:
self.doc = fitz.open(self.pdf_path)
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
messagebox.showerror("Error", f"Failed to reopen PDF file: {e}")
self.master.destroy()
return
@@ -328,7 +382,8 @@ class PDFPreviewer:
self._initialize_current_page_settings()
self.load_page()
else:
self.finish_and_process()
if not self.finish_and_process():
return
self.history.append(self.pdf_path)
if self.setup_next_file():
self._initialize_current_page_settings()
@@ -336,59 +391,24 @@ class PDFPreviewer:
else:
self.master.destroy()
def finish_and_process(self):
"""Starts the PDF splitting process and moves files."""
self.split_pdf()
# print("Debug : ", self.page_settings)
# input("Splitting done. Continue ?")
self.reorder_pdfs()
# input("Reorder done. Continue ?")
self.concate_files()
# Logic to move original to backup and replace with new file
def finish_and_process(self) -> bool:
"""Render and transactionally install the processed PDF."""
try:
abs_path = os.path.abspath(self.pdf_path)
dir_name = os.path.dirname(abs_path)
file_name = os.path.basename(abs_path)
backup_dir = os.path.join(dir_name, "Copies Originales")
copies_dir = os.path.join(dir_name, "Copies")
os.makedirs(backup_dir, exist_ok=True)
os.makedirs(copies_dir, exist_ok=True)
backup_path = os.path.join(backup_dir, file_name)
copies_path = os.path.join(copies_dir, file_name)
# Remove backup if it already exists (overwrite)
if os.path.exists(backup_path):
os.remove(backup_path)
# Move the original file to "Copies Originales"
shutil.move(self.pdf_path, backup_path)
# Move the temp output file to replace the original
shutil.move(self.final_file, copies_path)
# print(f"Original moved to {backup_path}, new file saved at {self.pdf_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to move/replace files: {e}")
self.remove_dirs()
def _restore_original(self, path):
"""Restores the original file from the 'Copies Originales' backup."""
dir_name = os.path.dirname(os.path.abspath(path))
file_name = os.path.basename(path)
backup_path = os.path.join(dir_name, "Copies Originales", file_name)
if os.path.exists(backup_path):
try:
# Moving overwrites the generated PDF with the original backup
shutil.move(backup_path, path)
print(f"Restored original file from: {backup_path}")
except Exception as e:
print(f"Failed to restore original file: {e}")
self.split_pdf()
self.reorder_pdfs()
self.concate_files()
commit_processed_pdf(self.workspace, self.pdf_path, self.final_file)
except Exception as exc: # noqa: BLE001 - interactive boundary
self.failed = True
self.processing = False
print(f"Failed to process {self.pdf_path}: {exc}")
messagebox.showerror("Error", f"Failed to process PDF: {exc}")
self._temporary_directory.cleanup()
self.master.destroy()
return False
else:
self._temporary_directory.cleanup()
return True
def go_to_previous_file(self, event=None):
"""Goes back to the beginning of the previously completed file."""
@@ -398,14 +418,16 @@ class PDFPreviewer:
# Close the currently open document to avoid lock issues
if hasattr(self, 'doc'):
self.doc.close()
if hasattr(self, "_temporary_directory"):
self._temporary_directory.cleanup()
# 1. Push current file back onto the stack so it processes next
self.inputs.append(self.pdf_path)
# 2. Get the previous file, restore its original state, and push to stack
# 2. Reprocess the previous file from its preserved original backup
prev_file = self.history.pop()
self._restore_original(prev_file)
self.inputs.append(prev_file)
backup = self.workspace.original_copies_dir / Path(prev_file).name
self.inputs.append(backup if backup.is_file() else Path(prev_file))
# 3. Reload environment (setup_next_file will pop prev_file back off the stack)
self.setup_next_file()
@@ -426,13 +448,9 @@ class PDFPreviewer:
for pdf in pdf_files:
try:
os.remove(pdf)
except Exception as e:
except OSError as e:
print(f"Error deleting {pdf}: {e}")
def remove_dirs(self):
shutil.rmtree(self.split_dir)
shutil.rmtree(self.reorder_dir)
def split_pdf(self):
"""Splits each page of the PDF according to the saved settings."""
print("Starting PDF processing...")
@@ -588,13 +606,56 @@ class PDFPreviewer:
print(f"Created merged PDF: {self.final_file}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python script_name.py <path_to_pdf_file>")
sys.exit(1)
def _selected_inputs(
workspace: EvaluationWorkspace,
target: Path,
) -> list[Path]:
if target.is_file():
if target.suffix.casefold() != ".pdf":
raise CliError(f"Target is not a PDF: {target}", ExitCode.INVALID_ARGUMENTS)
backup = workspace.original_copies_dir / target.name
return [backup if backup.is_file() else target]
pdf_file_path = sys.argv[1]
directory = target
if target == workspace.copies_dir:
candidates = list_pdf_files(workspace.copies_dir)
candidates = [
(
workspace.original_copies_dir / path.name
if (workspace.original_copies_dir / path.name).is_file()
else path
)
for path in candidates
]
else:
candidates = list_pdf_files(directory)
return list(reversed(candidates))
def run(workspace: EvaluationWorkspace, target: Path) -> ExitCode:
inputs = _selected_inputs(workspace, target)
if not inputs:
print(f"No PDF files found in {target}")
return ExitCode.SUCCESS
root = tk.Tk()
app = PDFPreviewer(root, pdf_file_path)
application = PDFPreviewer(root, workspace, inputs)
root.mainloop()
return ExitCode.FAILURE if application.failed else ExitCode.SUCCESS
def build_parser() -> argparse.ArgumentParser:
return target_parser("Interactively split and reorder scanned PDF pages")
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
def handle(args: argparse.Namespace) -> ExitCode:
workspace, target = workspace_from_target(args)
return run(workspace, target)
return execute(parser, argv, handle)
if __name__ == "__main__":
raise SystemExit(main())