home · contact · privacy
Bookmaker: greatly refactor code.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 24 Sep 2023 19:54:19 +0000 (21:54 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 24 Sep 2023 19:54:19 +0000 (21:54 +0200)
bookmaker.py

index 1c7240a8fd8c5d29d6357d57e54a2affee2e65b4..dc4b1cbc50c2b03af5f7d61a52dc3bdb4f82ed0e 100755 (executable)
@@ -87,72 +87,12 @@ CUT_WIDTH = 1.05 * POINTS_PER_CM
 MIDDLE_POINT_DEPTH = 0.4 * POINTS_PER_CM
 SPINE_LIMIT = 1 * POINTS_PER_CM
 QUARTER_SCALE_FACTOR = 0.5
-PAGE_ORDER = (3,0,7,4,1,2,5,6)
+PAGE_ORDER_FOR_NUP4 = (3,0,7,4,1,2,5,6)
 
 # some helpers
 class HandledException(Exception):
     pass
 
-def validate_page_range(p_string, err_msg_prefix):
-    prefix = f"{err_msg_prefix}: page range string"
-    if '-' not in p_string:
-        raise HandledException(f"{prefix} lacks '-': {p_string}")
-    tokens = p_string.split("-")
-    if len(tokens) > 2:
-        raise HandledException(f"{prefix} has too many '-': {p_string}")
-    for i, token in enumerate(tokens):
-        if token == "":
-            continue
-        if i == 0 and token == "start":
-            continue
-        if i == 1 and token == "end":
-            continue
-        try:
-            int(token)
-        except ValueError:
-            raise HandledException(f"{prefix} carries value neither integer, nor 'start', nor 'end': {p_string}")
-        if int(token) < 1:
-            raise HandledException(f"{prefix} carries page number <1: {p_string}")
-    start = -1
-    end = -1
-    try:
-        start = int(tokens[0])
-        end = int(tokens[1])
-    except ValueError:
-        pass
-    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:
-        page_range = initial_split[0]
-        crops = initial_split[1]
-    else:
-        page_range = None
-        crops = initial_split[0]
-    return page_range, crops
-
-def parse_page_range(range_string, pages):
-    start_page = 0
-    end_page = len(pages)
-    if range_string:
-        start, end = range_string.split('-')
-        if not (len(start) == 0 or start == "start"):
-            start_page = int(start) - 1
-        if not (len(end) == 0 or end == "end"):
-            end_page = int(end)
-    return start_page, end_page
-
-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
-    middle_point_y =  A4_HALF_HEIGHT + MIDDLE_POINT_DEPTH * direction
-    end_point_y =  A4_HALF_HEIGHT + CUT_DEPTH * direction
-    canvas.line(inner_start_x, A4_HALF_HEIGHT, x_spine_limit, end_point_y)
-    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 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")
@@ -164,9 +104,9 @@ def parse_args():
     parser.add_argument("-n", "--nup4", action='store_true', help="puts 4 input pages onto 1 output page, adds binding cut stencil")
     parser.add_argument("-a", "--analyze", action="store_true", help="in --nup4, print lines identifying spine, page borders")
     parser.add_argument("-m", "--print_margin", type=float, default=0.43, help="print margin for --nup4 in cm (default 0.43)")
-    args = parser.parse_args()
+    return parser.parse_args()
 
-    # some basic input validation
+def validate_inputs_first_pass(args):
     for filename in args.input_file:
         if not os.path.isfile(filename):
             raise HandledException(f"-i: {filename} is not a file")
@@ -209,27 +149,68 @@ def parse_args():
     except ValueError:
         raise HandledException(f"-m: non-float value: {arg.print_margin}")
 
-    return args
-
-def main():
-    args = parse_args()
-    if args.nup4:
+def validate_page_range(p_string, err_msg_prefix):
+    prefix = f"{err_msg_prefix}: page range string"
+    if '-' not in p_string:
+        raise HandledException(f"{prefix} lacks '-': {p_string}")
+    tokens = p_string.split("-")
+    if len(tokens) > 2:
+        raise HandledException(f"{prefix} has too many '-': {p_string}")
+    for i, token in enumerate(tokens):
+        if token == "":
+            continue
+        if i == 0 and token == "start":
+            continue
+        if i == 1 and token == "end":
+            continue
         try:
-            import reportlab.pdfgen.canvas
-        except ImportError:
-            raise HandledException("-n: need reportlab library installed for --nup4")
+            int(token)
+        except ValueError:
+            raise HandledException(f"{prefix} carries value neither integer, nor 'start', nor 'end': {p_string}")
+        if int(token) < 1:
+            raise HandledException(f"{prefix} carries page number <1: {p_string}")
+    start = -1
+    end = -1
+    try:
+        start = int(tokens[0])
+        end = int(tokens[1])
+    except ValueError:
+        pass
+    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:
+        page_range = initial_split[0]
+        crops = initial_split[1]
+    else:
+        page_range = None
+        crops = initial_split[0]
+    return page_range, crops
+
+def parse_page_range(range_string, pages):
+    start_page = 0
+    end_page = len(pages)
+    if range_string:
+        start, end = range_string.split('-')
+        if not (len(start) == 0 or start == "start"):
+            start_page = int(start) - 1
+        if not (len(end) == 0 or end == "end"):
+            end_page = int(end)
+    return start_page, end_page
 
-    # select pages from input files
+def read_inputs_to_pagelist(args_input_file, args_page_range):
     pages_to_add = []
     opened_files = []
     new_page_num = 0
-    for i, input_file in enumerate(args.input_file):
+    for i, input_file in enumerate(args_input_file):
         file = open(input_file, 'rb')
         opened_files += [file]
         reader = pypdf.PdfReader(file)
         range_string = None
-        if args.page_range and len(args.page_range) > i:
-            range_string = args.page_range[i]
+        if args_page_range and len(args_page_range) > i:
+            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 HandledException(f"-p: page range goes beyond pages of input file: {range_string}")
@@ -238,8 +219,9 @@ def main():
             page = reader.pages[old_page_num]
             pages_to_add += [page]
             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
 
-    # we can do some more input validations now that we know how many pages output should have
+def validate_inputs_second_pass(args, pages_to_add):
     if args.crops:
         for c_string in args.crops:
             page_range, _= split_crops_string(c_string)
@@ -252,27 +234,27 @@ def main():
             if r > len(pages_to_add):
                  raise HandledException(f"-r: page number beyond number of pages we're building: {r}")
 
-    # rotate page canvas (as opposed to using PDF's /Rotate command)
-    if args.rotate_page:
-        for rotate_page in args.rotate_page:
+def rotate_pages(args_rotate_page, pages_to_add):
+    if args_rotate_page:
+        for rotate_page in args_rotate_page:
             page = pages_to_add[rotate_page - 1]
             page.add_transformation(pypdf.Transformation().translate(tx=-A4_WIDTH/2, ty=-A4_HEIGHT/2))
             page.add_transformation(pypdf.Transformation().rotate(-90))
             page.add_transformation(pypdf.Transformation().translate(tx=A4_WIDTH/2, ty=A4_HEIGHT/2))
             print(f"-r: rotating (by 90°) page {rotate_page}")
 
-    # if necessary, pad pages to multiple of 8
-    if args.nup4:
-        mod_to_8 = len(pages_to_add) % 8
-        if mod_to_8 > 0:
-            print(f"-n: number of input pages {len(pages_to_add)} not multiple of 8, padding to that")
-            for _ in range(8 - mod_to_8):
-                new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
-                pages_to_add += [new_page]
+def pad_pages_to_multiple_of_8(pages_to_add):
+    mod_to_8 = len(pages_to_add) % 8
+    if mod_to_8 > 0:
+        old_len = len(pages_to_add)
+        for _ in range(8 - mod_to_8):
+            new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
+            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)}")
 
-    # normalize all pages to portrait A4
+def normalize_pages_to_A4(pages_to_add):
     for page in pages_to_add:
-        if "/Rotate" in page:
+        if "/Rotate" in page:  # TODO: preserve rotation, but in canvas?
             page.rotate(360 - page["/Rotate"])
         page.mediabox.left = 0
         page.mediabox.bottom = 0
@@ -280,11 +262,11 @@ def main():
         page.mediabox.right = A4_WIDTH
         page.cropbox = page.mediabox
 
-    # determine page crops, zooms, crop symmetry
+def collect_per_page_crops_and_zooms(args_crops, args_symmetry, pages_to_add):
     crops_at_page = [(0,0,0,0)]*len(pages_to_add)
     zoom_at_page = [1]*len(pages_to_add)
-    if args.crops:
-        for c_string in args.crops:
+    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)
             crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm = [float(x) for x in  crops.split(',')]
@@ -292,8 +274,8 @@ def main():
             crop_bottom = crop_bottom_cm * POINTS_PER_CM
             crop_right = crop_right_cm * POINTS_PER_CM
             crop_top = crop_top_cm * POINTS_PER_CM
-            prefix = "-c, -t" if args.symmetry else "-c"
-            suffix = " (but alternating left and right crop between even and odd pages)" if args.symmetry else ""
+            prefix = "-c, -t" if args_symmetry else "-c"
+            suffix = " (but alternating left and right crop between even and odd pages)" if args_symmetry else ""
             print(f"{prefix}: to pages {start_page + 1} to {end_page} applying crops: left {crop_left_cm}cm, bottom {crop_bottom_cm}cm, right {crop_right_cm}cm, top {crop_top_cm}cm{suffix}")
             cropped_width  = A4_WIDTH - crop_left - crop_right
             cropped_height = A4_HEIGHT - crop_bottom - crop_top
@@ -307,32 +289,147 @@ def main():
             else:
                 zoom = max(zoom_horizontal, zoom_vertical)
             for page_num in range(start_page, end_page):
-                if args.symmetry and page_num % 2:
+                if args_symmetry and page_num % 2:
                     crops_at_page[page_num] = (crop_right, crop_bottom, crop_left, crop_top)
                 else:
                     crops_at_page[page_num] = (crop_left, crop_bottom, crop_right, crop_top)
                 zoom_at_page[page_num] = zoom
+    return crops_at_page, zoom_at_page
+
+def build_single_pages_output(writer, pages_to_add, crops_at_page, zoom_at_page):
+    print("building 1-input-page-per-output-page book")
+    odd_page = True
+    for i, page in enumerate(pages_to_add):
+        crop_left, crop_bottom, crop_right, crop_top = crops_at_page[i]
+        zoom = zoom_at_page[i]
+        page.add_transformation(pypdf.Transformation().translate(tx=-crop_left, ty=-crop_bottom))
+        page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
+        cropped_width  = A4_WIDTH - crop_left - crop_right
+        cropped_height = A4_HEIGHT - crop_bottom - crop_top
+        page.mediabox.right = cropped_width * zoom
+        page.mediabox.top = cropped_height * zoom
+        writer.add_page(page)
+        odd_page = not odd_page
+        print(f"built page number {i+1} (of {len(pages_to_add)})")
+
+def resort_pages_for_nup4(pages_to_add):
+    new_page_order = []
+    new_i_order = []
+    eight_pack = []
+    i = 0
+    n_eights = 0
+    for page in pages_to_add:
+        if i == 0:
+            eight_pack = []
+        eight_pack += [page]
+        i += 1
+        if i == 8:
+            i = 0
+            for n in PAGE_ORDER_FOR_NUP4:
+                new_i_order += [8 * n_eights + n]
+                new_page_order += [eight_pack[n]]
+            n_eights += 1
+    return new_page_order, new_i_order
+
+def nup4_inner_page_transform(page, crops, zoom, bonus_shrink_factor, printable_margin, printable_scale, nup4_position):
+    crop_left, crop_bottom, crop_right, crop_top = crops
+    page.add_transformation(pypdf.Transformation().translate(ty=(A4_HEIGHT / zoom - (A4_HEIGHT - crop_top))))
+    if nup4_position == 0 or nup4_position == 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:
+        y_section = A4_HEIGHT
+        page.mediabox.bottom = A4_HALF_HEIGHT
+        page.mediabox.top    = A4_HEIGHT
+    if nup4_position == 2 or nup4_position == 3:
+        y_section = 0
+        page.mediabox.bottom = 0
+        page.mediabox.top  =  A4_HALF_HEIGHT
+    if nup4_position == 0 or nup4_position == 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))
+        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):
+    if args_analyze:
+        # borders
+        packet = io.BytesIO()
+        c = canvas_class(packet, pagesize=A4)
+        c.setLineWidth(0.1)
+        c.line(0, A4_HEIGHT, A4_WIDTH, A4_HEIGHT)
+        c.line(0, A4_HALF_HEIGHT, A4_WIDTH, A4_HALF_HEIGHT)
+        c.line(0, 0, A4_WIDTH, 0)
+        c.line(0, A4_HEIGHT, 0, 0)
+        c.line(A4_HALF_WIDTH, A4_HEIGHT, A4_HALF_WIDTH, 0)
+        c.line(A4_WIDTH, A4_HEIGHT, A4_WIDTH, 0)
+        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))
+    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_right_spine_limit = A4_WIDTH - x_left_spine_limit
+    if args_analyze or is_front_page:
+        packet = io.BytesIO()
+        c = canvas_class(packet, pagesize=A4)
+    if args_analyze:
+        # spine lines
+        c.setLineWidth(0.1)
+        c.line(x_left_spine_limit, A4_HEIGHT, x_left_spine_limit, 0)
+        c.line(x_right_spine_limit, A4_HEIGHT, x_right_spine_limit, 0)
+    if is_front_page:
+        c.setLineWidth(0.2)
+        draw_cut(c, x_left_spine_limit, (1))
+        draw_cut(c, x_right_spine_limit, (-1))
+    if args_analyze or is_front_page:
+        c.save()
+        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
+    middle_point_y =  A4_HALF_HEIGHT + MIDDLE_POINT_DEPTH * direction
+    end_point_y =  A4_HALF_HEIGHT + CUT_DEPTH * direction
+    canvas.line(inner_start_x, A4_HALF_HEIGHT, x_spine_limit, end_point_y)
+    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)
+    if args.nup4:
+        try:
+            from reportlab.pdfgen.canvas import Canvas
+        except ImportError:
+            raise HandledException("-n: need reportlab.pdfgen.canvas installed for --nup4")
+    pages_to_add, opened_files = read_inputs_to_pagelist(args.input_file, args.page_range)
+    validate_inputs_second_pass(args, pages_to_add)
+    rotate_pages(args.rotate_page, pages_to_add)
+    if args.nup4:
+        pad_pages_to_multiple_of_8(pages_to_add)
+    normalize_pages_to_A4(pages_to_add)
+    crops_at_page, zoom_at_page = collect_per_page_crops_and_zooms(args.crops, args.symmetry, pages_to_add)
     writer = pypdf.PdfWriter()
     if not args.nup4:
-        # single-page output
-        print("building 1-input-page-per-output-page book")
-        odd_page = True
-        for i, page in enumerate(pages_to_add):
-            crop_left, crop_bottom, crop_right, crop_top = crops_at_page[i]
-            zoom = zoom_at_page[i]
-            page.add_transformation(pypdf.Transformation().translate(tx=-crop_left, ty=-crop_bottom))
-            page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
-            cropped_width  = A4_WIDTH - crop_left - crop_right
-            cropped_height = A4_HEIGHT - crop_bottom - crop_top
-            page.mediabox.right = cropped_width * zoom
-            page.mediabox.top = cropped_height * zoom
-            writer.add_page(page)
-            odd_page = not odd_page
-            print(f"built page number {i+1} (of {len(pages_to_add)})")
-
+        build_single_pages_output(writer, pages_to_add, crops_at_page, zoom_at_page)
     else:
-        # --nup4 output
         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:
@@ -341,109 +438,25 @@ def main():
         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
-        new_page_order = []
-        new_i_order = []
-        eight_pack = []
-        i = 0
-        n_eights = 0
-        for page in pages_to_add:
-            if i == 0:
-                eight_pack = []
-            eight_pack += [page]
-            i += 1
-            if i == 8:
-                i = 0
-                for n in PAGE_ORDER:
-                    new_i_order += [8 * n_eights + n]
-                    new_page_order += [eight_pack[n]]
-                n_eights += 1
+        pages_to_add, new_i_order = resort_pages_for_nup4(pages_to_add)
         i = 0
         page_count = 0
-        front_page = True
-        for j, page in enumerate(new_page_order):
+        is_front_page = True
+        for j, page in enumerate(pages_to_add):
             if i == 0:
                 new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
-
-            # in-section transformations: align pages on top, left-hand pages to left, right-hand to right
             new_i = new_i_order[j]
-            crop_left, crop_bottom, crop_right, crop_top = crops_at_page[new_i]
-            zoom = zoom_at_page[new_i]
-            page.add_transformation(pypdf.Transformation().translate(ty=(A4_HEIGHT / zoom - (A4_HEIGHT - crop_top))))
-            if i == 0 or i == 2:
-                page.add_transformation(pypdf.Transformation().translate(tx=-crop_left))
-            elif i == 1 or i == 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 i == 2 or i == 3:
-                page.add_transformation(pypdf.Transformation().translate(ty=-2*printable_margin/printable_scale))
-
-            # outer section transformations
-            page.add_transformation(pypdf.Transformation().translate(ty=(1-bonus_shrink_factor)*A4_HEIGHT))
-            if i == 0 or i == 1:
-                y_section = A4_HEIGHT
-                page.mediabox.bottom = A4_HALF_HEIGHT
-                page.mediabox.top    = A4_HEIGHT
-            if i == 2 or i == 3:
-                y_section = 0
-                page.mediabox.bottom = 0
-                page.mediabox.top  =  A4_HALF_HEIGHT
-            if i == 0 or i == 2:
-                x_section = 0
-                page.mediabox.left   = 0
-                page.mediabox.right  = A4_HALF_WIDTH
-            if i == 1 or i == 3:
-                page.add_transformation(pypdf.Transformation().translate(tx=(1-bonus_shrink_factor)*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))
+            nup4_inner_page_transform(page, crops_at_page[new_i], zoom_at_page[new_i], bonus_shrink_factor, printable_margin, printable_scale, i)
+            nup4_outer_page_transform(page, bonus_shrink_factor, i)
             new_page.merge_page(page)
             page_count += 1
             print(f"merged page number {page_count} (of {len(pages_to_add)})")
             i += 1
             if i > 3:
-                if args.analyze:
-                    # borders
-                    packet = io.BytesIO()
-                    c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
-                    c.setLineWidth(0.1)
-                    c.line(0, A4_HEIGHT, A4_WIDTH, A4_HEIGHT)
-                    c.line(0, A4_HALF_HEIGHT, A4_WIDTH, A4_HALF_HEIGHT)
-                    c.line(0, 0, A4_WIDTH, 0)
-                    c.line(0, A4_HEIGHT, 0, 0)
-                    c.line(A4_HALF_WIDTH, A4_HEIGHT, A4_HALF_WIDTH, 0)
-                    c.line(A4_WIDTH, A4_HEIGHT, A4_WIDTH, 0)
-                    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))
-                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_right_spine_limit = A4_WIDTH - x_left_spine_limit
-                if args.analyze or front_page:
-                    packet = io.BytesIO()
-                    c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
-                if args.analyze:
-                    # spine lines
-                    c.setLineWidth(0.1)
-                    c.line(x_left_spine_limit, A4_HEIGHT, x_left_spine_limit, 0)
-                    c.line(x_right_spine_limit, A4_HEIGHT, x_right_spine_limit, 0)
-                if front_page:
-                    c.setLineWidth(0.2)
-                    draw_cut(c, x_left_spine_limit, (1))
-                    draw_cut(c, x_right_spine_limit, (-1))
-                if args.analyze or front_page:
-                    c.save()
-                    new_pdf = pypdf.PdfReader(packet)
-                    new_page.merge_page(new_pdf.pages[0])
+                ornate_nup4(writer, args.analyze, is_front_page, new_page, printable_margin, printable_scale, bonus_shrink_factor, Canvas)
                 writer.add_page(new_page)
                 i = 0
-                front_page = not front_page
-
-    # write and close
+                is_front_page = not is_front_page
     for file in opened_files:
         file.close()
     with open(args.output_file, 'wb') as output_file: