def handled_error_exit(msg):
+ 'Print msg, then exit(1).'
print(f'ERROR: {msg}')
sys.exit(1)
class PageCrop:
+ 'Per-page crop instructions as sizes in point and cm, and A4-zoom factor.'
def __init__(self, left_cm=0, bottom_cm=0, right_cm=0, top_cm=0):
self.left_cm = left_cm
@property
def format_in_cm(self):
+ 'Human-readable listing of crops in cm.'
return (f'left {self.left_cm}cm, bottom {self.bottom_cm}cm, '
f'right {self.right_cm}cm, top {self.top_cm}cm')
@property
def remaining_width(self):
+ "What's left of A4_WIDTH after applying width croppings."
return A4_WIDTH - self.left - self.right
@property
def remaining_height(self):
+ "What's left of A4_WIDTH after applying height croppings."
return A4_HEIGHT - self.bottom - self.top
- def give_mirror(self):
+ def make_mirror(self):
+ 'Return PageCrop of swapped .left and .right.'
return PageCrop(left_cm=self.right_cm,
bottom_cm=self.bottom_cm,
right_cm=self.left_cm,
class Nup4Geometry:
+ 'Nup4-specific attributes, i.e. outer-page margins, spine sizes.'
def __init__(self, margin_cm):
self.margin = margin_cm * POINTS_PER_CM
def parse_args():
+ 'Collect command line arguments.'
help_epilogue = ('See README.txt for detailed usage instructions, '
'command examples, etc.')
parser = argparse.ArgumentParser(
def rotate_pages(args_rotate_page, pages_to_add):
+ 'For pages_to_add page numbered in args_rotate_page, rotate by 90°.'
if args_rotate_page:
for rotate_page in args_rotate_page:
page = pages_to_add[rotate_page - 1]
def pad_pages_to_multiple_of_8(pages_to_add):
+ 'To pages_to_add add blank pages until its size is multiple of 8.'
mod_to_8 = len(pages_to_add) % 8
if mod_to_8 > 0:
old_len = len(pages_to_add)
def normalize_pages_to_a4(pages_to_add):
+ 'Adjust pages_to_add .mediabox=.cropbox to A4, enact /Rotate inside that.'
for page in pages_to_add:
if '/Rotate' in page: # TODO: preserve rotation, but in canvas?
page.rotate(360 - page['/Rotate'])
page.cropbox = page.mediabox
-def collect_per_page_crops_and_zooms(args_crops,
- args_keep_mediabox,
- args_symmetry,
- pages_to_add):
- crop_at_page = [PageCrop()] * len(pages_to_add)
+def collect_page_croppings(args_crops,
+ args_keep_mediabox,
+ args_symmetry,
+ pages_to_add):
+ 'Calculate individual PageCrops from inputs.'
+ page_croppings = [PageCrop()] * len(pages_to_add)
if args_crops:
for c_string in args_crops:
page_range, crops = split_crops_string(c_string)
page_crop = PageCrop(*crops.split(','))
print(f'{prefix}: to pages {idx_start + 1}:{idx_after} '
f'applying crop: {page_crop.format_in_cm}{suffix}')
- for page_num in range(idx_start, idx_after):
- if args_symmetry and page_num % 2:
- crop_at_page[page_num] = page_crop.give_mirror()
+ for n_page in range(idx_start, idx_after):
+ if args_symmetry and n_page % 2:
+ page_croppings[n_page] = page_crop.make_mirror()
else:
- crop_at_page[page_num] = page_crop
+ page_croppings[n_page] = page_crop
elif args_keep_mediabox:
- for page_num, page in enumerate(pages_to_add):
- crop_at_page[page_num] = PageCrop(
+ for n_page, page in enumerate(pages_to_add):
+ page_croppings[n_page] = PageCrop(
page.mediabox.left / POINTS_PER_CM,
page.mediabox.bottom / POINTS_PER_CM,
(0.01 + A4_WIDTH - page.mediabox.right) / POINTS_PER_CM,
(0.01 + A4_HEIGHT - page.mediabox.top) / POINTS_PER_CM)
- if args_symmetry and not page_num % 2:
- crop_at_page[page_num] = crop_at_page[page_num].give_mirror()
- return crop_at_page
+ if args_symmetry and not n_page % 2:
+ page_croppings[n_page] = page_croppings[n_page].make_mirror()
+ return page_croppings
-def build_single_pages_output(writer, pages_to_add, crop_at_page):
+def build_single_pages_output(writer, pages_to_add, page_croppings):
+ 'On each of pages_to_add apply its page_croppings, then writer.add_page.'
print('building 1-input-page-per-output-page book')
- odd_page = True
+ odd_page = True # TODO: removable?
for i, page in enumerate(pages_to_add):
page.add_transformation(pypdf.Transformation().translate(
- tx=-crop_at_page[i].left, ty=-crop_at_page[i].bottom))
+ tx=-page_croppings[i].left, ty=-page_croppings[i].bottom))
page.add_transformation(pypdf.Transformation().scale(
- crop_at_page[i].zoom, crop_at_page[i].zoom))
+ page_croppings[i].zoom, page_croppings[i].zoom))
page.mediabox.right\
- = crop_at_page[i].remaining_width * crop_at_page[i].zoom
+ = page_croppings[i].remaining_width * page_croppings[i].zoom
page.mediabox.top\
- = crop_at_page[i].remaining_height * crop_at_page[i].zoom
+ = page_croppings[i].remaining_height * page_croppings[i].zoom
writer.add_page(page)
- odd_page = not odd_page
+ odd_page = not odd_page # TODO: removable?
print(f'built page number {i+1} (of {len(pages_to_add)})')
def build_nup4_output(writer,
pages_to_add,
- crop_at_page,
+ page_croppings,
args_print_margin,
args_analyze,
canvas_class):
+ 'Build nup4 pages from inputs.'
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)
+ pages_to_add, old_indices = resort_pages_for_nup4(pages_to_add)
nup4_i = 0
page_count = 0
is_front_page = True
if nup4_i == 0:
new_page = pypdf.PageObject.create_blank_page(
width=A4_WIDTH, height=A4_HEIGHT)
- corrected_i = new_i_order[i]
+ corrected_i = old_indices[i]
nup4_inner_page_transform(
- page, crop_at_page[corrected_i], nup4_geometry, nup4_i)
+ page, page_croppings[corrected_i], nup4_geometry, nup4_i)
nup4_outer_page_transform(page, nup4_geometry, nup4_i)
new_page.merge_page(page)
page_count += 1
def resort_pages_for_nup4(pages_to_add):
+ 'Adapt pages_to_add towards PAGE_ORDER_FOR_NUP4.'
new_page_order = []
- new_i_order = []
+ old_indices = []
eight_pack = []
i = 0
n_eights = 0
if i == 8:
i = 0
for n in PAGE_ORDER_FOR_NUP4:
- new_i_order += [8 * n_eights + n]
+ old_indices += [8 * n_eights + n]
new_page_order += [eight_pack[n]]
n_eights += 1
- return new_page_order, new_i_order
+ return new_page_order, old_indices
def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i):
+ 'Apply to page crop instructions adequate to position in nup4 geometry.'
page.add_transformation(pypdf.Transformation().translate(
ty=A4_HEIGHT / crop.zoom - (A4_HEIGHT - crop.top)))
if nup4_i in {0, 2}:
def nup4_outer_page_transform(page, nup4_geometry, nup4_i):
+ 'Shrink and position page into nup4_geometry as per its position nup4_i.'
page.add_transformation(pypdf.Transformation().translate(
ty=(1-nup4_geometry.shrink_for_spine)*A4_HEIGHT))
if nup4_i in {0, 1}:
new_page,
nup4_geometry,
canvas_class):
+ 'Apply nup4 line guides onto new_page.'
if args_analyze:
# borders
packet = io.BytesIO()
def draw_cut(canvas, x_spine_limit, direction):
+ 'Into canvas draw book binding cut guide at x_spine_limit into 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
def main():
+ 'Full program run to be wrapped into ArgFail catcher.'
args = parse_args()
validate_args_syntax(args)
if args.nup4:
pad_pages_to_multiple_of_8(pages_to_add)
if not args.keep_mediabox:
normalize_pages_to_a4(pages_to_add)
- crop_at_page = collect_per_page_crops_and_zooms(
- args.crops, args.keep_mediabox, args.symmetry, pages_to_add)
+ page_croppings = collect_page_croppings(args.crops,
+ args.keep_mediabox,
+ args.symmetry,
+ pages_to_add)
writer = pypdf.PdfWriter()
if args.nup4:
build_nup4_output(writer,
pages_to_add,
- crop_at_page,
+ page_croppings,
args.print_margin,
args.analyze,
Canvas)
else:
- build_single_pages_output(writer, pages_to_add, crop_at_page)
+ build_single_pages_output(writer, pages_to_add, page_croppings)
for file in opened_files:
file.close()
with open(args.output_file, 'wb') as output_file: