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