-pages_to_add = []
-opened_files = []
-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]
- start_page, end_page = parse_page_range(range_string, reader.pages)
- for page_num in range(start_page, end_page):
- page = reader.pages[page_num]
- pages_to_add += [page]
- print("read in %s page number %d" % (input_file, page_num+1))
-
-# rotate page canvas
-if args.rotate:
- for rotate in args.rotate:
- page = pages_to_add[rotate - 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))
-
-# normalize all pages to portrait A4
-for page in pages_to_add:
- if "/Rotate" in page:
- page.rotate(360 - page["/Rotate"])
- page.mediabox.left = 0
- page.mediabox.bottom = 0
- page.mediabox.top = a4_height
- page.mediabox.right = a4_width
- page.cropbox = page.mediabox
-
-# determine page crops, zooms
-crops_at_page = [(0,0,0,0)]*len(pages_to_add)
-zoom_at_page = [1]*len(pages_to_add)
-if args.crop_range:
- for crop_range in args.crop_range:
- initial_split = crop_range.split(':')
- if len(initial_split) > 1:
- page_range = initial_split[0]
- crops = initial_split[1]
- else:
- page_range = None
- crops = initial_split[0]
- start_page, end_page = parse_page_range(page_range, pages_to_add)
- crop_left, crop_bottom, crop_right, crop_top = [float(x) for x in crops.split(',')]
- 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):
- print("Error: opposing zooms.")
- exit(1)
- elif zoom_horizontal + zoom_vertical > 2:
- zoom = min(zoom_horizontal, zoom_vertical)
- else:
- zoom = max(zoom_horizontal, zoom_vertical)
- for page_num in range(start_page, end_page):
- crops_at_page[page_num] = (crop_left, crop_bottom, crop_right, crop_top)
- zoom_at_page[page_num] = zoom
-
-writer = pypdf.PdfWriter()
-if not args.nup4:
- 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]
- if args.symmetry and odd_page:
- page.add_transformation(pypdf.Transformation().translate(tx=-crop_right, ty=-crop_bottom))
- else:
- 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
-
-else:
- points_per_mm = 2.83465
- n_pages_per_axis = 2
- spine_limit = 10
- printable_margin = args.margin * points_per_mm
- printable_scale = (a4_width - 2*printable_margin)/a4_width
- half_width = a4_width / n_pages_per_axis
- half_height = a4_height / n_pages_per_axis
- section_scale_factor = 1 / n_pages_per_axis
- spine_part_of_page = (spine_limit / half_width) / printable_scale
- bonus_shrink_factor = 1 - spine_part_of_page
- new_page_order = []
- new_i_order = []
- eight_pack = []
- mod_to_8 = len(pages_to_add) % 8
- if mod_to_8 > 0:
- 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]
- i = 0
- n_eights = 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 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")
+ parser.add_argument("-o", "--output_file", required=True, help="output PDF file")
+ parser.add_argument("-p", "--page_range", action="append", help="page range, e.g., '2-9' or '3-end' or 'start-14'")
+ parser.add_argument("-c", "--crops", action="append", help="cm crops left, bottom, right, top – e.g., '10,10,10,10'; prefix with ':'-delimited page range to limit effect")
+ parser.add_argument("-r", "--rotate_page", type=int, action="append", help="rotate page of number by 90° (usable multiple times on same page!)")
+ parser.add_argument("-s", "--symmetry", action="store_true", help="alternate horizontal crops between odd and even pages")
+ 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()
+
+ # some basic input validation
+ for filename in args.input_file:
+ if not os.path.isfile(filename):
+ raise HandledException(f"-i: {filename} is not a file")
+ try:
+ with open(filename, 'rb') as file:
+ pypdf.PdfReader(file)
+ except pypdf.errors.PdfStreamError:
+ raise HandledException(f"-i: cannot interpret {filename} as PDF file")
+ if args.page_range:
+ for p_string in args.page_range:
+ validate_page_range(p_string, "-p")
+ if len(args.page_range) > len(args.input_file):
+ raise HandledException("-p: more --page_range arguments than --input_file arguments")
+ if args.crops:
+ for c_string in args.crops:
+ initial_split = c_string.split(':')
+ if len(initial_split) > 2:
+ raise HandledException(f"-c: cropping string has multiple ':': {c_string}")
+ page_range, crops = split_crops_string(c_string)
+ crops = crops.split(",")
+ if page_range:
+ validate_page_range(page_range, "-c")
+ if len(crops) != 4:
+ raise HandledException(f"-c: cropping does not contain exactly three ',': {c_string}")
+ for crop in crops:
+ try:
+ float(crop)
+ except ValueError:
+ raise HandledException(f"-c: non-number crop in: {c_string}")
+ if args.rotate_page:
+ for r in args.rotate_page:
+ try:
+ int(r)
+ except ValueError:
+ raise HandledException(f"-r: non-integer value: {r}")
+ if r < 1:
+ raise HandledException(f"-r: value must not be <1: {r}")
+ try:
+ float(args.print_margin)
+ except ValueError:
+ raise HandledException(f"-m: non-float value: {arg.print_margin}")
+
+ return args
+
+def main():
+ args = parse_args()
+ if args.nup4:
+ try:
+ import reportlab.pdfgen.canvas
+ except ImportError:
+ raise HandledException("-n: need reportlab library installed for --nup4")
+
+ # select pages from input files
+ pages_to_add = []
+ opened_files = []
+ new_page_num = 0
+ 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]
+ 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}")
+ for old_page_num in range(start_page, end_page):
+ new_page_num += 1
+ 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}")
+
+ # we can do some more input validations now that we know how many pages output should have
+ if args.crops:
+ for c_string in args.crops:
+ page_range, _= split_crops_string(c_string)
+ if page_range:
+ start, end = parse_page_range(page_range, pages_to_add)
+ if end > len(pages_to_add):
+ raise HandledException(f"-c: page range goes beyond number of pages we're building: {page_range}")
+ if args.rotate_page:
+ for r in args.rotate_page:
+ 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:
+ 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]
+
+ # normalize all pages to portrait A4