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])