home · contact · privacy
Bookmaker: Only handle explicitly defined Exceptions.
[misc] / bookmaker.py
index 291d00bf9f2f6412fafbdbfe5fcf5cfabcf47f60..868866e9e57f426e07c4fe6a23bff817c0148a4f 100755 (executable)
@@ -2,29 +2,6 @@
 """
 bookmaker.py is a helper for optimizing PDFs of books for the production of small self-printed, self-bound physical books.  Towards this goal it offers various PDF manipulation options that may also be used indepéndently and for other purposes.
 """
-import argparse
-import io
-import os
-import sys
-def fail_with_msg(msg):
-    print("ERROR:", msg)
-    sys.exit(1)
-try:
-    import pypdf
-except ImportError:
-    fail_with_msg("Can't run without pypdf installed.")
-try:
-    from reportlab.lib.pagesizes import A4
-except ImportError:
-    fail_with_msg("Can't run without reportlab installed.")
-
-# some constants
-A4_WIDTH, A4_HEIGHT = A4
-POINTS_PER_CM = 10 * 72 / 25.4
-CUT_DEPTH = 1.95 * POINTS_PER_CM
-CUT_WIDTH = 1.05 * POINTS_PER_CM
-MIDDLE_POINT_DEPTH = 0.4 * POINTS_PER_CM
-SPINE_LIMIT = 1 * POINTS_PER_CM
 help_epilogue = """
 EXAMPLES:
 
@@ -55,10 +32,10 @@ Initially declare 5cm crop from the left and 1cm crop from right, but alternate
 Quarter each OUTPUT.pdf page to carry 4 pages from INPUT.pdf, draw stencils into inner margins for cuts to carry binding strings:
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --nup4
 
-Same as --nup4, but define a printable-region margin of 1.3cm to limit the space for the INPUT.pdf pages in OUTPUT.pdf page quarters:
+Same --nup4, but define a printable-region margin of 1.3cm to limit the space for the INPUT.pdf pages in OUTPUT.pdf page quarters:
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --print_margin 1.3
 
-Same as -n, but draw lines marking printable-region margins, page quarts, spine margins:
+Same --nup4, but draw lines marking printable-region margins, page quarts, spine margins:
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --analyze
 
 NOTES:
@@ -82,15 +59,41 @@ To facilitate this layout, --nup4 also pads the input PDF pages to a total numbe
 
 (To turn above double-sided example page into a tiny 8-page book:  Cut the paper in two on its horizontal middle line.  Fold the two halves by their vertical middle lines, with pages 3-2 and 7-6 on the folds' insides.  This creates two 4-page books of pages 1-4 and pages 5-8.  Fold them both closed and (counter-intuitively) put the book of pages 5-8 on top of the other one (creating a temporary page order of 5,6,7,8,1,2,3,4).  A binding cut stencil should be visible on the top left of this stack – cut it out (with all pages folded together) to add the same inner-margin upper cut to each page.  Turn around your 8-pages stack to find the mirror image of aforementioned stencil on the stack's back's bottom, and cut that out too.  Each page now has binding cuts on top and bottom of its inner margins.  Swap the order of both books (back to the final page order of 1,2,3,4,5,6,7,8), and you now have an 8-pages book that can be "bound" in its binding cuts through a rubber band or the like.  Repeat with the next 8-pages double-page, et cetera.  (Actually, with just 8 pages, the paper may curl under the pressure of a rubber band – but go up to 32 pages or so, and the result will become quite stable.)
 """
+import argparse
+import io
+import os
+import sys
+
+def handled_error_exit(msg):
+    print("ERROR:", msg)
+    sys.exit(1)
+
+try:
+    import pypdf
+except ImportError:
+    handled_error_exit("Can't run at all without pypdf installed.")
+
+# some constants
+POINTS_PER_CM = 10 * 72 / 25.4
+A4_WIDTH = 21 * POINTS_PER_CM
+A4_HEIGHT = 29.7 * POINTS_PER_CM
+A4 = (A4_WIDTH, A4_HEIGHT)
+CUT_DEPTH = 1.95 * POINTS_PER_CM
+CUT_WIDTH = 1.05 * POINTS_PER_CM
+MIDDLE_POINT_DEPTH = 0.4 * POINTS_PER_CM
+SPINE_LIMIT = 1 * POINTS_PER_CM
 
 # some helpers
+class HandledException(Exception):
+    pass
+
 def validate_page_range(p_string, err_msg_prefix):
     err_msg = "%s: invalid page range string: %s" % (err_msg_prefix, p_string)
     if '-' not in p_string:
-        raise ValueError("%s: page range string lacks '-': %s" % (err_msg_prefix, p_string))
+        raise HandledException("%s: page range string lacks '-': %s" % (err_msg_prefix, p_string))
     tokens = p_string.split("-")
     if len(tokens) > 2:
-        raise ValueError("%s: page range string has too many '-': %s" % (err_msg_prefix, p_string))
+        raise HandledException("%s: page range string has too many '-': %s" % (err_msg_prefix, p_string))
     for i, token in enumerate(tokens):
         if token == "":
             continue
@@ -100,19 +103,19 @@ def validate_page_range(p_string, err_msg_prefix):
             continue
         try:
             int(token)
-        except:
-            raise ValueError("%s: page range string carries values that are neither integer, nor 'start', nor 'end': %s" % (err_msg_prefix, p_string))
+        except ValueError:
+            raise HandledException("%s: page range string carries values that are neither integer, nor 'start', nor 'end': %s" % (err_msg_prefix, p_string))
         if int(token) < 1:
-            raise ValueError("%s: page range string may not carry page numbers <1: %s" % (err_msg_prefix, p_string))
+            raise HandledException("%s: page range string may not carry page numbers <1: %s" % (err_msg_prefix, p_string))
     start = -1
     end = -1
     try:
         start = int(tokens[0])
         end = int(tokens[1])
-    except:
+    except ValueError:
         pass
     if start > 0 and end > 0 and start > end:
-        raise ValueError("%s: page range starts higher than it ends: %s" % (err_msg_prefix, p_string))
+        raise HandledException("%s: page range starts higher than it ends: %s" % (err_msg_prefix, p_string))
 
 def split_crops_string(c_string):
     initial_split = c_string.split(':')
@@ -151,50 +154,55 @@ def parse_args():
     # some basic input validation
     for filename in args.input_file:
         if not os.path.isfile(filename):
-            raise ValueError("-i: %s is not a file" % filename)
+            raise HandledException("-i: %s is not a file" % filename)
         try:
             with open(filename, 'rb') as file:
                 pypdf.PdfReader(file)
         except pypdf.errors.PdfStreamError:
-            raise ValueError("-i: cannot interpret %s as PDF file" % filename)
+            raise HandledException("-i: cannot interpret %s as PDF file" % filename)
     if args.page_range:
         for p_string in args.page_range:
             validate_page_range(p_string, "-p")
         if len(args.page_range) > len(args.input_file):
-            raise ValueError("more -p arguments than -i arguments")
+            raise HandledException("more -p arguments than -i arguments")
     if args.crops:
         for c_string in args.crops:
             initial_split = c_string.split(':')
             if len(initial_split) > 2:
-                raise ValueError("-c: cropping string has multiple ':': %s" % c_string)
+                raise HandledException("-c: cropping string has multiple ':': %s" % c_string)
             page_range, crops = split_crops_string(c_string)
             crops = crops.split(",")
             if page_range:
                 validate_page_range(page_range, "-c")
             if len(crops) != 4:
-                raise ValueError("-c: cropping should contain three ',': %s" % c_string)
+                raise HandledException("-c: cropping should contain three ',': %s" % c_string)
             for crop in crops:
                 try:
                     float(crop)
-                except:
-                    raise ValueError("-c: non-number crop in %s" % c_string)
+                except ValueError:
+                    raise HandledException("-c: non-number crop in %s" % c_string)
     if args.rotate_page:
         for r in args.rotate_page:
             try:
                 int(r)
-            except:
-                raise ValueError("-r: non-integer value: %s" % r)
+            except ValueError:
+                raise HandledException("-r: non-integer value: %s" % r)
             if r < 1:
-                raise ValueError("-r: value must not be <1: %s" % r)
+                raise HandledException("-r: value must not be <1: %s" % r)
     try:
         float(args.print_margin)
-    except:
-        raise ValueError("-m: non-float value: %s" % arg.print_margin)
+    except ValueError:
+        raise HandledException("-m: non-float value: %s" % arg.print_margin)
 
     return args
 
 def main():
     args = parse_args()
+    if args.nup4:
+        try:
+            import reportlab.pdfgen.canvas
+        except ImportError:
+            raise HandledException("-n: need reportlab library installed for --nup4")
 
     # select pages from input files
     pages_to_add = []
@@ -209,7 +217,7 @@ def main():
             range_string = args.page_range[i]
         start_page, end_page = parse_page_range(range_string, reader.pages)
         if end_page > len(reader.pages):  # no need to test start_page cause start_page > end_page is checked above
-            raise ValueError("-p: page range goes beyond pages of input file: %s" % range_string)
+            raise HandledException("-p: page range goes beyond pages of input file: %s" % range_string)
         for old_page_num in range(start_page, end_page):
             new_page_num += 1
             page = reader.pages[old_page_num]
@@ -223,11 +231,11 @@ def main():
             if page_range:
                 start, end = parse_page_range(page_range, pages_to_add)
                 if end > len(pages_to_add):
-                     raise ValueError("-c: page range goes beyond number of pages we're building: %s" % page_range)
+                     raise HandledException("-c: page range goes beyond number of pages we're building: %s" % page_range)
     if args.rotate_page:
         for r in args.rotate_page:
             if r > len(pages_to_add):
-                 raise ValueError("-r: page number beyond number of pages we're building: %d" % r)
+                 raise HandledException("-r: page number beyond number of pages we're building: %d" % r)
 
     # rotate page canvas (as opposed to using PDF's /Rotate command)
     if args.rotate_page:
@@ -279,7 +287,7 @@ def main():
             zoom_horizontal = A4_WIDTH / (A4_WIDTH - crop_left - crop_right)
             zoom_vertical = A4_HEIGHT / (A4_HEIGHT - crop_bottom - crop_top)
             if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
-                raise ValueError("crops would create opposing zoom directions")
+                raise HandledException("-c: crops would create opposing zoom directions")
             elif zoom_horizontal + zoom_vertical > 2:
                 zoom = min(zoom_horizontal, zoom_vertical)
             else:
@@ -397,11 +405,10 @@ def main():
             print("merged page number %d (of %d)" % (page_count, len(pages_to_add)))
             i += 1
             if i > 3:
-                from reportlab.pdfgen import canvas
                 if args.analyze:
                     # borders
                     packet = io.BytesIO()
-                    c = canvas.Canvas(packet, pagesize=A4)
+                    c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
                     c.setLineWidth(0.1)
                     c.line(0, A4_HEIGHT, A4_WIDTH, A4_HEIGHT)
                     c.line(0, half_height, A4_WIDTH, half_height)
@@ -420,7 +427,7 @@ def main():
                 x_right_SPINE_LIMIT = A4_WIDTH - x_left_SPINE_LIMIT
                 if args.analyze or front_page:
                     packet = io.BytesIO()
-                    c = canvas.Canvas(packet, pagesize=A4)
+                    c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
                 if args.analyze:
                     # # spine lines
                     c.setLineWidth(0.1)
@@ -462,5 +469,5 @@ def main():
 if __name__ == "__main__":
     try:
         main()
-    except ValueError as e:
-        fail_with_msg(e)
+    except HandledException as e:
+        handled_error_exit(e)