import io
import os
import sys
+from typing import Any, Optional
-def handled_error_exit(msg):
+def handled_error_exit(
+ msg: str
+ ) -> None:
'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):
+ def __init__(
+ self,
+ left_cm: int = 0,
+ bottom_cm: int = 0,
+ right_cm: int = 0,
+ top_cm: int = 0
+ ) -> None:
self.left_cm = left_cm
self.bottom_cm = bottom_cm
self.right_cm = right_cm
else:
self.zoom = max(zoom_horizontal, zoom_vertical)
- def __str__(self):
+ def __str__(
+ self
+ ) -> str:
return str(vars(self))
@property
- def format_in_cm(self):
+ def format_in_cm(
+ self
+ ) -> str:
'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):
+ def remaining_width(
+ self
+ ) -> float:
"What's left of A4_WIDTH after applying width croppings."
return A4_WIDTH - self.left - self.right
@property
- def remaining_height(self):
+ def remaining_height(
+ self
+ ) -> float:
"What's left of A4_WIDTH after applying height croppings."
return A4_HEIGHT - self.bottom - self.top
- def make_mirror(self):
+ def make_mirror(
+ self
+ ) -> 'PageCrop':
'Return PageCrop of swapped .left and .right.'
return PageCrop(left_cm=self.right_cm,
bottom_cm=self.bottom_cm,
class Nup4Geometry:
'Nup4-specific attributes, i.e. outer-page margins, spine sizes.'
- def __init__(self, margin_cm):
+ def __init__(
+ self,
+ margin_cm: int
+ ) -> None:
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
class ArgFail(Exception):
'Collects relevant command character, followed by explanation.'
- def __init__(self, arg_char, msg, *args, **kwargs):
+ def __init__(
+ self,
+ arg_char: str,
+ msg: str,
+ *args,
+ **kwargs
+ ) -> None:
super().__init__(*args, **kwargs)
self.arg_char = arg_char
self.msg = msg
-def parse_args():
+def parse_args(
+ ) -> argparse.Namespace:
'Collect command line arguments.'
help_epilogue = ('See README.txt for detailed usage instructions, '
'command examples, etc.')
return parser.parse_args()
-def validate_args_syntax(args):
+def validate_args_syntax(
+ args: argparse.Namespace
+ ) -> None:
'Check command args against general syntax expectations.'
- def validate_page_range(pr_string, arg_char):
+
+ def validate_page_range(
+ pr_string: str,
+ arg_char: str
+ ) -> None:
prefix = 'page range string'
if '-' not in pr_string:
raise ArgFail(arg_char, f'{prefix} lacks "-": {pr_string}')
if len(initial_split) > 2:
raise ArgFail('c',
f'cropping string has multiple ":": {c_string}')
- page_range, crops = split_crops_string(c_string)
- crops = crops.split(',')
+ page_range, crops_str = split_crops_string(c_string)
+ crops = crops_str.split(',')
if page_range:
validate_page_range(page_range, 'c')
if len(crops) != 4:
raise ArgFail('m', f'non-float value: {args.print_margin}')
-def split_crops_string(c_string):
+def split_crops_string(
+ c_string: str
+ ) -> tuple[Optional[str], str]:
'If c_string contains ":" return before and after, else None and c_string.'
initial_split = c_string.split(':', maxsplit=1)
if len(initial_split) > 1:
return page_range, crops
-def parse_page_range(range_string, pages):
+def parse_page_range(
+ range_string: Optional[str],
+ pages: list[pypdf.PageObject]
+ ) -> tuple[int, int]:
'Based on actual pages size read range_string into range limit indices.'
idx_start = 0
idx_after = len(pages)
return idx_start, idx_after
-def args_to_pagelist(args_input_file, args_page_range):
+def args_to_pagelist(
+ args_input_file: list[str],
+ args_page_range: list[str]
+ ) -> tuple[list[pypdf.PageObject], list[io.BufferedReader]]:
'Follow args_input_file ranged by args_page_range into pages, open files.'
pages_to_add = []
opened_files = []
return pages_to_add, opened_files
-def validate_ranges(args, pages_to_add):
+def validate_ranges(
+ args: argparse.Namespace,
+ pages_to_add: list[pypdf.PageObject]
+ ) -> None:
'Check command args\' ranges fit into pages_to_add count.'
if args.crops:
for c_string in args.crops:
f'pages we\'re building: {r}')
-def rotate_pages(args_rotate_page, pages_to_add):
+def rotate_pages(
+ args_rotate_page: list[int],
+ pages_to_add: list[pypdf.PageObject]
+ ) -> None:
'For pages_to_add page numbered in args_rotate_page, rotate by 90°.'
if args_rotate_page:
for rotate_page in args_rotate_page:
print(f'-r: rotating (by 90°) page {rotate_page}')
-def pad_pages_to_multiple_of_8(pages_to_add):
+def pad_pages_to_multiple_of_8(
+ pages_to_add: list[pypdf.PageObject]
+ ) -> None:
'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:
f'of 8, padded to {len(pages_to_add)}')
-def normalize_pages_to_a4(pages_to_add):
+def normalize_pages_to_a4(
+ pages_to_add: list[pypdf.PageObject]
+ ) -> None:
'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'])
+ rotation: int = page['/Rotate'] # type: ignore
+ page.rotate(360 - rotation)
page.mediabox.left = 0
page.mediabox.bottom = 0
page.mediabox.top = A4_HEIGHT
return page_croppings
-def build_single_pages_output(writer, pages_to_add, page_croppings):
+def build_single_pages_output(
+ writer: pypdf.PdfWriter,
+ pages_to_add: list[pypdf.PageObject],
+ page_croppings: list[PageCrop]
+ ) -> None:
'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 # TODO: removable?
print(f'built page number {i+1} (of {len(pages_to_add)})')
-def build_nup4_output(writer,
- pages_to_add,
- page_croppings,
- args_print_margin,
- args_analyze,
- canvas_class):
+def build_nup4_output(
+ writer: pypdf.PdfWriter,
+ pages_to_add: list[pypdf.PageObject],
+ page_croppings: list[PageCrop],
+ args_print_margin: int,
+ args_analyze: str,
+ canvas_class: Any
+ ) -> None:
'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')
is_front_page = not is_front_page
-def resort_pages_for_nup4(pages_to_add):
+def resort_pages_for_nup4(
+ pages_to_add: list[pypdf.PageObject]
+ ) -> tuple[list[pypdf.PageObject], list[int]]:
'Adapt pages_to_add towards PAGE_ORDER_FOR_NUP4.'
new_page_order = []
old_indices = []
- eight_pack = []
+ eight_pack: list[pypdf.PageObject] = []
i = 0
n_eights = 0
for page in pages_to_add:
return new_page_order, old_indices
-def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i):
+def nup4_inner_page_transform(
+ page: pypdf.PageObject,
+ crop: PageCrop,
+ nup4_geometry: Nup4Geometry,
+ nup4_i: int
+ ) -> None:
'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)))
ty=-2*nup4_geometry.margin/nup4_geometry.shrink_for_margin))
-def nup4_outer_page_transform(page, nup4_geometry, nup4_i):
+def nup4_outer_page_transform(
+ page: pypdf.PageObject,
+ nup4_geometry: Nup4Geometry,
+ nup4_i: int
+ ) -> None:
'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))
y_section = 0
page.mediabox.bottom = 0
page.mediabox.top = A4_HALF_HEIGHT
+ x_section: float
if nup4_i in {0, 2}:
x_section = 0
page.mediabox.left = 0
QUARTER_SCALE_FACTOR))
-def ornate_nup4(args_analyze,
- is_front_page,
- new_page,
- nup4_geometry,
- canvas_class):
+def ornate_nup4(
+ args_analyze: str,
+ is_front_page: bool,
+ new_page: pypdf.PageObject,
+ nup4_geometry: Nup4Geometry,
+ canvas_class
+ ) -> None:
'Apply nup4 line guides onto new_page.'
if args_analyze:
# borders
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))
+ 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):
+def draw_cut(
+ canvas: Any,
+ x_spine_limit: float,
+ direction: int
+ ) -> None:
'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
canvas.line(x_spine_limit, middle_point_y, outer_start_x, A4_HALF_HEIGHT)
-def main():
+def main(
+ ) -> None:
'Full program run to be wrapped into ArgFail catcher.'
args = parse_args()
validate_args_syntax(args)