Skip to content

Commit d81cf4f

Browse files
committed
Add PDF splitting functionality and mode selection to UI
1 parent ce4a2d9 commit d81cf4f

File tree

1 file changed

+209
-32
lines changed

1 file changed

+209
-32
lines changed

main.py

Lines changed: 209 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,27 @@
44

55
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
66
QHBoxLayout, QLabel, QPushButton, QLineEdit,
7-
QFileDialog, QMessageBox, QGroupBox)
7+
QFileDialog, QMessageBox, QGroupBox, QRadioButton,
8+
QButtonGroup)
89
from PyQt6.QtCore import Qt
910
from PyQt6.QtGui import QFont
1011
from PyPDF2 import PdfReader, PdfWriter
1112

1213

14+
class Mode:
15+
"""Represents a PDF processing mode with all associated metadata."""
16+
17+
def __init__(self, name, display_name, section_title, placeholder, help_text,
18+
process_func, output_suffix):
19+
self.name = name
20+
self.display_name = display_name
21+
self.section_title = section_title
22+
self.placeholder = placeholder
23+
self.help_text = help_text
24+
self.process_func = process_func
25+
self.output_suffix = output_suffix
26+
27+
1328
# Stylesheet constants
1429
DARK_THEME = """
1530
QMainWindow, QWidget {
@@ -53,6 +68,19 @@
5368
background-color: #555555;
5469
color: #888888;
5570
}
71+
QRadioButton::indicator {
72+
width: 18px;
73+
height: 18px;
74+
border-radius: 9px;
75+
border: 2px solid #555555;
76+
}
77+
QRadioButton::indicator:checked {
78+
background-color: #0d47a1;
79+
border: 2px solid #0d47a1;
80+
}
81+
QRadioButton::indicator:hover {
82+
border: 2px solid #1565c0;
83+
}
5684
"""
5785

5886

@@ -105,17 +133,81 @@ def trim_pdf(input_path, page_numbers, output_path):
105133
return message
106134

107135

136+
def split_pdf(input_path, chunk_size, output_path):
137+
"""Split a PDF into multiple files with specified chunk size."""
138+
reader = PdfReader(input_path)
139+
total_pages = len(reader.pages)
140+
141+
if chunk_size <= 0:
142+
raise ValueError("Chunk size must be a positive integer.")
143+
144+
# Determine output naming
145+
output_file = Path(output_path)
146+
base_name = output_file.stem
147+
output_dir = output_file.parent
148+
149+
created_files = []
150+
chunk_num = 1
151+
152+
for start_page in range(0, total_pages, chunk_size):
153+
writer = PdfWriter()
154+
end_page = min(start_page + chunk_size, total_pages)
155+
156+
for page_idx in range(start_page, end_page):
157+
writer.add_page(reader.pages[page_idx])
158+
159+
# Generate output filename
160+
output_filename = output_dir / f"{base_name}_part{chunk_num}.pdf"
161+
with open(output_filename, "wb") as f:
162+
writer.write(f)
163+
164+
created_files.append(str(output_filename))
165+
chunk_num += 1
166+
167+
num_chunks = len(created_files)
168+
message = f"Successfully split PDF into {num_chunks} file{'s' if num_chunks > 1 else ''}"
169+
message += f"\n\nCreated {num_chunks} PDF{'s' if num_chunks > 1 else ''} in:\n{output_dir}"
170+
171+
return message
172+
173+
108174
class PDFPageSelectorApp(QMainWindow):
109175
def __init__(self):
110176
super().__init__()
111177
self.input_path = None
112178
self.output_path = None
179+
180+
# Define available modes
181+
self.modes = [
182+
Mode(
183+
name="selection",
184+
display_name="Selection",
185+
section_title="Page Ranges",
186+
placeholder="e.g., 1-3,5,6-9,11",
187+
help_text="Examples: 1-3,5,6-9,11 or 1,3,5 or 10-20",
188+
process_func=self._process_selection,
189+
output_suffix="_trimmed"
190+
),
191+
Mode(
192+
name="split",
193+
display_name="Split",
194+
section_title="Chunk Size",
195+
placeholder="e.g., 10",
196+
help_text="Number of pages per file (greater than 0)",
197+
process_func=self._process_split,
198+
output_suffix="_split"
199+
)
200+
]
201+
202+
# Default to first mode in list
203+
self.current_mode = self.modes[0]
204+
113205
self.init_ui()
114206

115207
def init_ui(self):
116208
self.setWindowTitle("PDF Page Selector")
117-
self.setMinimumSize(400, 500)
118-
self.resize(400, 500)
209+
self.setMinimumSize(400, 580)
210+
self.resize(400, 580)
119211
self.setStyleSheet(DARK_THEME)
120212

121213
central_widget = QWidget()
@@ -134,8 +226,12 @@ def init_ui(self):
134226
# Input section
135227
layout.addWidget(self._create_input_section())
136228

137-
# Page ranges section
138-
layout.addWidget(self._create_page_section())
229+
# Mode selection
230+
layout.addWidget(self._create_mode_section())
231+
232+
# Page ranges section (dynamic based on mode)
233+
self.page_section = self._create_page_section()
234+
layout.addWidget(self.page_section)
139235

140236
# Output section
141237
layout.addWidget(self._create_output_section())
@@ -180,6 +276,26 @@ def _create_input_section(self):
180276
group.setLayout(layout)
181277
return group
182278

279+
def _create_mode_section(self):
280+
group = QGroupBox("Mode")
281+
layout = QHBoxLayout()
282+
283+
self.mode_group = QButtonGroup(self)
284+
285+
# Dynamically create radio buttons for each mode
286+
for i, mode in enumerate(self.modes):
287+
radio = QRadioButton(mode.display_name)
288+
if i == 0: # Select first mode by default
289+
radio.setChecked(True)
290+
radio.toggled.connect(lambda checked, m=mode: self._on_mode_changed(checked, m))
291+
self.mode_group.addButton(radio)
292+
layout.addWidget(radio)
293+
294+
layout.addStretch()
295+
296+
group.setLayout(layout)
297+
return group
298+
183299
def _create_page_section(self):
184300
group = QGroupBox("Page Ranges")
185301
layout = QVBoxLayout()
@@ -188,13 +304,20 @@ def _create_page_section(self):
188304
self.page_entry.setPlaceholderText("e.g., 1-3,5,6-9,11")
189305
layout.addWidget(self.page_entry)
190306

191-
help_label = QLabel("Examples: 1-3,5,6-9,11 or 1,3,5 or 10-20")
192-
help_label.setStyleSheet("color: #aaaaaa; font-size: 9pt;")
193-
layout.addWidget(help_label)
307+
self.help_label = QLabel("Examples: 1-3,5,6-9,11 or 1,3,5 or 10-20")
308+
self.help_label.setStyleSheet("color: #aaaaaa; font-size: 9pt;")
309+
layout.addWidget(self.help_label)
194310

195311
group.setLayout(layout)
196312
return group
197313

314+
def _update_page_section_for_mode(self):
315+
"""Update the page section UI based on current mode."""
316+
self.page_section.setTitle(self.current_mode.section_title)
317+
self.page_entry.setPlaceholderText(self.current_mode.placeholder)
318+
self.help_label.setText(self.current_mode.help_text)
319+
self.page_entry.clear()
320+
198321
def _create_output_section(self):
199322
group = QGroupBox("Output PDF")
200323
layout = QVBoxLayout()
@@ -221,6 +344,23 @@ def _update_label(self, label, text, active=False):
221344
color = "#ffffff" if active else "#888888"
222345
label.setStyleSheet(f"color: {color}; padding: 5px;")
223346

347+
def _on_mode_changed(self, checked, mode):
348+
"""Handle mode radio button changes."""
349+
if checked:
350+
self.current_mode = mode
351+
self._update_page_section_for_mode()
352+
self._update_output_suggestion()
353+
354+
def _update_output_suggestion(self):
355+
"""Update the output path suggestion based on mode and input."""
356+
if not self.input_path:
357+
return
358+
359+
input_file = Path(self.input_path)
360+
self.output_path = str(input_file.parent / f"{input_file.stem}{self.current_mode.output_suffix}.pdf")
361+
362+
self._update_label(self.output_label, self.output_path, active=True)
363+
224364
def browse_input(self):
225365
filename, _ = QFileDialog.getOpenFileName(
226366
self, "Select Input PDF", "", "PDF files (*.pdf);;All files (*.*)"
@@ -238,9 +378,7 @@ def browse_input(self):
238378
self.page_info_label.setText(f"Total pages: {total_pages}")
239379

240380
# Auto-suggest output path
241-
input_file = Path(filename)
242-
self.output_path = str(input_file.parent / f"{input_file.stem}_trimmed.pdf")
243-
self._update_label(self.output_label, self.output_path, active=True)
381+
self._update_output_suggestion()
244382

245383
self.process_button.setEnabled(True)
246384
except Exception as e:
@@ -252,7 +390,7 @@ def browse_input(self):
252390
def browse_output(self):
253391
if self.input_path:
254392
input_file = Path(self.input_path)
255-
initial = f"{input_file.parent}/{input_file.stem}_trimmed.pdf"
393+
initial = f"{input_file.parent}/{input_file.stem}{self.current_mode.output_suffix}.pdf"
256394
else:
257395
initial = str(Path.home() / "output_trimmed.pdf")
258396

@@ -267,37 +405,76 @@ def browse_output(self):
267405
def process_pdf(self):
268406
page_input = self.page_entry.text().strip()
269407
if not page_input:
270-
QMessageBox.warning(self, "Error", "Please enter page ranges.")
271-
return
272-
273-
try:
274-
page_numbers = parse_page_ranges(page_input)
275-
except (ValueError, AttributeError) as e:
276-
QMessageBox.warning(self, "Error", f"Invalid page format:\n{str(e)}")
408+
# Determine appropriate input type based on current mode
409+
input_type = self.current_mode.section_title.lower()
410+
QMessageBox.warning(self, "Error", f"Please enter {input_type}.")
277411
return
278412

279-
if self.output_path and os.path.exists(self.output_path):
280-
reply = QMessageBox.question(
281-
self, "Confirm Overwrite",
282-
f"File already exists:\n{self.output_path}\n\nOverwrite?",
283-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
284-
)
285-
if reply == QMessageBox.StandardButton.No:
286-
return
287-
288413
self.status_label.setText("Processing...")
289414
self.status_label.setStyleSheet("color: #aaaaaa;")
290415
QApplication.processEvents()
291416

292417
try:
293-
message = trim_pdf(self.input_path, page_numbers, self.output_path)
294-
self.status_label.setText(f"Success! Saved to: {self.output_path}")
295-
self.status_label.setStyleSheet("color: #4caf50;")
296-
QMessageBox.information(self, "Success", message)
418+
# Use the current mode's process function
419+
self.current_mode.process_func(page_input)
297420
except Exception as e:
298421
self.status_label.setText("Failed")
299422
self.status_label.setStyleSheet("color: #f44336;")
300423
QMessageBox.critical(self, "Error", str(e))
424+
425+
def _process_selection(self, page_input):
426+
"""Process PDF in selection mode."""
427+
try:
428+
page_numbers = parse_page_ranges(page_input)
429+
except (ValueError, AttributeError) as e:
430+
raise ValueError(f"Invalid page format:\n{str(e)}")
431+
432+
if self.output_path and os.path.exists(self.output_path):
433+
if not self._confirm_overwrite():
434+
return
435+
436+
message = trim_pdf(self.input_path, page_numbers, self.output_path)
437+
self._show_success(message)
438+
439+
def _process_split(self, chunk_input):
440+
"""Process PDF in split mode."""
441+
try:
442+
chunk_size = int(chunk_input)
443+
if chunk_size <= 0:
444+
raise ValueError("Chunk size must be a positive integer.")
445+
except ValueError:
446+
raise ValueError("Chunk size must be a positive integer.")
447+
448+
# Check if any output files would be overwritten
449+
if self.output_path:
450+
output_file = Path(self.output_path)
451+
base_name = output_file.stem
452+
output_dir = output_file.parent
453+
454+
# Check if part1 exists as a simple overwrite check
455+
first_file = output_dir / f"{base_name}_part1.pdf"
456+
if first_file.exists():
457+
if not self._confirm_overwrite():
458+
return
459+
460+
message = split_pdf(self.input_path, chunk_size, self.output_path)
461+
self._show_success(message)
462+
463+
def _confirm_overwrite(self):
464+
"""Ask user to confirm file overwrite."""
465+
reply = QMessageBox.question(
466+
self, "Confirm Overwrite",
467+
f"Output file(s) already exist.\n\nOverwrite?",
468+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
469+
)
470+
return reply == QMessageBox.StandardButton.Yes
471+
472+
def _show_success(self, message):
473+
"""Display success message."""
474+
output_dir = Path(self.output_path).parent if self.output_path else ""
475+
self.status_label.setText(f"Success! Saved to: {output_dir}")
476+
self.status_label.setStyleSheet("color: #4caf50;")
477+
QMessageBox.information(self, "Success", message)
301478

302479

303480
if __name__ == "__main__":

0 commit comments

Comments
 (0)