X-Git-Url: https://plomlompom.com/repos/condition_descriptions?a=blobdiff_plain;ds=sidebyside;f=bookmaker.py;h=2dd96532e7f8876e3f7218b88a99a6a27e75e86f;hb=47391e94c5c3676c652e7530c8cbbe96e9ca6da0;hp=dc4b1cbc50c2b03af5f7d61a52dc3bdb4f82ed0e;hpb=acf358640521aac0c645087084be5e65190feab0;p=misc diff --git a/bookmaker.py b/bookmaker.py index dc4b1cb..2dd9653 100755 --- a/bookmaker.py +++ b/bookmaker.py @@ -63,6 +63,7 @@ import argparse import io import os import sys +from collections import namedtuple def handled_error_exit(msg): print(f"ERROR: {msg}") @@ -85,14 +86,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 + +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") @@ -106,6 +157,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): @@ -149,6 +201,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: @@ -179,6 +232,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: @@ -189,6 +243,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) @@ -200,6 +255,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 = [] @@ -221,6 +277,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: @@ -234,6 +291,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: @@ -243,6 +301,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: @@ -252,6 +311,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? @@ -262,56 +322,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): - crops_at_page = [(0,0,0,0)]*len(pages_to_add) - zoom_at_page = [1]*len(pages_to_add) + crop_at_page = [PageCrop()] * 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) - crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm = [float(x) for x in crops.split(',')] - crop_left = crop_left_cm * POINTS_PER_CM - 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 "" - 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 - zoom = 1 - 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 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: - crops_at_page[page_num] = (crop_right, crop_bottom, crop_left, crop_top) + crop_at_page[page_num] = page_crop.give_mirror() 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 + crop_at_page[page_num] = page_crop + return crop_at_page + -def build_single_pages_output(writer, pages_to_add, crops_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): - 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 + page.add_transformation(pypdf.Transformation().translate(tx=-crop_at_page[i].left, ty=-crop_at_page[i].bottom)) + 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, 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") + nup4_geometry = Nup4Geometry(args_print_margin) + pages_to_add, new_i_order = resort_pages_for_nup4(pages_to_add) + nup4_i = 0 + page_count = 0 + is_front_page = True + for i, page in enumerate(pages_to_add): + 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], 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_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_i = 0 + is_front_page = not is_front_page + + def resort_pages_for_nup4(pages_to_add): new_page_order = [] new_i_order = [] @@ -331,40 +400,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, 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: + +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_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() @@ -379,11 +450,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() @@ -402,6 +473,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 @@ -411,6 +483,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) @@ -425,38 +498,12 @@ def main(): 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) + crop_at_page = collect_per_page_crops_and_zooms(args.crops, args.symmetry, pages_to_add) writer = pypdf.PdfWriter() - if not args.nup4: - build_single_pages_output(writer, pages_to_add, crops_at_page, zoom_at_page) + if args.nup4: + build_nup4_output(writer, pages_to_add, crop_at_page, args.print_margin, args.analyze, Canvas) else: - 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 - pages_to_add, new_i_order = resort_pages_for_nup4(pages_to_add) - i = 0 - page_count = 0 - 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) - new_i = new_i_order[j] - 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: - 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 - is_front_page = not is_front_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: