662 lines
25 KiB
Python
662 lines
25 KiB
Python
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 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: 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())
|