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 pymupdf # PyMuPDF from PIL import Image, ImageDraw, ImageTk from pypdf import PdfReader, PdfWriter from copienator import configuration from copienator import ( CliError, EvaluationWorkspace, ExitCode, execute, target_parser, workspace_from_target, ) from copienator.platform import launch_pdf_arranger from copienator.copy_errors import marked_copy_paths, clear_copy_error from copienator.orientation import save_provenance, report_path, provenance_path # --- 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, workspace: EvaluationWorkspace | None = None, ) -> list[Path]: workspace = workspace or EvaluationWorkspace(Path(directory)) 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() and not workspace.is_statement_pdf(workspace.root / path.name)] 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 | None, ) -> 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) if generated_path is not None: 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 = pymupdf.open(self.pdf_path) self.original_page_ids = {page.xref: index + 1 for index, page in enumerate(self.doc)} from copienator.commands.automatic_page_splitter import load_decisions saved = load_decisions(self.workspace, self.pdf_path) self.automatic_plans = {r['source_page']: r for r in saved} if saved else {} self.automatic_review = bool(self.automatic_plans) 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 = configuration.PAGE_SPLITTER_KB.get(action, "") return k # Dynamic instructions text instructions = ( f"{fmt('move_left')} / {fmt('move_right')} : Move active line 0.5cm 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" f"{fmt('add_secondary_splitter')}: add secondary line, {fmt('delete_secondary_splitter')}: delete newest (arrows move newest; main line red, active secondary orange)\n" f"'{fmt('reverse_pages')}': reverse page order and restart from the new first page\n" ) if self.automatic_review: instructions += ( "\nPropositions automatiques : la touche suivante valide le choix affiché. " "Croix rouge : page retirée.\n" "Déplacer une ligne reprend le découpage manuel pour cette page." ) 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 = { "add_secondary_splitter": self.add_secondary_splitter, "delete_secondary_splitter": self.delete_secondary_splitter, "move_left": self.move_line_left, "move_right": self.move_line_right, "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, "reverse_pages": self.reverse_pages, "restart_file": self.restart_current_file, "arranger": self.start_arranger, "prev_file": self.go_to_previous_file, } for action, key in configuration.PAGE_SPLITTER_KB.items(): if action in action_map: self.master.bind(key, action_map[action]) # self.master.bind("", self.move_line_left) # self.master.bind("", self.move_line_right) # 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("", 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 self.secondary_splitters = [] origin = getattr(self, 'original_page_ids', {}).get(page.xref, self.current_page_index+1) plan = getattr(self, 'automatic_plans', {}).get(origin) self.current_automatic_outputs = plan['outputs'] if plan else None def _automatic_outputs(self): """Express saved asymmetric crops in the current preview orientation.""" outputs = getattr(self, 'current_automatic_outputs', None) if outputs is None: return None flipped = (self.current_rotation+self.file_rotation+self.global_rotation) % 360 if not flipped: return outputs width, height = self.doc[self.current_page_index].rect.width, self.doc[self.current_page_index].rect.height result = [] for output in outputs: x0,y0,x1,y1 = output['crop_points'] result.append(dict(output, side={'left':'right','right':'left'}.get(output['side'], 'whole'), crop_points=[width-x1,height-y1,width-x0,height-y0])) return result 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"{self.pdf_path.name} — 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 = pymupdf.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 proposed = self._automatic_outputs() if proposed is not None: for output in proposed: x0,y0,x1,y1 = output['crop_points'] box = tuple(v*self.current_zoom for v in (x0,y0,x1,y1)) draw.rectangle(box, outline='green' if output['side']=='left' else 'purple', width=3) if not output['keep']: draw.line((box[0],box[1],box[2],box[3]), fill='red', width=3) draw.line((box[0],box[3],box[2],box[1]), fill='red', width=3) self.page_label.config(text=self.page_label.cget('text')+' — Proposition automatique (croix rouge : page retirée)') else: 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) for index, position in enumerate(self.secondary_splitters): x = position * self.current_zoom active = index == len(self.secondary_splitters) - 1 draw.line([(x, 0), (x, pix.height)], fill="orange" if active else "cyan", width=3) draw.text((x + 5, 8), str(index + 1), fill="orange" if active else "cyan") # --- 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 = pymupdf.open(self.pdf_path) self.original_page_ids = {page.xref: index + 1 for index, page in enumerate(self.doc)} 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 reverse_pages(self, event=None): """Reverse the current document and discard earlier page decisions.""" if self.processing or not len(self.doc): return self.doc.select(list(reversed(range(len(self.doc))))) self.current_page_index = 0 self.page_settings = [] self._initialize_current_page_settings() self.load_page() def add_secondary_splitter(self, event=None): """Start a new line in the widest remaining interval of this page.""" width = self.doc.load_page(self.current_page_index).rect.width self.current_automatic_outputs = None boundaries = sorted({0, width, self.current_line_x, *self.secondary_splitters}) left, right = max(zip(boundaries, boundaries[1:]), key=lambda pair: pair[1] - pair[0]) self.secondary_splitters.append((left + right) / 2) self.load_page() def delete_secondary_splitter(self, event=None): if self.secondary_splitters: self.secondary_splitters.pop() self.load_page() def _move_active_line(self, delta): self.current_automatic_outputs = None width = self.doc.load_page(self.current_page_index).rect.width if self.secondary_splitters: self.secondary_splitters[-1] = min(width, max(0, self.secondary_splitters[-1] + delta)) else: self.current_line_x = min(width, max(0, self.current_line_x + delta)) self.load_page() def move_line_left(self, event=None): self._move_active_line(-CM_TO_POINTS / 2) def move_line_right(self, event=None): self._move_active_line(CM_TO_POINTS / 2) 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=None): """Saves the settings for the current page and moves to the next.""" proposed = self._automatic_outputs() if keep is None: sides = {o['side'] for o in proposed if o['keep']} if proposed is not None else {'left','right'} keep = ('as_is' if 'whole' in sides else 'both' if len(sides)==2 else next(iter(sides)) if sides else 'none') self.page_settings.append({ "line_x": self.current_line_x, "secondary_splitters": list(self.secondary_splitters), "rotation": self.current_rotation, "keep": keep, "automatic_outputs": proposed, }) 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: if all(s['keep']=='none' for s in self.page_settings): self.doc.close() output = commit_processed_pdf(self.workspace, self.pdf_path, None) provenance_path(self.workspace, output.stem).unlink(missing_ok=True) else: self.split_pdf() self.reorder_pdfs() self.concate_files() output = commit_processed_pdf(self.workspace, self.pdf_path, self.final_file) save_provenance(self.workspace, output, self.output_origins) clear_copy_error(self.workspace, output, source='orientation') clear_copy_error(self.workspace, output, source='page_splitter') report_path(self.workspace, output.stem).unlink(missing_ok=True) 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) self.fragment_origins = {} for i, settings in enumerate(self.page_settings): page = self.doc.load_page(i) origin = getattr(self, 'original_page_ids', {}).get(page.xref, i + 1) line_x = settings['line_x'] rotation_settings = settings['rotation'] keep = settings['keep'] # Normalize the displayed page without losing an offset CropBox. # This matches the automatic export's coordinate system. source_rotation = page.rotation width, height = page.rect.width, page.rect.height normalized = pymupdf.open() flat = normalized.new_page(width=width, height=height) page.set_rotation(0) if page.get_contents(): flat.show_pdf_page(flat.rect, self.doc, i, rotate=-source_rotation) flat.set_rotation((rotation_settings+self.file_rotation+self.global_rotation) % 360) flat.remove_rotation() sides = [] if keep == "as_is": sides.append((0, width, self.split_filename_left(i))) elif settings.get('automatic_outputs') is not None: for output in settings['automatic_outputs']: side = output['side'] if keep == 'both' or keep == side: x0,_,x1,_ = output['crop_points'] filename = self.split_filename_left(i) if side=='left' else self.split_filename_right(i) sides.append((x0,x1,filename)) else: if keep in {"both", "left"} and line_x > 0: sides.append((0, line_x, self.split_filename_left(i))) if keep in {"both", "right"} and line_x < width: sides.append((line_x, width, self.split_filename_right(i))) for left, right, output_path in sides: cuts = settings.get("secondary_splitters", []) boundaries = [left, *sorted({x for x in cuts if left < x < right}), right] # Each side is a multi-page PDF, keeping its subpages together # when the existing booklet ordering moves this side. origins = [] with pymupdf.open() as output: for x0, x1 in zip(boundaries, boundaries[1:]): clip = pymupdf.Rect(x0, 0, x1, height) target = output.new_page(width=clip.width, height=clip.height) if flat.get_contents(): target.show_pdf_page(target.rect, normalized, 0, clip=clip) origins.append({'origin': origin, 'crop_points': list(clip)}) output.save(output_path) self.fragment_origins[str(output_path)] = origins normalized.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 def copy_side(source, destination): if os.path.isfile(source): shutil.copy2(source, destination) if hasattr(self, 'fragment_origins'): self.fragment_origins[str(destination)] = self.fragment_origins[str(source)] 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 not getattr(self, 'automatic_review', False) and 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"]: copy_side(self.split_filename_right(i), self.reorder_filename(ri)) ri += 1 # 2. Intérieur Gauche if ps[i+1]['keep'] in ["both", "left"]: copy_side(self.split_filename_left(i+1), self.reorder_filename(ri)) ri += 1 # 3. Intérieur Droit if ps[i+1]['keep'] in ["both", "right"]: copy_side(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"]: copy_side(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"]: copy_side(self.split_filename_left(i), self.reorder_filename(ri)) ri += 1 if psk in ["right", "both"]: copy_side(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"): # copy_side(self.split_filename_right(i), self.reorder_filename(ri)) # ri += 1 # if ps[i+1]['keep'] != "none": # copy_side(self.split_filename_left(i+1), self.reorder_filename(ri)) # ri += 1 # if ps[i+1]['keep'] != "left": # copy_side(self.split_filename_right(i+1), self.reorder_filename(ri)) # ri += 1 # if ps[i]['keep'] == "both": # copy_side(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": # copy_side(self.split_filename_left(i), self.reorder_filename(ri)) # ri += 1 # if psk == "right" or psk == "both": # copy_side(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 ) self.output_origins = [] for pdf in pdf_files: self.output_origins.extend(getattr(self, 'fragment_origins', {}).get(str(pdf), [])) 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) if workspace.is_statement_pdf(workspace.root / target.name): raise CliError(f"Target is the statement PDF, not a copy: {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, workspace) 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, workspace) if target == workspace.root and not candidates: candidates = list_pdf_files(workspace.original_copies_dir, workspace) return list(reversed(candidates)) def run(workspace: EvaluationWorkspace, target: Path, *, marked: bool = False, automatic: bool = False, workers: int = 4) -> ExitCode: inputs = list(reversed(marked_copy_paths(workspace, originals=True))) if marked else _selected_inputs(workspace, target) if automatic: from copienator.commands.automatic_page_splitter import run as run_automatic return run_automatic(workspace, list(reversed(inputs)), workers=workers) 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: parser = target_parser("Interactively split and reorder scanned PDF pages") parser.add_argument("--marked", action="store_true", help="Process only copies flagged during margin review") parser.add_argument("--automatic", action="store_true", help="Split automatically and save editable proposals without reordering") parser.add_argument("--workers", type=int, default=4, help="Copies analysed simultaneously in automatic mode") return parser 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, marked=args.marked, automatic=args.automatic, workers=args.workers) return execute(parser, argv, handle) if __name__ == "__main__": raise SystemExit(main())