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