/ src / ppp / shell.py
shell.py
  1  """
  2  Python wrappers for the bash-based PostScript pipeline tools.
  3  These replace the original bash scripts (singledingle, flippar, etc.)
  4  so everything can be installed via a single `pipx install`.
  5  """
  6  
  7  import glob
  8  import os
  9  import subprocess
 10  import shutil
 11  import sys
 12  
 13  
 14  def _run(cmd, description=None):
 15      """Run a command, printing errors on failure."""
 16      try:
 17          subprocess.run(cmd, check=True)
 18      except FileNotFoundError:
 19          tool = cmd[0]
 20          print(f'ERROR: {tool} not found.')
 21          if sys.platform == 'darwin':
 22              hints = {
 23                  'pdftops': 'brew install poppler',
 24                  'psbook': 'brew install psutils',
 25                  'pstops': 'brew install psutils',
 26                  'ps2pdf': 'brew install ghostscript',
 27                  'calibredb': 'brew install calibre',
 28              }
 29          else:
 30              hints = {
 31                  'pdftops': 'sudo apt install poppler-utils',
 32                  'psbook': 'sudo apt install psutils',
 33                  'pstops': 'sudo apt install psutils',
 34                  'ps2pdf': 'sudo apt install ghostscript',
 35                  'calibredb': 'sudo apt install calibre',
 36              }
 37          if tool in hints:
 38              print(f'Install it with: {hints[tool]}')
 39          sys.exit(1)
 40      except subprocess.CalledProcessError:
 41          if description:
 42              print(f'ERROR: {description} failed')
 43          sys.exit(1)
 44  
 45  
 46  def _cleanup_ps():
 47      """Remove all .ps files in current directory."""
 48      for f in glob.glob('*.ps'):
 49          os.remove(f)
 50  
 51  
 52  def _cowsay(message):
 53      """Run cowsay if available, otherwise just print."""
 54      if shutil.which('cowsay'):
 55          subprocess.run(['cowsay', message])
 56      else:
 57          print(message)
 58  
 59  
 60  def singledingle(filename):
 61      """Single-sided 2-up imposition: PDF -> PS -> psbook -> pstops -> PDF."""
 62      base = filename.removesuffix('.pdf')
 63  
 64      print(f'got file {filename}')
 65      print('.PSenating ...')
 66      _run(['pdftops', '-level3', '-origpagesizes', filename, f'{base}.ps'])
 67  
 68      print(' ... rearranging pages ...')
 69      _run(['psbook', f'{base}.ps', f'b{base}.ps'])
 70  
 71      print(' ... imposing pages 2-up ...')
 72      _run(['pstops', '2:0L@1.0(1w,0)+1L@1.0(1w,0.5h)', f'b{base}.ps', f'i{base}.ps'])
 73  
 74      print(' ... re.PDFenating ...')
 75      _run(['ps2pdf', f'i{base}.ps', f'PPP{base}.pdf'])
 76  
 77      _cleanup_ps()
 78      _cowsay('and boom goes the dynamite.')
 79  
 80  
 81  def singledingle_main():
 82      if len(sys.argv) < 2:
 83          print('Usage: singledingle <file.pdf>')
 84          sys.exit(1)
 85      singledingle(sys.argv[1])
 86  
 87  
 88  def flippar(filename):
 89      """Page flip fixer: PDF -> PS -> pstops -> PDF."""
 90      base = filename.removesuffix('.pdf')
 91  
 92      print(f'got file {filename}')
 93      print('.PSenating ...')
 94      _run(['pdftops', '-level3', '-origpagesizes', filename, f'{base}.ps'])
 95  
 96      print(' ... reticulating splines ...')
 97      _run(['pstops', '2:0,1U(1w,1h)', f'{base}.ps', f'i{base}.ps'])
 98  
 99      print(' ... re.PDFenating ...')
100      _run(['ps2pdf', f'i{base}.ps', f'{base}-fixed.pdf'])
101  
102      _cleanup_ps()
103      _cowsay('and boom goes the dynamite.')
104  
105  
106  def flippar_main():
107      if len(sys.argv) < 2:
108          print('Usage: flippar <file.pdf>')
109          sys.exit(1)
110      flippar(sys.argv[1])
111  
112  
113  def fppp_main():
114      """Run singledingle on all PDFs in current directory."""
115      pdf_files = sorted(glob.glob('*.pdf'))
116      if not pdf_files:
117          print('No PDF files found in current directory.')
118          sys.exit(1)
119      for f in pdf_files:
120          singledingle(f)
121  
122  
123  def pppf_main():
124      """Run flippar on all PDFs in current directory."""
125      pdf_files = sorted(glob.glob('*.pdf'))
126      if not pdf_files:
127          print('No PDF files found in current directory.')
128          sys.exit(1)
129      for f in pdf_files:
130          flippar(f)
131  
132  
133  def impose_4up(filename):
134      """4-up imposition: PDF -> PS -> pstops -> PDF."""
135      base = filename.removesuffix('.pdf')
136  
137      print(f'got file {filename}')
138      print('.PSenating ...')
139      _run(['pdftops', '-level3', '-origpagesizes', filename, f'{base}.ps'])
140  
141      print(' ... imposing pages 4-up ...')
142      _run(['pstops',
143            '4:0U(0,0)+7U(0.5w,0)+3(0,0.5h)+4(0.5w,0.5h), 4:6U(0,0)+1U(0.5w,0)+5(0,0.5h)+2(0.5w,0.5h)',
144            f'{base}.ps', f'i{base}.ps'])
145  
146      print(' ... re.PDFenating ...')
147      _run(['ps2pdf', f'i{base}.ps', f'PPP{base}.pdf'])
148  
149      _cleanup_ps()
150      _cowsay('and boom goes the dynamite.')
151  
152  
153  def impose_4up_main():
154      if len(sys.argv) < 2:
155          print('Usage: impose-4up <file.pdf>')
156          sys.exit(1)
157      impose_4up(sys.argv[1])
158  
159  
160  def isbnner_main():
161      """Set ISBN metadata on a Calibre book entry."""
162      isbn = input('ISBN to add: ').strip()
163      theid = input('ID to add it to: ').strip()
164  
165      _run(['calibredb', 'set_metadata', '--field', f'identifiers:isbn:{isbn}', theid])
166  
167      go = input('AGAIN???!??!   ').strip()
168      if go not in ('Y', 'y'):
169          return
170  
171      isbn = input('ISBN to add: ').strip()
172      theid = input('ID to add it to: ').strip()
173      _run(['calibredb', 'set_metadata', '--field', f'identifiers:isbn:{isbn}', theid])