workflow.py
1 2 # This program is part of 3 # Paul's Preponderating Prepresser v1.1 4 # (CC-BY-SA) 2025 era vulgaris, by 5 # The Rev. Paul T. Fusco-Gessick, J.D., SDA 6 # <<paul@neroots.net>> 7 8 # I.F.E.T. -- I.V.V.S. 9 10 """ 11 Main workflow orchestrator for PDF signature preparation and imposition. 12 Automates the complete process from source PDF to print-ready signatures. 13 """ 14 15 import sys 16 import os 17 import subprocess 18 import PyPDF2 19 import shutil 20 21 from ppp._util import get_data_path 22 23 VALID_SIG_SIZES = [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48] 24 SIG_TABLES = { 25 4: (1,4,5,8,9,12,13,16,17,20,21,24,25,28,29,32,33,36,37,40,41,44,45,48,49,52,53,56,57,60,61,64,65,68,69,72,73,76,77,80,81,84,85,88,89,92,93,96,97,100,101,104,105,108,109,112,113,116,117,120), 26 8: (1,8,9,16,17,24,25,32,33,40,41,48,49,56,57,64,65,72,73,80,81,88,89,96,97,104,105,112,113,120,121,128,129,136,137,144,145,152,153,160,161,168,169,176,177,184,185,192,193,200,201,208,209,216,217,224,225,232,233,240), 27 12: (1,12,13,24,25,36,37,48,49,60,61,72,73,84,85,96,97,108,109,120,121,132,133,144,145,156,157,168,169,180,181,192,193,204,205,216,217,228,229,240,241,252,253,264,265,276,277,288,289,300,301,312,313,324,325,336,337,348,349,360), 28 16: (1,16,17,32,33,48,49,64,65,80,81,96,97,112,113,128,129,144,145,160,161,176,177,192,193,208,209,224,225,240,241,256,257,272,273,288,289,304,305,320,321,336,337,352,353,368,369,384,385,400,401,416,417,432,433,448,449,465,480), 29 20: (1,20,21,40,41,60,61,80,81,100,101,120,121,140,141,160,161,180,181,200,201,220,221,240,241,260,261,280,281,300,301,320,321,340,341,360,361,380,381,400,401,420,421,440,441,460,461,480,481,500,501,520,521,540,541,560,561,580,581,600), 30 24: (1,24,25,48,49,72,73,96,97,120,121,144,145,168,169,192,193,216,217,240,241,264,265,288,289,312,313,336,337,360,361,384,385,408,409,432,433,456,457,480,481,504,505,528,529,552,553,576,577,600,601,624,625,648,649,672,673,696,697,720,721,744,745,768,769,792,793,816,817,840,841,864,865,888,889,912,913,936,937,960), 31 28: (1,28,29,56,57,84,85,112,113,140,141,168,169,196,197,224,225,252,253,280,281,308,309,336,337,364,365,392,393,420,421,448,449,476,477,504,505,532,533,560,561,588,589,616,617,644,645,672,673,700,701,728,729,756,757,784,785,812,813,840,841,868,869,896,897,924,925,952,953,980,981,1008,1009,1036,1037,1064,1065,1092,1093,1120), 32 32: (1,32,33,64,65,96,97,128,129,160,161,192,193,224,225,256,257,288,289,320,321,352,353,384,385,416,417,448,449,480,481,512,513,544,545,576,577,608,609,640,641,672,673,704,705,736,737,768,769,800,801,832,833,864,865,896,897,928,929,960,961,992,993,1024,1025,1056,1057,1088,1089,1120,1121,1152,1153,1184,1185,1216,1217,1248,1249,1280,1281,1312,1313,1344,1345,1376,1377,1408,1409,1440,1441,1472,1473,1504,1505,1536,1537,1568,1569,1600), 33 36: (1,36,37,72,73,108,109,144,145,180,181,216,217,252,253,288,289,324,325,360,361,396,397,432,433,468,469,504,505,540,541,576,577,612,613,648,649,684,685,720,721,756,757,792,793,828,829,864,865,900,901,936,937,972,973,1008,1009,1044,1045,1080,1081,1116,1117,1152,1153,1188,1189,1224,1225,1260,1261,1296,1297,1332,1333,1368,1369,1404,1405,1440,1441,1476,1477,1512,1513,1548,1549,1584,1585,1620,1621,1656,1657,1692,1693,1728,1729,1764,1765,1800), 34 40: (1,40,41,80,81,120,121,160,161,200,201,240,241,280,281,320,321,360,361,400,401,440,441,480,481,520,521,560,561,600,601,640,641,680,681,720,721,760,761,800,801,840,841,880,881,920,921,960,961,1000,1001,1040,1041,1080,1081,1120,1121,1160,1161,1200,1201,1240,1241,1280,1281,1320,1321,1360,1361,1400,1401,1440,1441,1480,1481,1520,1521,1560,1561,1600), 35 44: (1,44,45,88,89,132,133,176,177,220,221,264,265,308,309,352,353,396,397,440,441,484,485,528,529,572,573,616,617,660,661,704,705,748,749,792,793,836,837,880,881,924,925,968,969,1012,1013,1056,1057,1100,1101,1144,1145,1188,1189,1232,1233,1276,1277,1320,1321,1364,1365,1408,1409,1452,1453,1496,1497,1540,1541,1584,1585,1628,1629,1672,1673,1716,1717,1760), 36 48: (1,48,49,96,97,144,145,192,193,240,241,288,289,336,337,384,385,432,433,480,481,528,529,576,577,624,625,672,673,720,721,768,769,816,817,864,865,912,913,960,961,1008,1009,1056,1057,1104,1105,1152,1153,1200,1201,1248,1249,1296,1297,1344,1345,1392,1393,1440,1441,1488,1489,1536,1537,1584,1585,1632,1633,1680,1681,1728,1729,1776,1777,1824,1825,1872,1873,1920) 37 } 38 39 def get_page_count(filename): 40 """Get page count from a PDF file.""" 41 try: 42 with open(filename, 'rb') as f: 43 reader = PyPDF2.PdfReader(f) 44 return len(reader.pages) 45 except Exception as e: 46 print(f'ERROR: Could not read PDF file: {e}') 47 return None 48 49 def suggest_signature_sizes(page_count): 50 """ 51 Suggest optimal signature sizes for a given page count. 52 Prefers 32 and 36 page signatures. Never suggests adding >8 blank pages. 53 For large padding needs, suggests mixed signature sizes instead. 54 55 Returns list of tuples: (description, sig_sizes_list, pages_to_add) 56 where sig_sizes_list is a list of signature sizes (e.g., [32, 32, 32, 28]) 57 """ 58 MAX_PADDING = 8 59 PREFERRED_SIZES = [32, 36] # Preferred for stapling 60 MAX_SIGNATURES = 40 # Don't suggest absurd numbers of signatures 61 MIN_SIG_SIZE = 16 if page_count > 100 else 8 # Use larger sigs for larger docs 62 63 suggestions = [] 64 65 # Try preferred sizes first 66 for size in PREFERRED_SIZES: 67 full_sigs = page_count // size 68 remainder = page_count % size 69 70 if remainder == 0: 71 # Perfect fit 72 desc = f'{full_sigs} × {size}-page signatures' 73 sig_config = [size] * full_sigs 74 suggestions.append((desc, sig_config, 0)) 75 else: 76 # Option A: Pad to make all signatures the same size (if padding <= 8) 77 pages_to_add = size - remainder 78 if pages_to_add <= MAX_PADDING: 79 desc = f'{full_sigs + 1} × {size}-page signatures' 80 sig_config = [size] * (full_sigs + 1) 81 suggestions.append((desc, sig_config, pages_to_add)) 82 83 # Option B: Use smaller final signature 84 # Find best-fitting smaller size for the remainder 85 for final_size in reversed([s for s in VALID_SIG_SIZES if s < size]): 86 if remainder <= final_size: 87 final_padding = final_size - remainder 88 if final_padding <= MAX_PADDING: 89 desc = f'{full_sigs} × {size}-page + 1 × {final_size}-page' 90 sig_config = [size] * full_sigs + [final_size] 91 suggestions.append((desc, sig_config, final_padding)) 92 break # Take the first (largest) that fits 93 94 # Also try other sizes (non-preferred) as fallback 95 for size in [s for s in VALID_SIG_SIZES if s not in PREFERRED_SIZES and s >= MIN_SIG_SIZE]: 96 full_sigs = page_count // size 97 remainder = page_count % size 98 99 # Skip if it would result in too many signatures 100 if full_sigs > MAX_SIGNATURES: 101 continue 102 103 if remainder == 0: 104 desc = f'{full_sigs} × {size}-page signatures' 105 sig_config = [size] * full_sigs 106 suggestions.append((desc, sig_config, 0)) 107 else: 108 pages_to_add = size - remainder 109 if pages_to_add <= MAX_PADDING and (full_sigs + 1) <= MAX_SIGNATURES: 110 desc = f'{full_sigs + 1} × {size}-page signatures' 111 sig_config = [size] * (full_sigs + 1) 112 suggestions.append((desc, sig_config, pages_to_add)) 113 114 # Sort by: 1) least padding, 2) fewest total signatures 115 # Remove duplicates by signature configuration 116 seen_configs = set() 117 unique_suggestions = [] 118 for desc, sig_config, padding in suggestions: 119 config_key = tuple(sig_config) 120 if config_key not in seen_configs: 121 seen_configs.add(config_key) 122 unique_suggestions.append((desc, sig_config, padding)) 123 124 unique_suggestions.sort(key=lambda x: (x[2], len(x[1]))) # Sort by padding, then num signatures 125 126 return unique_suggestions[:5] # Return top 5 127 128 def pad_pdf(source_file, pages_to_add): 129 """Pad a PDF with blank pages, combining padding files if needed.""" 130 131 def find_padding_file(filename): 132 """Look for a padding file in cwd and package data.""" 133 return get_data_path(filename) 134 135 # Try to find exact padding file first 136 exact_file = f'{pages_to_add}pp.pdf' 137 exact_path = find_padding_file(exact_file) 138 139 if exact_path: 140 # Exact file exists, use it 141 padding_files = [exact_path] 142 else: 143 # Build padding from available files (1pp.pdf, 2pp.pdf, 3pp.pdf) 144 # Greedy algorithm: use largest files first 145 available = [(3, '3pp.pdf'), (2, '2pp.pdf'), (1, '1pp.pdf')] 146 padding_files = [] 147 remaining = pages_to_add 148 149 for size, filename in available: 150 while remaining >= size: 151 path = find_padding_file(filename) 152 if not path: 153 print(f'ERROR: Cannot create {pages_to_add} blank pages - missing {filename}') 154 return None 155 padding_files.append(path) 156 remaining -= size 157 158 if remaining > 0: 159 print(f'ERROR: Cannot create {pages_to_add} blank pages with available padding files') 160 return None 161 162 # Create padded filename 163 base, ext = os.path.splitext(source_file) 164 output_file = f'{base}-padded{ext}' 165 166 # Run pdftk with all necessary padding files 167 print(f'Adding {pages_to_add} blank page(s)...') 168 try: 169 # Build pdftk command: A=source, B=pad1, C=pad2, etc. 170 cmd = ['pdftk', f'A={source_file}'] 171 labels = [] 172 for i, pad_file in enumerate(padding_files): 173 label = chr(66 + i) # B, C, D, E, ... 174 cmd.append(f'{label}={pad_file}') 175 labels.append(label) 176 177 # Cat: A B C D ... output 178 cmd.extend(['cat', 'A'] + labels + ['output', output_file]) 179 180 subprocess.run(cmd, check=True, capture_output=True) 181 print(f'Created {output_file}') 182 return output_file 183 except subprocess.CalledProcessError as e: 184 print(f'ERROR: pdftk failed') 185 return None 186 187 def split_into_signatures(source_file, sig_sizes): 188 """ 189 Split PDF into signature files. 190 sig_sizes can be either: 191 - A single integer (uniform signatures) 192 - A list of integers (mixed signature sizes) 193 """ 194 page_count = get_page_count(source_file) 195 if page_count is None: 196 return False 197 198 # Handle both uniform and mixed signatures 199 if isinstance(sig_sizes, int): 200 sig_sizes = [sig_sizes] * (page_count // sig_sizes) 201 202 # Verify total pages match 203 total_pages = sum(sig_sizes) 204 if total_pages != page_count: 205 print(f'ERROR: Signature sizes ({total_pages} pages) do not match file ({page_count} pages)') 206 return False 207 208 print(f'\nSplitting {source_file} into {len(sig_sizes)} signature(s)...') 209 210 current_page = 1 211 for i, sig_size in enumerate(sig_sizes, 1): 212 end_page = current_page + sig_size - 1 213 pagenums = f'{current_page}-{end_page}' 214 outputnum = f'sig{str(i).zfill(2)}.pdf' 215 216 try: 217 subprocess.run(['pdftk', source_file, 'cat', pagenums, 'output', outputnum], 218 check=True, capture_output=True) 219 print(f' Created {outputnum} ({sig_size} pages)') 220 except subprocess.CalledProcessError: 221 print(f' ERROR: Failed to create {outputnum}') 222 return False 223 224 current_page = end_page + 1 225 226 return True 227 228 def impose_signatures(output_dir): 229 """Run singledingle on all signature files and organize output.""" 230 sig_files = sorted([f for f in os.listdir('.') if f.startswith('sig') and f.endswith('.pdf')]) 231 232 if not sig_files: 233 print('No signature files found (sig*.pdf)') 234 return False 235 236 # Clean up any old PPPsig files from previous runs 237 old_imposed = [f for f in os.listdir('.') if f.startswith('PPPsig') and f.endswith('.pdf')] 238 if old_imposed: 239 print('\nCleaning up old imposed files from previous run...') 240 for old_file in old_imposed: 241 os.remove(old_file) 242 print(f' Deleted {old_file}') 243 244 print(f'\nImposing {len(sig_files)} signature(s) for 2-up printing...') 245 246 # Check if singledingle is available 247 singledingle_path = shutil.which('singledingle') 248 if singledingle_path is None: 249 print('ERROR: singledingle command not found in PATH') 250 print('Make sure ppp-prepress is installed: pipx install ppp-prepress') 251 return False 252 253 for sig_file in sig_files: 254 print(f' Imposing {sig_file}...') 255 try: 256 subprocess.run([singledingle_path, sig_file], check=True) 257 except subprocess.CalledProcessError: 258 print(f' ERROR: Failed to impose {sig_file}') 259 return False 260 261 print('\nImposition complete!') 262 263 # Create output directory 264 os.makedirs(output_dir, exist_ok=True) 265 266 # Get list of imposed files 267 imposed_files = sorted([f for f in os.listdir('.') if f.startswith('PPPsig') and f.endswith('.pdf')]) 268 269 if not imposed_files: 270 print('ERROR: No imposed signature files found (PPPsig*.pdf)') 271 return False 272 273 # Move and rename imposed files to output directory 274 print(f'\nOrganizing files into {output_dir}/') 275 for imposed_file in imposed_files: 276 # Extract number: PPPsig01.pdf -> 01.pdf 277 number = imposed_file.replace('PPPsig', '').replace('.pdf', '') 278 new_name = f'{number}.pdf' 279 src = imposed_file 280 dst = os.path.join(output_dir, new_name) 281 shutil.move(src, dst) 282 print(f' {imposed_file} → {output_dir}/{new_name}') 283 284 # Move original signature files to output directory (keep them!) 285 print('\nMoving original signature files...') 286 for sig_file in sig_files: 287 dst = os.path.join(output_dir, sig_file) 288 shutil.move(sig_file, dst) 289 print(f' {sig_file} → {output_dir}/{sig_file}') 290 291 return True 292 293 def combine_signatures(output_dir): 294 """Combine imposed signatures with spacers, limiting to ~200 pages per combined file.""" 295 PAGES_PER_COMBINED = 200 296 297 # Find spacer file (1LTRpp.pdf) 298 spacer_file = get_data_path('1LTRpp.pdf') 299 300 if spacer_file is None: 301 print('WARNING: Spacer file (1LTRpp.pdf) not found. Skipping combination.') 302 return False 303 304 # Get list of imposed signature files in output directory (##.pdf, not sig##.pdf) 305 import re 306 sig_files = sorted([f for f in os.listdir(output_dir) 307 if f.endswith('.pdf') and re.match(r'^\d+\.pdf$', f)]) 308 309 if not sig_files: 310 print('ERROR: No signature files found in output directory') 311 return False 312 313 print(f'\nCombining signatures with spacers (max {PAGES_PER_COMBINED} pages per file)...') 314 315 # Calculate page counts for each signature 316 sig_page_counts = [] 317 for sig_file in sig_files: 318 full_path = os.path.join(output_dir, sig_file) 319 page_count = get_page_count(full_path) 320 if page_count is None: 321 print(f'WARNING: Could not read {sig_file}, skipping') 322 continue 323 sig_page_counts.append((sig_file, page_count)) 324 325 # Get spacer page count (we'll insert it twice for a full double-sided sheet) 326 spacer_pages = get_page_count(spacer_file) 327 if spacer_pages is None: 328 print('ERROR: Could not read spacer file') 329 return False 330 331 spacer_pages_total = spacer_pages * 2 # Insert spacer twice 332 333 # Calculate total pages (including spacers) 334 total_sig_pages = sum(pc for _, pc in sig_page_counts) 335 total_spacer_pages = (len(sig_page_counts) - 1) * spacer_pages_total # n-1 spacers 336 total_pages = total_sig_pages + total_spacer_pages 337 338 # Determine number of batches needed 339 import math 340 num_batches = math.ceil(total_pages / PAGES_PER_COMBINED) 341 target_per_batch = total_pages / num_batches 342 343 # Group signatures into balanced batches 344 batches = [] 345 current_batch = [] 346 current_pages = 0 347 348 for sig_file, page_count in sig_page_counts: 349 # Each signature adds: its pages + spacer pages (except first in batch) 350 pages_to_add = page_count + (spacer_pages_total if current_batch else 0) 351 352 # Check if adding this would: 353 # 1. Exceed the hard limit of 200 pages, OR 354 # 2. Push us significantly over the target (and we have room for another batch) 355 would_exceed_limit = current_pages + pages_to_add > PAGES_PER_COMBINED 356 over_target = current_pages + pages_to_add > target_per_batch * 1.1 # 10% tolerance 357 have_more_batches = len(batches) < num_batches - 1 358 359 if current_batch and (would_exceed_limit or (over_target and have_more_batches)): 360 # Start new batch 361 batches.append(current_batch) 362 current_batch = [(sig_file, page_count)] 363 current_pages = page_count 364 else: 365 current_batch.append((sig_file, page_count)) 366 current_pages += pages_to_add 367 368 if current_batch: 369 batches.append(current_batch) 370 371 print(f' Creating {len(batches)} combined file(s)...') 372 373 # Create combined files 374 for batch_num, batch in enumerate(batches, 1): 375 combined_name = f'job{str(batch_num).zfill(2)}.pdf' 376 combined_path = os.path.join(output_dir, combined_name) 377 378 # Build pdftk command with spacers between signatures 379 cmd = ['pdftk'] 380 381 # Add all files as inputs (A=first sig, B=spacer, C=second sig, etc.) 382 file_labels = [] 383 for i, (sig_file, _) in enumerate(batch): 384 label = chr(65 + i * 2) # A, C, E, G, ... 385 cmd.append(f'{label}={os.path.join(output_dir, sig_file)}') 386 file_labels.append(label) 387 388 # Add spacer as all the even letters (B, D, F, H, ...) 389 for i in range(len(batch) - 1): 390 label = chr(66 + i * 2) # B, D, F, H, ... 391 cmd.append(f'{label}={spacer_file}') 392 393 # Build cat sequence: A B B C D D E ... (sig, spacer twice, sig, spacer twice, sig) 394 cmd.append('cat') 395 for i, label in enumerate(file_labels): 396 cmd.append(label) 397 if i < len(file_labels) - 1: # Add spacer twice after all but last 398 spacer_label = chr(66 + i * 2) 399 cmd.append(spacer_label) 400 cmd.append(spacer_label) # Insert spacer twice for full double-sided sheet 401 402 cmd.extend(['output', combined_path]) 403 404 # Execute 405 try: 406 subprocess.run(cmd, check=True, capture_output=True) 407 total_pages = sum(pc for _, pc in batch) + (len(batch) - 1) * spacer_pages_total 408 print(f' Created {combined_name} ({len(batch)} signatures, {total_pages} pages)') 409 except subprocess.CalledProcessError as e: 410 print(f' ERROR: Failed to create {combined_name}') 411 return False 412 413 print(f'\nCombined files created in {output_dir}/') 414 return True 415 416 def main(): 417 """Main workflow orchestrator.""" 418 print('=' * 60) 419 print("PAUL'S PREPONDERATING PREPRESSER v1.1") 420 print('Automated Workflow for Signature Preparation') 421 print('=' * 60) 422 print() 423 424 # Get source file 425 if len(sys.argv) > 1: 426 original_source = sys.argv[1] 427 else: 428 original_source = input('Source PDF file: ').strip() 429 430 if not original_source.endswith('.pdf'): 431 original_source += '.pdf' 432 433 if not os.path.isfile(original_source): 434 print(f'ERROR: File "{original_source}" not found.') 435 sys.exit(1) 436 437 # Track temporary files for cleanup 438 temp_files = [] 439 source_file = original_source 440 441 # Get page count 442 page_count = get_page_count(source_file) 443 if page_count is None: 444 sys.exit(1) 445 446 # Detect page size 447 try: 448 with open(source_file, 'rb') as f: 449 reader = PyPDF2.PdfReader(f) 450 first_page = reader.pages[0] 451 mediabox = first_page.mediabox 452 width_pts = float(mediabox.width) 453 height_pts = float(mediabox.height) 454 width_in = width_pts / 72 # Convert points to inches 455 height_in = height_pts / 72 456 except Exception as e: 457 # If we can't detect size, just continue 458 width_in = 0 459 height_in = 0 460 461 print(f'Source: {source_file}') 462 print(f'Pages: {page_count}') 463 if width_in > 0 and height_in > 0: 464 print(f'Page size: {width_in:.2f}" × {height_in:.2f}"') 465 print() 466 467 # Check if source is close to letter size (8.5 × 11 in) 468 is_letter_sized = width_in > 7.2 or height_in > 10.0 469 470 if is_letter_sized: 471 print('⚠ Large pages detected (close to letter size)') 472 print('This document may not need the signature workflow.') 473 print() 474 print('Options:') 475 print(' 1. Print as-is on letter-size paper (1-up, double-sided)') 476 print(' 2. Continue with signature workflow (resize to half-letter)') 477 print() 478 print('Your choice [1/2]: ', end='') 479 size_choice = input().strip() 480 481 if size_choice == '1': 482 print() 483 print('Suggested workflow for letter-sized document:') 484 print(' 1. Open PDF in your PDF viewer') 485 print(' 2. Print double-sided, flip on long edge') 486 print(' 3. No signature splitting needed!') 487 print() 488 print('Exiting workflow.') 489 sys.exit(0) 490 elif size_choice != '2': 491 print('Invalid choice. Exiting.') 492 sys.exit(1) 493 # If choice is 2, continue below 494 495 # Check if source is already 5.5 × 8.5 inches (allow 0.1 inch tolerance) 496 is_half_letter = (abs(width_in - 5.5) < 0.1 and abs(height_in - 8.5) < 0.1) 497 498 if is_half_letter: 499 print() 500 print('✓ Pages are already half-letter size (5.5" × 8.5"), skipping resize step.') 501 print() 502 else: 503 # Ask about resizing to half-letter 504 print('Resize pages to half-letter size (5.5 × 8.5 in) first? [y/n] [DEFAULT: yes]: ', end='') 505 resize_response = input().strip().lower() 506 507 if resize_response in ['y', 'yes', '']: 508 print('Resizing pages to half-letter...') 509 base, ext = os.path.splitext(source_file) 510 resized_file = f'{base}-halfletter{ext}' 511 512 try: 513 # Use ghostscript to resize/fit pages to 5.5 x 8.5 inches 514 # 5.5" = 396 points, 8.5" = 612 points (72 points per inch) 515 cmd = [ 516 'gs', 517 '-sPAPERSIZE=custom', 518 '-dFIXEDMEDIA', 519 '-dPDFFitPage', 520 '-dDEVICEWIDTHPOINTS=396', # 5.5 inches 521 '-dDEVICEHEIGHTPOINTS=612', # 8.5 inches 522 '-dNOPAUSE', 523 '-dBATCH', 524 '-dSAFER', 525 '-sDEVICE=pdfwrite', 526 '-sOutputFile=' + resized_file, 527 source_file 528 ] 529 subprocess.run(cmd, check=True, capture_output=True) 530 print(f'Created {resized_file}') 531 532 # Update source to use resized file and track for cleanup 533 source_file = resized_file 534 temp_files.append(resized_file) 535 536 # Recalculate page count (shouldn't change, but be thorough) 537 page_count = get_page_count(source_file) 538 if page_count is None: 539 sys.exit(1) 540 541 except subprocess.CalledProcessError as e: 542 print('ERROR: ghostscript (gs) failed to resize PDF') 543 if sys.platform == 'darwin': 544 print('Make sure ghostscript is installed: brew install ghostscript') 545 else: 546 print('Make sure ghostscript is installed: sudo apt install ghostscript') 547 sys.exit(1) 548 except FileNotFoundError: 549 print('ERROR: ghostscript (gs) not found') 550 if sys.platform == 'darwin': 551 print('Install it with: brew install ghostscript') 552 else: 553 print('Install it with: sudo apt install ghostscript') 554 sys.exit(1) 555 556 print() 557 558 # Handle very small PDFs automatically (≤16 pages) 559 if page_count <= 16: 560 print('Small PDF detected - processing automatically...') 561 562 # Calculate padding needed to reach next multiple of 4 563 if page_count % 4 == 0: 564 pages_to_add = 0 565 sig_config = [page_count] 566 print(f'Configuration: Single {page_count}-page signature (no padding needed)') 567 else: 568 pages_to_add = 4 - (page_count % 4) 569 padded_count = page_count + pages_to_add 570 sig_config = [padded_count] 571 print(f'Configuration: Single {padded_count}-page signature (adding {pages_to_add} blank pages)') 572 else: 573 # Normal workflow for larger PDFs 574 # Suggest signature sizes 575 print('Suggested signature configurations:') 576 suggestions = suggest_signature_sizes(page_count) 577 578 if not suggestions: 579 print('ERROR: Could not find suitable signature configuration.') 580 sys.exit(1) 581 582 # Show suggestions 583 for i, (desc, sig_config, pages_to_add) in enumerate(suggestions, 1): 584 if pages_to_add == 0: 585 print(f' {i}. {desc} (no padding needed)') 586 else: 587 print(f' {i}. {desc} (add {pages_to_add} blank pages)') 588 589 # Get user choice with validation loop 590 while True: 591 print() 592 print(f'Choose a configuration [1-{len(suggestions)}], or q to quit: ', end='') 593 choice = input().strip().lower() 594 595 # Check for quit 596 if choice in ['q', 'quit']: 597 print('Aborted.') 598 sys.exit(0) 599 600 # Parse choice 601 try: 602 choice_idx = int(choice) 603 if 1 <= choice_idx <= len(suggestions): 604 desc, sig_config, pages_to_add = suggestions[choice_idx - 1] 605 break # Valid choice, exit loop 606 else: 607 print(f'ERROR: Please choose a number between 1 and {len(suggestions)}, or q to quit.') 608 continue 609 except ValueError: 610 print('ERROR: Invalid input. Enter a number or q to quit.') 611 continue 612 613 print() 614 print(f'Configuration: {desc}') 615 if pages_to_add > 0: 616 print(f'Padding: {pages_to_add} blank page(s) will be added') 617 618 # Pad if needed 619 working_file = source_file 620 if pages_to_add > 0: 621 print() 622 working_file = pad_pdf(source_file, pages_to_add) 623 if working_file is None: 624 sys.exit(1) 625 temp_files.append(working_file) 626 627 # Split into signatures 628 print() 629 if not split_into_signatures(working_file, sig_config): 630 sys.exit(1) 631 632 # Create output directory name from source filename 633 base_name = os.path.splitext(os.path.basename(source_file))[0] 634 output_dir = f'{base_name}-output' 635 636 # Ask about imposition 637 print() 638 print('Split complete! Would you like to impose the signatures now? [y/n] [DEFAULT: yes]: ', end='') 639 response = input().strip().lower() 640 641 if response in ['y', 'yes', '']: 642 if not impose_signatures(output_dir): 643 sys.exit(1) 644 645 # Clean up temporary files 646 for temp_file in temp_files: 647 if os.path.isfile(temp_file): 648 os.remove(temp_file) 649 650 # Only ask about combining if there are multiple signatures 651 if len(sig_config) > 1: 652 # Ask about combining 653 print() 654 print('Would you like to combine signatures with spacers for easier printing? [y/n] [DEFAULT: yes]: ', end='') 655 combine_response = input().strip().lower() 656 657 if combine_response in ['y', 'yes', '']: 658 if combine_signatures(output_dir): 659 print(f'\n✓ All files ready in {output_dir}/') 660 print(' - Individual signatures: 01.pdf, 02.pdf, ...') 661 print(' - Print jobs: job01.pdf, job02.pdf, ...') 662 else: 663 print(f'\n✓ Individual signatures ready in {output_dir}/') 664 else: 665 print(f'\n✓ Individual signatures ready in {output_dir}/') 666 else: 667 # Single signature - no need to combine 668 print(f'\n✓ Single signature ready in {output_dir}/') 669 else: 670 # User declined imposition - move sig files to output dir anyway 671 print() 672 print('Organizing signature files...') 673 os.makedirs(output_dir, exist_ok=True) 674 675 sig_files = sorted([f for f in os.listdir('.') if f.startswith('sig') and f.endswith('.pdf')]) 676 for sig_file in sig_files: 677 dst = os.path.join(output_dir, sig_file) 678 shutil.move(sig_file, dst) 679 print(f' {sig_file} → {output_dir}/{sig_file}') 680 681 # Clean up temporary files 682 for temp_file in temp_files: 683 if os.path.isfile(temp_file): 684 os.remove(temp_file) 685 686 print(f'\n✓ Signature files ready in {output_dir}/') 687 print(' To impose later:') 688 print(f' cd {output_dir}') 689 print(f' fppp') 690 691 print() 692 print('Workflow complete!') 693 694 if __name__ == '__main__': 695 main()