Restructuration de l'application
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import tkinter as tk
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox
|
||||
|
||||
import fitz # PyMuPDF
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from copienator.configuration import PAGE_SPLITTER_KB
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
execute,
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from copienator.platform 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: 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:
|
||||
|
||||
def setup_next_file(self):
|
||||
self.num += 1
|
||||
if len(self.inputs) == 0:
|
||||
return False
|
||||
self.pdf_path = self.inputs.pop()
|
||||
self.file_rotation = 0
|
||||
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 (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 - {self.pdf_path.name}")
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.Tk,
|
||||
workspace: EvaluationWorkspace,
|
||||
inputs: list[Path],
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the application.
|
||||
|
||||
Args:
|
||||
master (tk.Tk): The root Tkinter window.
|
||||
pdf_path (str): The path to the input PDF file.
|
||||
"""
|
||||
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"No PDF files found in {workspace.root}")
|
||||
master.destroy()
|
||||
return
|
||||
|
||||
self._resize_job = None # For debouncing resize events
|
||||
|
||||
self._initialize_current_page_settings()
|
||||
|
||||
# --- UI Setup ---
|
||||
# Set a reasonable initial size for the window
|
||||
self.master.geometry("800x1000")
|
||||
|
||||
def fmt(action):
|
||||
k = PAGE_SPLITTER_KB.get(action, "")
|
||||
return k
|
||||
|
||||
# Dynamic instructions text
|
||||
instructions = (
|
||||
f"{fmt('move_left')} / {fmt('move_right')} : Move line 1cm left/right\n"
|
||||
f"'{fmt('rotate_page')}': Rotate page 180°, '{fmt('rotate_all_pages')}' : rotate all pages, '{fmt('rotate_all_files')}' : rotate all files\n"
|
||||
f"{fmt('keep_left')} {fmt('next_page')} {fmt('discard_page')} {fmt('keep_right')} {fmt('keep_as_is')}: keep left, next page, keep none, keep right, keep as is\n"
|
||||
f"{fmt('send_end')}: send page to end, '{fmt('arranger')}': pdf arranger, '{fmt('restart_file')}': restart file, '{fmt('prev_file')}': previous file\n"
|
||||
)
|
||||
|
||||
self.info_label = tk.Label(master, text=instructions, justify=tk.LEFT)
|
||||
self.info_label.pack(pady=5, side=tk.TOP)
|
||||
|
||||
self.page_label = tk.Label(master, text="", font=("Helvetica", 12))
|
||||
self.page_label.pack(pady=5, side=tk.TOP)
|
||||
|
||||
# Canvas for PDF page preview
|
||||
self.canvas = tk.Canvas(master, bg="gray")
|
||||
self.canvas.pack(fill="both", expand=True)
|
||||
|
||||
# --- Bindings ---
|
||||
action_map = {
|
||||
"move_left": self.move_line_left,
|
||||
"move_right": self.move_line_right,
|
||||
"confirm_next": self.confirm_and_next_page,
|
||||
"rotate_page": self.rotate_page,
|
||||
"rotate_all_pages": self.rotate_all_pages,
|
||||
"rotate_all_files": self.rotate_all_files,
|
||||
"keep_left": self.keep_left,
|
||||
"keep_right": self.keep_right,
|
||||
"keep_as_is": self.keep_as_is,
|
||||
"next_page": self.confirm_and_next_page,
|
||||
"discard_page": self.discard_page,
|
||||
"send_end": self.send_page_end,
|
||||
"restart_file": self.restart_current_file,
|
||||
"arranger": self.start_arranger,
|
||||
"prev_file": self.go_to_previous_file,
|
||||
}
|
||||
|
||||
for action, key in PAGE_SPLITTER_KB.items():
|
||||
if action in action_map:
|
||||
self.master.bind(key, action_map[action])
|
||||
|
||||
# self.master.bind("<Left>", self.move_line_left)
|
||||
# self.master.bind("<Right>", self.move_line_right)
|
||||
# self.master.bind("<Return>", self.confirm_and_next_page)
|
||||
# self.master.bind("c", self.rotate_page)
|
||||
# self.master.bind("C", self.rotate_all_pages)
|
||||
# self.master.bind(",", self.rotate_all_files)
|
||||
# self.master.bind("t", self.keep_left)
|
||||
# self.master.bind("n", self.keep_right)
|
||||
# self.master.bind("m", self.keep_as_is)
|
||||
# self.master.bind("s", self.confirm_and_next_page)
|
||||
# self.master.bind("r", self.discard_page)
|
||||
# self.master.bind("z", self.send_page_end)
|
||||
# self.master.bind("R", self.restart_current_file)
|
||||
# self.master.bind("A", self.start_arranger)
|
||||
# self.master.bind("P", self.go_to_previous_file)
|
||||
|
||||
|
||||
# Bind the resize event on the canvas
|
||||
self.canvas.bind("<Configure>", self.on_resize)
|
||||
|
||||
self.current_zoom = 1.0
|
||||
|
||||
def start_arranger(self):
|
||||
try:
|
||||
launch_pdf_arranger(self.pdf_path)
|
||||
except FileNotFoundError as exc:
|
||||
messagebox.showerror("PDF Arranger", str(exc))
|
||||
|
||||
def on_resize(self, event):
|
||||
"""
|
||||
Handles window resize events by reloading the page.
|
||||
Uses a "debounce" mechanism to avoid excessive redrawing.
|
||||
"""
|
||||
if self._resize_job:
|
||||
self.master.after_cancel(self._resize_job)
|
||||
self._resize_job = self.master.after(250, self.load_page) # Redraw after 250ms of no resizing
|
||||
|
||||
def _initialize_current_page_settings(self):
|
||||
"""Initializes or resets the settings for the current page."""
|
||||
if self.current_page_index < len(self.doc):
|
||||
page = self.doc.load_page(self.current_page_index)
|
||||
self.current_line_x = page.rect.width / 2
|
||||
self.current_rotation = 0
|
||||
|
||||
def load_page(self):
|
||||
"""Loads and displays the current page on the canvas, scaled to fit."""
|
||||
if self.current_page_index >= len(self.doc):
|
||||
if not self.processing:
|
||||
self.processing = True
|
||||
self.finish_and_process()
|
||||
return
|
||||
|
||||
page = self.doc.load_page(self.current_page_index)
|
||||
self.page_label.config(text=f"Page {self.current_page_index + 1} of {len(self.doc)}")
|
||||
|
||||
# --- Calculate Scaling ---
|
||||
canvas_width = self.canvas.winfo_width()
|
||||
canvas_height = self.canvas.winfo_height()
|
||||
|
||||
# Don't try to render if the canvas has no size yet.
|
||||
if canvas_width <= 1 or canvas_height <= 1:
|
||||
return
|
||||
|
||||
page_rect = page.rect
|
||||
zoom_x = canvas_width / page_rect.width
|
||||
zoom_y = canvas_height / page_rect.height
|
||||
# Use 98% of the smallest zoom factor to leave a small margin
|
||||
self.current_zoom = min(zoom_x, zoom_y) * 0.98
|
||||
|
||||
# --- Render Page ---
|
||||
mat = fitz.Matrix(self.current_zoom, self.current_zoom)
|
||||
pix = page.get_pixmap(matrix=mat, alpha=False)
|
||||
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
|
||||
|
||||
# Apply rotation if needed *after* drawing the line
|
||||
if (self.current_rotation + self.file_rotation + self.global_rotation) % 360 != 0:
|
||||
img = img.rotate(180, expand=True)
|
||||
|
||||
# --- Draw Line and Rotate ---
|
||||
draw = ImageDraw.Draw(img)
|
||||
# The line position is scaled by the same zoom factor
|
||||
line_x_scaled = self.current_line_x * self.current_zoom
|
||||
draw.line([(line_x_scaled, 0), (line_x_scaled, pix.height)], fill="red", width=3)
|
||||
|
||||
|
||||
# --- Display on Canvas ---
|
||||
self.photo_img = ImageTk.PhotoImage(img)
|
||||
self.canvas.delete("all")
|
||||
# Center the image on the canvas
|
||||
self.canvas.create_image(canvas_width / 2, canvas_height / 2, anchor="center",
|
||||
image=self.photo_img)
|
||||
|
||||
def restart_current_file(self, event=None):
|
||||
"""Restarts the processing of the current file."""
|
||||
# Close the modified in-memory document
|
||||
if hasattr(self, 'doc'):
|
||||
self.doc.close()
|
||||
|
||||
# Re-open the file from disk to reset changes (like moved pages)
|
||||
try:
|
||||
self.doc = fitz.open(self.pdf_path)
|
||||
except (OSError, RuntimeError, ValueError) as e:
|
||||
messagebox.showerror("Error", f"Failed to reopen PDF file: {e}")
|
||||
self.master.destroy()
|
||||
return
|
||||
|
||||
# Reset state variables for the current file
|
||||
self.file_rotation = 0
|
||||
self.current_page_index = 0
|
||||
self.page_settings = []
|
||||
self.processing = False
|
||||
|
||||
# Reload UI
|
||||
self._initialize_current_page_settings()
|
||||
self.load_page()
|
||||
|
||||
|
||||
def move_line_left(self, event=None):
|
||||
"""Moves the split line to the left."""
|
||||
self.current_line_x = max(0, self.current_line_x - CM_TO_POINTS / 2)
|
||||
self.load_page()
|
||||
|
||||
def move_line_right(self, event=None):
|
||||
"""Moves the split line to the right."""
|
||||
page = self.doc.load_page(self.current_page_index)
|
||||
self.current_line_x = min(page.rect.width, self.current_line_x + CM_TO_POINTS / 2)
|
||||
self.load_page()
|
||||
|
||||
def rotate_page(self, event=None):
|
||||
"""Toggles the page rotation between 0 and 180 degrees."""
|
||||
self.current_rotation = 180 if self.current_rotation == 0 else 0
|
||||
self.load_page()
|
||||
|
||||
def rotate_all_pages(self, event=None):
|
||||
"""Toggles the page rotation between 0 and 180 degrees."""
|
||||
self.file_rotation = 180 if self.file_rotation == 0 else 0
|
||||
self.load_page()
|
||||
|
||||
def rotate_all_files(self, event=None):
|
||||
"""Toggles the page rotation between 0 and 180 degrees."""
|
||||
self.global_rotation = 180 if self.global_rotation == 0 else 0
|
||||
self.load_page()
|
||||
|
||||
def keep_left(self, event=None):
|
||||
self.confirm_and_next_page(keep="left")
|
||||
def keep_right(self, event=None):
|
||||
self.confirm_and_next_page(keep="right")
|
||||
def discard_page(self, event=None):
|
||||
self.confirm_and_next_page(keep="none")
|
||||
def keep_as_is(self, event=None):
|
||||
self.confirm_and_next_page(keep="as_is")
|
||||
def send_page_end(self, event=None):
|
||||
# Do nothing if we are already at or past the last page
|
||||
if self.current_page_index >= len(self.doc) - 1:
|
||||
return
|
||||
|
||||
# Move the current page to the end of the document
|
||||
# -1 as the destination puts it after the last page
|
||||
self.doc.move_page(self.current_page_index, -1)
|
||||
|
||||
# Initialize settings for the page that shifted into the current slot
|
||||
self._initialize_current_page_settings()
|
||||
|
||||
# Reload the canvas to show the new page
|
||||
self.load_page()
|
||||
|
||||
def confirm_and_next_page(self, event=None, keep="both"):
|
||||
"""Saves the settings for the current page and moves to the next."""
|
||||
self.page_settings.append({
|
||||
"line_x": self.current_line_x,
|
||||
"rotation": self.current_rotation,
|
||||
"keep": keep
|
||||
})
|
||||
|
||||
self.current_page_index += 1
|
||||
|
||||
if self.current_page_index < len(self.doc):
|
||||
self._initialize_current_page_settings()
|
||||
self.load_page()
|
||||
else:
|
||||
if not self.finish_and_process():
|
||||
return
|
||||
self.history.append(self.pdf_path)
|
||||
if self.setup_next_file():
|
||||
self._initialize_current_page_settings()
|
||||
self.load_page()
|
||||
else:
|
||||
self.master.destroy()
|
||||
|
||||
def finish_and_process(self) -> bool:
|
||||
"""Render and transactionally install the processed PDF."""
|
||||
try:
|
||||
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."""
|
||||
if not self.history:
|
||||
return # Nowhere to go back to
|
||||
|
||||
# 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. Reprocess the previous file from its preserved original backup
|
||||
prev_file = self.history.pop()
|
||||
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()
|
||||
self._initialize_current_page_settings()
|
||||
self.load_page()
|
||||
|
||||
def split_filename_left(self, i):
|
||||
return os.path.join(self.split_dir, f"{self.base_name}_{i+1}l.pdf")
|
||||
def split_filename_right(self, i):
|
||||
return os.path.join(self.split_dir, f"{self.base_name}_{i+1}r.pdf")
|
||||
def reorder_filename(self, i):
|
||||
return os.path.join(self.reorder_dir, f"{self.base_name}_{i+1}.pdf")
|
||||
|
||||
def clean_up_dir(self, dir, make=True):
|
||||
if make:
|
||||
os.makedirs(dir, exist_ok=True)
|
||||
pdf_files = glob.glob(os.path.join(dir, "*.pdf"))
|
||||
for pdf in pdf_files:
|
||||
try:
|
||||
os.remove(pdf)
|
||||
except OSError as e:
|
||||
print(f"Error deleting {pdf}: {e}")
|
||||
|
||||
def split_pdf(self):
|
||||
"""Splits each page of the PDF according to the saved settings."""
|
||||
print("Starting PDF processing...")
|
||||
self.clean_up_dir(self.split_dir)
|
||||
|
||||
for i, settings in enumerate(self.page_settings):
|
||||
page = self.doc.load_page(i)
|
||||
line_x = settings['line_x']
|
||||
rotation_settings = settings['rotation']
|
||||
keep = settings['keep']
|
||||
rotation = (page.rotation + rotation_settings +
|
||||
self.file_rotation + self.global_rotation) % 360
|
||||
|
||||
if keep == "as_is":
|
||||
doc_full = fitz.open()
|
||||
page_full = doc_full.new_page(width=page.rect.width, height=page.rect.height)
|
||||
page_full.show_pdf_page(page_full.rect, self.doc, i)
|
||||
page_full.set_rotation(rotation)
|
||||
|
||||
output_path_full = self.split_filename_left(i)
|
||||
doc_full.save(output_path_full)
|
||||
doc_full.close()
|
||||
continue # Skip left/right generation
|
||||
|
||||
# --- Create Left Part ---
|
||||
if rotation == 0:
|
||||
rect_left = fitz.Rect(0, 0, line_x, page.rect.height)
|
||||
else:
|
||||
rect_left = fitz.Rect(page.rect.width-line_x, 0, page.rect.width, page.rect.height)
|
||||
|
||||
if (keep == "both" or keep == "left") and line_x > 0:
|
||||
|
||||
doc_left = fitz.open()
|
||||
page_left = doc_left.new_page(width=rect_left.width, height=rect_left.height)
|
||||
page_left.show_pdf_page(page_left.rect, self.doc, i, clip=rect_left)
|
||||
page_left.set_rotation(rotation)
|
||||
|
||||
output_path_left = self.split_filename_left(i)
|
||||
doc_left.save(output_path_left)
|
||||
doc_left.close()
|
||||
|
||||
# --- Create Right Part ---
|
||||
if rotation == 0:
|
||||
rect_right = fitz.Rect(line_x, 0, page.rect.width, page.rect.height)
|
||||
else:
|
||||
rect_right = fitz.Rect(0, 0, page.rect.width-line_x, page.rect.height)
|
||||
if (keep == "both" or keep == "right") and line_x < page.rect.width:
|
||||
doc_right = fitz.open()
|
||||
page_right = doc_right.new_page(width=rect_right.width, height=rect_right.height)
|
||||
page_right.show_pdf_page(page_right.rect, self.doc, i, clip=rect_right)
|
||||
page_right.set_rotation(rotation)
|
||||
output_path_right = self.split_filename_right(i)
|
||||
doc_right.save(output_path_right)
|
||||
doc_right.close()
|
||||
|
||||
self.doc.close()
|
||||
print(f"\nProcessing complete. Files are in '{self.split_dir}' directory.")
|
||||
|
||||
def reorder_pdfs(self):
|
||||
"""Reordonne les pages, si ce sont des copies doubles."""
|
||||
self.clean_up_dir(self.reorder_dir)
|
||||
ps = self.page_settings
|
||||
ri = 0
|
||||
i = 0
|
||||
while i < len(ps):
|
||||
psk = ps[i]['keep']
|
||||
|
||||
# Si c'est une copie double (on s'assure qu'on a bien 2 pages consécutives modifiables)
|
||||
if psk in ["both", "right", "left", "none"] and i < len(ps)-1 and ps[i+1]['keep'] in ["both", "right", "left", "none"]:
|
||||
|
||||
# 1. Page de garde (Extérieur Droit)
|
||||
if ps[i]['keep'] in ["both", "right"]:
|
||||
shutil.copy2(self.split_filename_right(i), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
|
||||
# 2. Intérieur Gauche
|
||||
if ps[i+1]['keep'] in ["both", "left"]:
|
||||
shutil.copy2(self.split_filename_left(i+1), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
|
||||
# 3. Intérieur Droit
|
||||
if ps[i+1]['keep'] in ["both", "right"]:
|
||||
shutil.copy2(self.split_filename_right(i+1), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
|
||||
# 4. Dos de la copie (Extérieur Gauche)
|
||||
if ps[i]['keep'] in ["both", "left"]:
|
||||
shutil.copy2(self.split_filename_left(i), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
|
||||
i += 2
|
||||
else:
|
||||
# Si c'est une page simple (ou as_is)
|
||||
if psk in ["left", "both", "as_is"]:
|
||||
shutil.copy2(self.split_filename_left(i), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
if psk in ["right", "both"]:
|
||||
shutil.copy2(self.split_filename_right(i), self.reorder_filename(ri))
|
||||
ri += 1
|
||||
i += 1
|
||||
# def reorder_pdfs(self):
|
||||
# """Reordonne les pages, si ce sont des copies doubles."""
|
||||
# self.clean_up_dir(self.reorder_dir)
|
||||
# ps = self.page_settings
|
||||
# ri = 0
|
||||
# i = 0
|
||||
# while i < len(ps):
|
||||
# # Si c'est une copie double
|
||||
# if (ps[i]['keep'] == "both" or ps[i]['keep'] == "right") \
|
||||
# and i < len(ps)-1 and (ps[i+1]['keep'] != "right"):
|
||||
# shutil.copy2(self.split_filename_right(i), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# if ps[i+1]['keep'] != "none":
|
||||
# shutil.copy2(self.split_filename_left(i+1), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# if ps[i+1]['keep'] != "left":
|
||||
# shutil.copy2(self.split_filename_right(i+1), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# if ps[i]['keep'] == "both":
|
||||
# shutil.copy2(self.split_filename_left(i), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# i += 2
|
||||
# else:
|
||||
# psk = ps[i]['keep']
|
||||
# if psk == "left" or psk == "both" or psk == "as_is":
|
||||
# shutil.copy2(self.split_filename_left(i), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# if psk == "right" or psk == "both":
|
||||
# shutil.copy2(self.split_filename_right(i), self.reorder_filename(ri))
|
||||
# ri += 1
|
||||
# i += 1
|
||||
|
||||
def concate_files(self):
|
||||
writer = PdfWriter()
|
||||
|
||||
def natural_key(text):
|
||||
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', text)]
|
||||
|
||||
pdf_files = sorted(
|
||||
glob.glob(os.path.join(self.reorder_dir, "*.pdf")),
|
||||
key=natural_key
|
||||
)
|
||||
|
||||
for pdf in pdf_files:
|
||||
reader = PdfReader(pdf)
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
if self.output_dir != None:
|
||||
os.makedirs(os.path.dirname(self.final_file), exist_ok=True)
|
||||
with open(self.final_file, "wb") as f:
|
||||
writer.write(f)
|
||||
print(f"Created merged PDF: {self.final_file}")
|
||||
|
||||
|
||||
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]
|
||||
|
||||
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()
|
||||
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