44
55from PyQt6 .QtWidgets import (QApplication , QMainWindow , QWidget , QVBoxLayout ,
66 QHBoxLayout , QLabel , QPushButton , QLineEdit ,
7- QFileDialog , QMessageBox , QGroupBox )
7+ QFileDialog , QMessageBox , QGroupBox , QRadioButton ,
8+ QButtonGroup )
89from PyQt6 .QtCore import Qt
910from PyQt6 .QtGui import QFont
1011from 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
1429DARK_THEME = """
1530 QMainWindow, QWidget {
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 \n Created { num_chunks } PDF{ 's' if num_chunks > 1 else '' } in:\n { output_dir } "
170+
171+ return message
172+
173+
108174class 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 \n Overwrite?" ,
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 \n Overwrite?" ,
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
303480if __name__ == "__main__" :
0 commit comments