import fitz # PyMuPDF import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk, ImageDraw import sys import os import re import glob import shutil from pypdf import PdfReader, PdfWriter # --- 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): return list(reversed(sorted(glob.glob(os.path.join(directory, "*.pdf"))))) 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 = 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.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: 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)}") return True def __init__(self, master, path): """ Initializes the application. Args: 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) 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.output_dir = None self.master = master self.num = 0 self.global_rotation = 0 # Rotation appliquée à tous les fichiers self.setup_next_file() 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") instructions = ( "← / → : Move line 1cm left/right\n" "'c': Rotate page 180°, 'C' : rotate all pages, ',' : rotate all files\n" "t s r n: keep left, next page, keep none, keep right\n" "z: send this page to the end, 'R':restart file\n" ) self.info_label = tk.Label(master, text=instructions, justify=tk.LEFT) self.info_label.pack(pady=5, side=tk.TOP) # self.restart_btn = tk.Button(master, text="Restart File (R)", command=self.restart_current_file) # self.restart_btn.pack(pady=2, 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 --- self.master.bind("", self.move_line_left) self.master.bind("", self.move_line_right) self.master.bind("", 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("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) # New binding # Bind the resize event on the canvas self.canvas.bind("", self.on_resize) self.current_zoom = 1.0 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 Exception 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 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: self.finish_and_process() if self.setup_next_file(): self._initialize_current_page_settings() self.load_page() 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 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") os.makedirs(backup_dir, exist_ok=True) backup_path = os.path.join(backup_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, self.pdf_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 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 Exception 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...") 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 # --- 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) 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) if keep == "both" or keep == "left": 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) 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) if keep == "both" or keep == "right": 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): # 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": 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}") if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python script_name.py ") sys.exit(1) pdf_file_path = sys.argv[1] root = tk.Tk() app = PDFPreviewer(root, pdf_file_path) root.mainloop()