From acf358640521aac0c645087084be5e65190feab0 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 24 Sep 2023 21:54:19 +0200 Subject: [PATCH] Bookmaker: greatly refactor code. --- bookmaker.py | 419 ++++++++++++++++++++++++++------------------------- 1 file changed, 216 insertions(+), 203 deletions(-) diff --git a/bookmaker.py b/bookmaker.py index 1c7240a..dc4b1cb 100755 --- a/bookmaker.py +++ b/bookmaker.py @@ -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: -- 2.30.2