Standardisation 6
This commit is contained in:
+171
-110
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user