/ src / ppp / workflow.py
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()