+ 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")
+ 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