home · contact · privacy
Remove unused import.
[misc] / bookmaker.py
index 33351329855601e48cd0c16321b5f2cf537a2bd9..431587e2c877b4e504e3394ead47d123f3a168fb 100755 (executable)
@@ -1,9 +1,23 @@
 #!/usr/bin/env python3
 """
-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.
+bookmaker.py is a helper for optimizing PDFs 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.
 """
 help_epilogue = """
-EXAMPLES:
+OVERVIEW OF TARGET USAGE:
+
+By cropping with -c and studying the results, define the areas of the input PDF's pages you want visible.  Then, with--nup4, map those areas onto 4 input pages per 1 output page, arranged in such a way that double-sided print-out of those output pages can be cut, folded, and bound (helped by addition of stencils for small incisions to carry rubber bands or the like) into a small A6 book.  Each unit of 8 pages from the input PDF is mapped by --nup4 onto two pages representing two sides of a (no-tumble-duplex-printed) A4 paper:
+
+                      +-------=-------+                     __________________
+ (front)  (back)      | 4 | 1 = 2 | 3 |             4      /=|===|============
++-------=-------+ ==> +-------=-------+ ===>  _/|\_ v      >=|===|============
+| 4 | 1 = 2 | 3 |                            /  |  \_      \=|===|============
+|-------=-------|     +-------=-------+ 1-> | 2 | 3 |      | \   / <- cut out!
+| 8 | 5 = 6 | 7 | ==> | 8 | 5 = 6 | 7 |     | _/ \_ |      |  \ |
++-------=-------+     +-------=-------+     |/     \|      |   \| (p. 5)
+
+To turn this paper into a small 8-pages book, first cut it into two A5 papers along its horizontal middle.  Fold both A5's by their vertical middles, with pages 2-3 and 7-6 on the folds' insides.  You now have two 4-page A6 "books" of pages 1-4 and pages 5-8.  Fold both closed and (counter-intuitively) stack the second one on top of the first one (creating a temporary page order of 5,6,7,8,1,2,3,4).  This reveals a small stencil on the top left of page 5 – cut it out, with all other pages folded and aligned under it, creating a small notch in the upper "inner" corner of all pages.  Turn around the stack to find a mirror stencil on the bottom and repeat the cutting.  Each page now has cuts on top and bottom of its inner margins into which a rubber band can be hooked, or through which a string may be looped and tied, to bind the page's inner margins into a kind of book spine.  You may now swap the order of the 4-page books back into a proper final page order (of 1,2,3,4,5,6,7,8) and repeat the whole process for each further --nup4 output paper.
+
+COMMAND EXAMPLES:
 
 Concatenate two PDFs A.pdf and B.pdf to COMBINED.pdf:
     bookmaker.py --input_file A.pdf --input_file B.pdf --output_file COMBINED.pdf
@@ -17,7 +31,7 @@ Produce COMBINED.pdf from A.pdf's first 7 pages, B.pdf's pages except its first
 Crop each page 5cm from the left, 10cm from the bottom, 2cm from the right, and 0cm from the top:
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --crops "5,10,2,0"
 
-Include all pages from INPUT.pdf, but crop pages 10-20 by 5cm each from bottom and top:
+Include all pages from INPUT.pdf, but only crop pages 10-20 by 5cm each from bottom and top:
     bookmaker.py -i INPUT.pdf -c "10-20:0,5,0,5" -o OUTPUT.pdf
 
 Same crops for pages 10-20, but also crop all pages 30 and later by 3cm each from left and right:
@@ -27,7 +41,7 @@ Rotate by 90° pages 3, 5, 7; rotate page 7 once more by 90% (i.e. 180° in tota
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --rotate 3 -r 5 -r 7 -r 7
 
 Initially declare 5cm crop from the left and 1cm crop from right, but alternate direction between even and odd pages:
-    bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "5,0,1,0" -s
+    bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "5,0,1,0" --symmetry
 
 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
@@ -35,35 +49,19 @@ Quarter each OUTPUT.pdf page to carry 4 pages from INPUT.pdf, draw stencils into
 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 --nup4, but draw lines marking printable-region margins, page quarts, spine margins:
+Same --nup4, but draw lines marking printable-region margins, page quarters, spine margins:
     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --analyze
 
-NOTES:
+FURTHER NOTES:
 
 For arguments like -p, page numbers are assumed to start with 1 (not 0, which is treated as an invalid page number value).
 
 The target page shape so far is assumed to be A4 in portrait orientation; bookmaker.py normalizes all pages to this format before applying crops, and removes any source PDF /Rotate commands (for their production of landscape orientations).
-
-For --nup4, the -c cropping instructions do not so much erase content outside the cropped area, but rather zoom into the page in a way that maximes the cropped area as much as possible into the available per-page area between printable-area margins and the borders to the other quartered pages.  If the zoomed cropped area does not fit in neatly into its per-page area, this will preserve additional page content.
-
-The --nup4 quartering puts pages into a specific order optimized for no-tumble duplex print-outs that can easily be folded and cut into pages of a small A6 book.  Each unit of 8 pages from the source PDF is mapped thus onto two subsequent pages (i.e. front and back of a printed A4 paper):
-
- (front)      (back)
-+-------+   +-------+
-| 4 | 1 |   | 2 | 3 |
-|-------|   |-------|
-| 8 | 5 |   | 6 | 7 |
-+-------+   +-------+
-
-To facilitate this layout, --nup4 also pads the input PDF pages to a total number that is a multiple of 8, by adding empty pages if necessary.
-
-(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
-from collections import namedtuple
 
 def handled_error_exit(msg):
     print(f"ERROR: {msg}")
@@ -86,16 +84,64 @@ A4_HALF_HEIGHT = A4_HEIGHT / 2
 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
+INNER_SPINE_MARGIN_PER_PAGE = 1 * POINTS_PER_CM
 QUARTER_SCALE_FACTOR = 0.5
 PAGE_ORDER_FOR_NUP4 = (3,0,7,4,1,2,5,6)
 
-# some helpers
-PageCrop = namedtuple("PageCrop", ["left", "bottom", "right", "top"], defaults=[0,0,0,0])
+
+class PageCrop:
+
+    def __init__(self, left_cm=0, bottom_cm=0, right_cm=0, top_cm=0):
+        self.left_cm = left_cm
+        self.bottom_cm = bottom_cm
+        self.right_cm = right_cm
+        self.top_cm = top_cm
+        self.left = float(self.left_cm) * POINTS_PER_CM
+        self.bottom = float(self.bottom_cm) * POINTS_PER_CM
+        self.right = float(self.right_cm) * POINTS_PER_CM
+        self.top = float(self.top_cm) * POINTS_PER_CM
+        zoom_horizontal = A4_WIDTH / (A4_WIDTH - self.left - self.right)
+        zoom_vertical = A4_HEIGHT / (A4_HEIGHT - self.bottom - self.top)
+        if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
+            raise HandledException("-c: crops would create opposing zoom directions")
+        elif zoom_horizontal + zoom_vertical > 2:
+            self.zoom = min(zoom_horizontal, zoom_vertical)
+        else:
+            self.zoom = max(zoom_horizontal, zoom_vertical)
+
+    def __str__(self):
+        return str(vars(self))
+
+    @property
+    def format_in_cm(self):
+        return f"left {self.left_cm}cm, bottom {self.bottom_cm}cm, right {self.right_cm}cm, top {self.top_cm}cm"
+
+    @property
+    def remaining_width(self):
+        return A4_WIDTH - self.left - self.right
+
+    @property
+    def remaining_height(self):
+        return A4_HEIGHT - self.bottom - self.top
+
+    def give_mirror(self):
+        return PageCrop(left_cm=self.right_cm, bottom_cm=self.bottom_cm, right_cm=self.left_cm, top_cm=self.top_cm)
+
+
+class Nup4Geometry:
+
+    def __init__(self, margin_cm):
+        self.margin = margin_cm * POINTS_PER_CM
+        self.shrink_for_margin = (A4_WIDTH - 2 * self.margin)/A4_WIDTH
+        # NB: We define spine size un-shrunk, but .shrink_for_spine is used with values shrunk for the margin, which we undo here.
+        spine_part_of_page = (INNER_SPINE_MARGIN_PER_PAGE / A4_HALF_WIDTH) / self.shrink_for_margin
+        self.shrink_for_spine = 1 - spine_part_of_page
+
 
 class HandledException(Exception):
     pass
 
+
 def parse_args():
     parser = argparse.ArgumentParser(description=__doc__, epilog=help_epilogue, formatter_class=argparse.RawDescriptionHelpFormatter)
     parser.add_argument("-i", "--input_file", action="append", required=True, help="input PDF file")
@@ -109,6 +155,7 @@ def parse_args():
     parser.add_argument("-m", "--print_margin", type=float, default=0.43, help="print margin for --nup4 in cm (default 0.43)")
     return parser.parse_args()
 
+
 def validate_inputs_first_pass(args):
     for filename in args.input_file:
         if not os.path.isfile(filename):
@@ -152,6 +199,7 @@ def validate_inputs_first_pass(args):
     except ValueError:
         raise HandledException(f"-m: non-float value: {arg.print_margin}")
 
+
 def validate_page_range(p_string, err_msg_prefix):
     prefix = f"{err_msg_prefix}: page range string"
     if '-' not in p_string:
@@ -182,6 +230,7 @@ def validate_page_range(p_string, err_msg_prefix):
     if start > 0 and end > 0 and start > end:
         raise HandledException(f"{prefix} has higher start than end value: {p_string}")
 
+
 def split_crops_string(c_string):
     initial_split = c_string.split(':')
     if len(initial_split) > 1:
@@ -192,6 +241,7 @@ def split_crops_string(c_string):
         crops = initial_split[0]
     return page_range, crops
 
+
 def parse_page_range(range_string, pages):
     start_page = 0
     end_page = len(pages)
@@ -203,6 +253,7 @@ def parse_page_range(range_string, pages):
             end_page = int(end)
     return start_page, end_page
 
+
 def read_inputs_to_pagelist(args_input_file, args_page_range):
     pages_to_add = []
     opened_files = []
@@ -224,6 +275,7 @@ def read_inputs_to_pagelist(args_input_file, args_page_range):
             print(f"-i, -p: read in {input_file} page number {old_page_num+1} as new page {new_page_num}")
     return pages_to_add, opened_files
 
+
 def validate_inputs_second_pass(args, pages_to_add):
     if args.crops:
         for c_string in args.crops:
@@ -237,6 +289,7 @@ def validate_inputs_second_pass(args, pages_to_add):
             if r > len(pages_to_add):
                  raise HandledException(f"-r: page number beyond number of pages we're building: {r}")
 
+
 def rotate_pages(args_rotate_page, pages_to_add):
     if args_rotate_page:
         for rotate_page in args_rotate_page:
@@ -246,6 +299,7 @@ def rotate_pages(args_rotate_page, pages_to_add):
             page.add_transformation(pypdf.Transformation().translate(tx=A4_WIDTH/2, ty=A4_HEIGHT/2))
             print(f"-r: rotating (by 90°) page {rotate_page}")
 
+
 def pad_pages_to_multiple_of_8(pages_to_add):
     mod_to_8 = len(pages_to_add) % 8
     if mod_to_8 > 0:
@@ -255,6 +309,7 @@ def pad_pages_to_multiple_of_8(pages_to_add):
             pages_to_add += [new_page]
         print(f"-n: number of input pages {old_len} not required multiple of 8, padded to {len(pages_to_add)}")
 
+
 def normalize_pages_to_A4(pages_to_add):
     for page in pages_to_add:
         if "/Rotate" in page:  # TODO: preserve rotation, but in canvas?
@@ -265,82 +320,65 @@ def normalize_pages_to_A4(pages_to_add):
         page.mediabox.right = A4_WIDTH
         page.cropbox = page.mediabox
 
+
 def collect_per_page_crops_and_zooms(args_crops, args_symmetry, pages_to_add):
     crop_at_page = [PageCrop()] * len(pages_to_add)
-    zoom_at_page = [1]*len(pages_to_add)
     if args_crops:
         for c_string in args_crops:
             page_range, crops = split_crops_string(c_string)
             start_page, end_page = parse_page_range(page_range, pages_to_add)
             prefix = "-c, -t" if args_symmetry else "-c"
             suffix = " (but alternating left and right crop between even and odd pages)" if args_symmetry else ""
-            page_crop_cm = PageCrop(*[x for x in crops.split(',')])
-            page_crop = PageCrop(*[float(x) * POINTS_PER_CM for x in page_crop_cm])
-            crop_listing = ", ".join([f"{key} {val}cm" for key, val in page_crop_cm._asdict().items()])
-            print(f"{prefix}: to pages {start_page + 1} to {end_page} applying crop: {crop_listing}{suffix}")
-            cropped_width  = A4_WIDTH - page_crop.left - page_crop.right
-            cropped_height = A4_HEIGHT - page_crop.bottom - page_crop.top
-            zoom = 1
-            zoom_horizontal = A4_WIDTH / (A4_WIDTH - page_crop.left - page_crop.right)
-            zoom_vertical = A4_HEIGHT / (A4_HEIGHT - page_crop.bottom - page_crop.top)
-            if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
-                raise HandledException("-c: crops would create opposing zoom directions")
-            elif zoom_horizontal + zoom_vertical > 2:
-                zoom = min(zoom_horizontal, zoom_vertical)
-            else:
-                zoom = max(zoom_horizontal, zoom_vertical)
+            page_crop = PageCrop(*[x for x in crops.split(',')])
+            print(f"{prefix}: to pages {start_page + 1} to {end_page} applying crop: {page_crop.format_in_cm}{suffix}")
             for page_num in range(start_page, end_page):
                 if args_symmetry and page_num % 2:
-                    crop_at_page[page_num] = PageCrop(left=page_crop.right, right=page_crop.left, bottom=page_crop.bottom, top=page_crop.top)
+                    crop_at_page[page_num] = page_crop.give_mirror()
                 else:
                     crop_at_page[page_num] = page_crop
-                zoom_at_page[page_num] = zoom
-    return crop_at_page, zoom_at_page
+    return crop_at_page
+
 
-def build_single_pages_output(writer, pages_to_add, crop_at_page, zoom_at_page):
+def build_single_pages_output(writer, pages_to_add, crop_at_page):
     print("building 1-input-page-per-output-page book")
     odd_page = True
     for i, page in enumerate(pages_to_add):
-        zoom = zoom_at_page[i]
         page.add_transformation(pypdf.Transformation().translate(tx=-crop_at_page[i].left, ty=-crop_at_page[i].bottom))
-        page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
-        cropped_width  = A4_WIDTH - crop_at_page[i].left - crop_at_page[i].right
-        cropped_height = A4_HEIGHT - crop_at_page[i].bottom - crop_at_page[i].top
-        page.mediabox.right = cropped_width * zoom
-        page.mediabox.top = cropped_height * zoom
+        page.add_transformation(pypdf.Transformation().scale(crop_at_page[i].zoom, crop_at_page[i].zoom))
+        page.mediabox.right = crop_at_page[i].remaining_width * crop_at_page[i].zoom
+        page.mediabox.top = crop_at_page[i].remaining_height * crop_at_page[i].zoom
         writer.add_page(page)
         odd_page = not odd_page
         print(f"built page number {i+1} (of {len(pages_to_add)})")
 
-def build_nup4_output(writer, pages_to_add, crop_at_page, zoom_at_page, args_print_margin, args_analyze, canvas_class):
+
+def build_nup4_output(writer, pages_to_add, crop_at_page, args_print_margin, args_analyze, canvas_class):
     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")
-    printable_margin = args_print_margin * POINTS_PER_CM
-    printable_scale = (A4_WIDTH - 2 * printable_margin)/A4_WIDTH
-    spine_part_of_page = (SPINE_LIMIT / A4_HALF_WIDTH) / printable_scale
-    bonus_shrink_factor = 1 - spine_part_of_page
+    nup4_geometry = Nup4Geometry(args_print_margin)
     pages_to_add, new_i_order = resort_pages_for_nup4(pages_to_add)
-    nup4_position = 0
+    nup4_i = 0
     page_count = 0
     is_front_page = True
     for i, page in enumerate(pages_to_add):
-        if nup4_position == 0:
+        if nup4_i == 0:
             new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
         corrected_i = new_i_order[i]
-        nup4_inner_page_transform(page, crop_at_page[corrected_i], zoom_at_page[corrected_i], bonus_shrink_factor, printable_margin, printable_scale, nup4_position)
-        nup4_outer_page_transform(page, bonus_shrink_factor, nup4_position)
+        nup4_inner_page_transform(page, crop_at_page[corrected_i], nup4_geometry, nup4_i)
+        nup4_outer_page_transform(page, nup4_geometry, nup4_i)
         new_page.merge_page(page)
         page_count += 1
         print(f"merged page number {page_count} (of {len(pages_to_add)})")
-        nup4_position += 1
-        if nup4_position > 3:
-            ornate_nup4(writer, args_analyze, is_front_page, new_page, printable_margin, printable_scale, bonus_shrink_factor, canvas_class)
+        nup4_i += 1
+        if nup4_i > 3:
+            ornate_nup4(writer, args_analyze, is_front_page, new_page, nup4_geometry, canvas_class)
             writer.add_page(new_page)
-            nup4_position = 0
+            nup4_i = 0
             is_front_page = not is_front_page
 
+
 def resort_pages_for_nup4(pages_to_add):
     new_page_order = []
     new_i_order = []
@@ -360,39 +398,42 @@ def resort_pages_for_nup4(pages_to_add):
             n_eights += 1
     return new_page_order, new_i_order
 
-def nup4_inner_page_transform(page, crop, zoom, bonus_shrink_factor, printable_margin, printable_scale, nup4_position):
-    page.add_transformation(pypdf.Transformation().translate(ty=(A4_HEIGHT / zoom - (A4_HEIGHT - crop.top))))
-    if nup4_position == 0 or nup4_position == 2:
+
+def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i):
+    page.add_transformation(pypdf.Transformation().translate(ty=(A4_HEIGHT / crop.zoom - (A4_HEIGHT - crop.top))))
+    if nup4_i == 0 or nup4_i == 2:
         page.add_transformation(pypdf.Transformation().translate(tx=-crop.left))
-    elif nup4_position == 1 or nup4_position == 3:
-        page.add_transformation(pypdf.Transformation().translate(tx=(A4_WIDTH / zoom - (A4_WIDTH - crop.right))))
-    page.add_transformation(pypdf.Transformation().scale(zoom * bonus_shrink_factor, zoom * bonus_shrink_factor))
-    if nup4_position == 2 or nup4_position == 3:
-        page.add_transformation(pypdf.Transformation().translate(ty=-2*printable_margin/printable_scale))
-
-def nup4_outer_page_transform(page, bonus_shrink_factor, nup4_position):
-    page.add_transformation(pypdf.Transformation().translate(ty=(1-bonus_shrink_factor)*A4_HEIGHT))
-    if nup4_position == 0 or nup4_position == 1:
+    elif nup4_i == 1 or nup4_i == 3:
+        page.add_transformation(pypdf.Transformation().translate(tx=(A4_WIDTH / crop.zoom - (A4_WIDTH - crop.right))))
+    page.add_transformation(pypdf.Transformation().scale(crop.zoom * nup4_geometry.shrink_for_spine, crop.zoom * nup4_geometry.shrink_for_spine))
+    if nup4_i == 2 or nup4_i == 3:
+        page.add_transformation(pypdf.Transformation().translate(ty=-2*nup4_geometry.margin/nup4_geometry.shrink_for_margin))
+
+
+def nup4_outer_page_transform(page, nup4_geometry, nup4_i):
+    page.add_transformation(pypdf.Transformation().translate(ty=(1-nup4_geometry.shrink_for_spine)*A4_HEIGHT))
+    if nup4_i == 0 or nup4_i == 1:
         y_section = A4_HEIGHT
         page.mediabox.bottom = A4_HALF_HEIGHT
         page.mediabox.top    = A4_HEIGHT
-    if nup4_position == 2 or nup4_position == 3:
+    if nup4_i == 2 or nup4_i == 3:
         y_section = 0
         page.mediabox.bottom = 0
         page.mediabox.top  =  A4_HALF_HEIGHT
-    if nup4_position == 0 or nup4_position == 2:
+    if nup4_i == 0 or nup4_i == 2:
         x_section = 0
         page.mediabox.left   = 0
         page.mediabox.right  = A4_HALF_WIDTH
-    if nup4_position == 1 or nup4_position == 3:
-        page.add_transformation(pypdf.Transformation().translate(tx=(1-bonus_shrink_factor)*A4_WIDTH))
+    if nup4_i == 1 or nup4_i == 3:
+        page.add_transformation(pypdf.Transformation().translate(tx=(1-nup4_geometry.shrink_for_spine)*A4_WIDTH))
         x_section = A4_WIDTH
         page.mediabox.left   = A4_HALF_WIDTH
         page.mediabox.right  = A4_WIDTH
     page.add_transformation(pypdf.Transformation().translate(tx=x_section, ty=y_section))
     page.add_transformation(pypdf.Transformation().scale(QUARTER_SCALE_FACTOR, QUARTER_SCALE_FACTOR))
 
-def ornate_nup4(writer, args_analyze, is_front_page, new_page, printable_margin, printable_scale, bonus_shrink_factor, canvas_class):
+
+def ornate_nup4(writer, args_analyze, is_front_page, new_page, nup4_geometry, canvas_class):
     if args_analyze:
         # borders
         packet = io.BytesIO()
@@ -407,11 +448,11 @@ def ornate_nup4(writer, args_analyze, is_front_page, new_page, printable_margin,
         c.save()
         new_pdf = pypdf.PdfReader(packet)
         new_page.merge_page(new_pdf.pages[0])
-    printable_offset_x = printable_margin
-    printable_offset_y = printable_margin * A4_HEIGHT / A4_WIDTH
-    new_page.add_transformation(pypdf.Transformation().scale(printable_scale, printable_scale))
+    printable_offset_x = nup4_geometry.margin
+    printable_offset_y = nup4_geometry.margin * A4_HEIGHT / A4_WIDTH
+    new_page.add_transformation(pypdf.Transformation().scale(nup4_geometry.shrink_for_margin, nup4_geometry.shrink_for_margin))
     new_page.add_transformation(pypdf.Transformation().translate(tx=printable_offset_x, ty=printable_offset_y))
-    x_left_spine_limit = A4_HALF_WIDTH * bonus_shrink_factor
+    x_left_spine_limit = A4_HALF_WIDTH * nup4_geometry.shrink_for_spine
     x_right_spine_limit = A4_WIDTH - x_left_spine_limit
     if args_analyze or is_front_page:
         packet = io.BytesIO()
@@ -430,6 +471,7 @@ def ornate_nup4(writer, args_analyze, is_front_page, new_page, printable_margin,
         new_pdf = pypdf.PdfReader(packet)
         new_page.merge_page(new_pdf.pages[0])
 
+
 def draw_cut(canvas, x_spine_limit, direction):
     outer_start_x = x_spine_limit - 0.5 * CUT_WIDTH * direction
     inner_start_x = x_spine_limit + 0.5 * CUT_WIDTH * direction
@@ -439,6 +481,7 @@ def draw_cut(canvas, x_spine_limit, direction):
     canvas.line(x_spine_limit, end_point_y, x_spine_limit, middle_point_y)
     canvas.line(x_spine_limit, middle_point_y, outer_start_x, A4_HALF_HEIGHT)
 
+
 def main():
     args = parse_args()
     validate_inputs_first_pass(args)
@@ -453,12 +496,12 @@ def main():
     if args.nup4:
         pad_pages_to_multiple_of_8(pages_to_add)
     normalize_pages_to_A4(pages_to_add)
-    crop_at_page, zoom_at_page = collect_per_page_crops_and_zooms(args.crops, args.symmetry, pages_to_add)
+    crop_at_page = collect_per_page_crops_and_zooms(args.crops, args.symmetry, pages_to_add)
     writer = pypdf.PdfWriter()
     if args.nup4:
-        build_nup4_output(writer, pages_to_add, crop_at_page, zoom_at_page, args.print_margin, args.analyze, Canvas)
+        build_nup4_output(writer, pages_to_add, crop_at_page, args.print_margin, args.analyze, Canvas)
     else:
-        build_single_pages_output(writer, pages_to_add, crop_at_page, zoom_at_page)
+        build_single_pages_output(writer, pages_to_add, crop_at_page)
     for file in opened_files:
         file.close()
     with open(args.output_file, 'wb') as output_file: