home · contact · privacy
Extend code documentation, improve some variable names.
authorPlom Heller <plom@plomlompom.com>
Sun, 5 Apr 2026 20:58:24 +0000 (22:58 +0200)
committerPlom Heller <plom@plomlompom.com>
Sun, 5 Apr 2026 20:58:24 +0000 (22:58 +0200)
bookmaker.py

index edd90bb60668548627b7906b2bf4e2ac67ca2fbf..b8d51f223b59bcfe29c51f66fd208c205c3de946 100755 (executable)
@@ -11,6 +11,7 @@ import sys
 
 
 def handled_error_exit(msg):
+    'Print msg, then exit(1).'
     print(f'ERROR: {msg}')
     sys.exit(1)
 
@@ -38,6 +39,7 @@ PAGE_ORDER_FOR_NUP4 = (3, 0, 7, 4, 1, 2, 5, 6)
 
 
 class PageCrop:
+    'Per-page crop instructions as sizes in point and cm, and A4-zoom factor.'
 
     def __init__(self, left_cm=0, bottom_cm=0, right_cm=0, top_cm=0):
         self.left_cm = left_cm
@@ -63,18 +65,22 @@ class PageCrop:
 
     @property
     def format_in_cm(self):
+        'Human-readable listing of crops in cm.'
         return (f'left {self.left_cm}cm, bottom {self.bottom_cm}cm, '
                 f'right {self.right_cm}cm, top {self.top_cm}cm')
 
     @property
     def remaining_width(self):
+        "What's left of A4_WIDTH after applying width croppings."
         return A4_WIDTH - self.left - self.right
 
     @property
     def remaining_height(self):
+        "What's left of A4_WIDTH after applying height croppings."
         return A4_HEIGHT - self.bottom - self.top
 
-    def give_mirror(self):
+    def make_mirror(self):
+        'Return PageCrop of swapped .left and .right.'
         return PageCrop(left_cm=self.right_cm,
                         bottom_cm=self.bottom_cm,
                         right_cm=self.left_cm,
@@ -82,6 +88,7 @@ class PageCrop:
 
 
 class Nup4Geometry:
+    'Nup4-specific attributes, i.e. outer-page margins, spine sizes.'
 
     def __init__(self, margin_cm):
         self.margin = margin_cm * POINTS_PER_CM
@@ -103,6 +110,7 @@ class ArgFail(Exception):
 
 
 def parse_args():
+    'Collect command line arguments.'
     help_epilogue = ('See README.txt for detailed usage instructions, '
                      'command examples, etc.')
     parser = argparse.ArgumentParser(
@@ -316,6 +324,7 @@ def validate_ranges(args, pages_to_add):
 
 
 def rotate_pages(args_rotate_page, pages_to_add):
+    'For pages_to_add page numbered in args_rotate_page, rotate by 90°.'
     if args_rotate_page:
         for rotate_page in args_rotate_page:
             page = pages_to_add[rotate_page - 1]
@@ -328,6 +337,7 @@ def rotate_pages(args_rotate_page, pages_to_add):
 
 
 def pad_pages_to_multiple_of_8(pages_to_add):
+    'To pages_to_add add blank pages until its size is multiple of 8.'
     mod_to_8 = len(pages_to_add) % 8
     if mod_to_8 > 0:
         old_len = len(pages_to_add)
@@ -340,6 +350,7 @@ def pad_pages_to_multiple_of_8(pages_to_add):
 
 
 def normalize_pages_to_a4(pages_to_add):
+    'Adjust pages_to_add .mediabox=.cropbox to A4, enact /Rotate inside that.'
     for page in pages_to_add:
         if '/Rotate' in page:  # TODO: preserve rotation, but in canvas?
             page.rotate(360 - page['/Rotate'])
@@ -350,11 +361,12 @@ def normalize_pages_to_a4(pages_to_add):
         page.cropbox = page.mediabox
 
 
-def collect_per_page_crops_and_zooms(args_crops,
-                                     args_keep_mediabox,
-                                     args_symmetry,
-                                     pages_to_add):
-    crop_at_page = [PageCrop()] * len(pages_to_add)
+def collect_page_croppings(args_crops,
+                           args_keep_mediabox,
+                           args_symmetry,
+                           pages_to_add):
+    'Calculate individual PageCrops from inputs.'
+    page_croppings = [PageCrop()] * len(pages_to_add)
     if args_crops:
         for c_string in args_crops:
             page_range, crops = split_crops_string(c_string)
@@ -365,52 +377,54 @@ def collect_per_page_crops_and_zooms(args_crops,
             page_crop = PageCrop(*crops.split(','))
             print(f'{prefix}: to pages {idx_start + 1}:{idx_after} '
                   f'applying crop: {page_crop.format_in_cm}{suffix}')
-            for page_num in range(idx_start, idx_after):
-                if args_symmetry and page_num % 2:
-                    crop_at_page[page_num] = page_crop.give_mirror()
+            for n_page in range(idx_start, idx_after):
+                if args_symmetry and n_page % 2:
+                    page_croppings[n_page] = page_crop.make_mirror()
                 else:
-                    crop_at_page[page_num] = page_crop
+                    page_croppings[n_page] = page_crop
     elif args_keep_mediabox:
-        for page_num, page in enumerate(pages_to_add):
-            crop_at_page[page_num] = PageCrop(
+        for n_page, page in enumerate(pages_to_add):
+            page_croppings[n_page] = PageCrop(
                     page.mediabox.left / POINTS_PER_CM,
                     page.mediabox.bottom / POINTS_PER_CM,
                     (0.01 + A4_WIDTH - page.mediabox.right) / POINTS_PER_CM,
                     (0.01 + A4_HEIGHT - page.mediabox.top) / POINTS_PER_CM)
-            if args_symmetry and not page_num % 2:
-                crop_at_page[page_num] = crop_at_page[page_num].give_mirror()
-    return crop_at_page
+            if args_symmetry and not n_page % 2:
+                page_croppings[n_page] = page_croppings[n_page].make_mirror()
+    return page_croppings
 
 
-def build_single_pages_output(writer, pages_to_add, crop_at_page):
+def build_single_pages_output(writer, pages_to_add, page_croppings):
+    'On each of pages_to_add apply its page_croppings, then writer.add_page.'
     print('building 1-input-page-per-output-page book')
-    odd_page = True
+    odd_page = True   # TODO: removable?
     for i, page in enumerate(pages_to_add):
         page.add_transformation(pypdf.Transformation().translate(
-            tx=-crop_at_page[i].left, ty=-crop_at_page[i].bottom))
+            tx=-page_croppings[i].left, ty=-page_croppings[i].bottom))
         page.add_transformation(pypdf.Transformation().scale(
-            crop_at_page[i].zoom, crop_at_page[i].zoom))
+            page_croppings[i].zoom, page_croppings[i].zoom))
         page.mediabox.right\
-            = crop_at_page[i].remaining_width * crop_at_page[i].zoom
+            = page_croppings[i].remaining_width * page_croppings[i].zoom
         page.mediabox.top\
-            = crop_at_page[i].remaining_height * crop_at_page[i].zoom
+            = page_croppings[i].remaining_height * page_croppings[i].zoom
         writer.add_page(page)
-        odd_page = not odd_page
+        odd_page = not odd_page   # TODO: removable?
         print(f'built page number {i+1} (of {len(pages_to_add)})')
 
 
 def build_nup4_output(writer,
                       pages_to_add,
-                      crop_at_page,
+                      page_croppings,
                       args_print_margin,
                       args_analyze,
                       canvas_class):
+    'Build nup4 pages from inputs.'
     print('-n: building 4-input-pages-per-output-page book')
     print(f'-m: applying printable-area margin of {args_print_margin}cm')
     if args_analyze:
         print('-a: drawing page borders, spine limits')
     nup4_geometry = Nup4Geometry(args_print_margin)
-    pages_to_add, new_i_order = resort_pages_for_nup4(pages_to_add)
+    pages_to_add, old_indices = resort_pages_for_nup4(pages_to_add)
     nup4_i = 0
     page_count = 0
     is_front_page = True
@@ -418,9 +432,9 @@ def build_nup4_output(writer,
         if nup4_i == 0:
             new_page = pypdf.PageObject.create_blank_page(
                     width=A4_WIDTH, height=A4_HEIGHT)
-        corrected_i = new_i_order[i]
+        corrected_i = old_indices[i]
         nup4_inner_page_transform(
-                page, crop_at_page[corrected_i], nup4_geometry, nup4_i)
+                page, page_croppings[corrected_i], nup4_geometry, nup4_i)
         nup4_outer_page_transform(page, nup4_geometry, nup4_i)
         new_page.merge_page(page)
         page_count += 1
@@ -439,8 +453,9 @@ def build_nup4_output(writer,
 
 
 def resort_pages_for_nup4(pages_to_add):
+    'Adapt pages_to_add towards PAGE_ORDER_FOR_NUP4.'
     new_page_order = []
-    new_i_order = []
+    old_indices = []
     eight_pack = []
     i = 0
     n_eights = 0
@@ -452,13 +467,14 @@ def resort_pages_for_nup4(pages_to_add):
         if i == 8:
             i = 0
             for n in PAGE_ORDER_FOR_NUP4:
-                new_i_order += [8 * n_eights + n]
+                old_indices += [8 * n_eights + n]
                 new_page_order += [eight_pack[n]]
             n_eights += 1
-    return new_page_order, new_i_order
+    return new_page_order, old_indices
 
 
 def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i):
+    'Apply to page crop instructions adequate to position in nup4 geometry.'
     page.add_transformation(pypdf.Transformation().translate(
         ty=A4_HEIGHT / crop.zoom - (A4_HEIGHT - crop.top)))
     if nup4_i in {0, 2}:
@@ -476,6 +492,7 @@ def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i):
 
 
 def nup4_outer_page_transform(page, nup4_geometry, nup4_i):
+    'Shrink and position page into nup4_geometry as per its position nup4_i.'
     page.add_transformation(pypdf.Transformation().translate(
         ty=(1-nup4_geometry.shrink_for_spine)*A4_HEIGHT))
     if nup4_i in {0, 1}:
@@ -507,6 +524,7 @@ def ornate_nup4(args_analyze,
                 new_page,
                 nup4_geometry,
                 canvas_class):
+    'Apply nup4 line guides onto new_page.'
     if args_analyze:
         # borders
         packet = io.BytesIO()
@@ -548,6 +566,7 @@ def ornate_nup4(args_analyze,
 
 
 def draw_cut(canvas, x_spine_limit, direction):
+    'Into canvas draw book binding cut guide at x_spine_limit into direction.'
     outer_start_x = x_spine_limit - 0.5 * CUT_WIDTH * direction
     inner_start_x = x_spine_limit + 0.5 * CUT_WIDTH * direction
     middle_point_y = A4_HALF_HEIGHT + MIDDLE_POINT_DEPTH * direction
@@ -558,6 +577,7 @@ def draw_cut(canvas, x_spine_limit, direction):
 
 
 def main():
+    'Full program run to be wrapped into ArgFail catcher.'
     args = parse_args()
     validate_args_syntax(args)
     if args.nup4:
@@ -574,18 +594,20 @@ def main():
         pad_pages_to_multiple_of_8(pages_to_add)
     if not args.keep_mediabox:
         normalize_pages_to_a4(pages_to_add)
-    crop_at_page = collect_per_page_crops_and_zooms(
-            args.crops, args.keep_mediabox, args.symmetry, pages_to_add)
+    page_croppings = collect_page_croppings(args.crops,
+                                            args.keep_mediabox,
+                                            args.symmetry,
+                                            pages_to_add)
     writer = pypdf.PdfWriter()
     if args.nup4:
         build_nup4_output(writer,
                           pages_to_add,
-                          crop_at_page,
+                          page_croppings,
                           args.print_margin,
                           args.analyze,
                           Canvas)
     else:
-        build_single_pages_output(writer, pages_to_add, crop_at_page)
+        build_single_pages_output(writer, pages_to_add, page_croppings)
     for file in opened_files:
         file.close()
     with open(args.output_file, 'wb') as output_file: