From: Plom Heller Date: Sun, 5 Apr 2026 21:48:44 +0000 (+0200) Subject: Add typing. X-Git-Url: https://plomlompom.com/repos/booking/%22https:/validator.w3.org/task?a=commitdiff_plain;h=0819ce91282c1f330fb216bd77bb994d92763f86;p=bookmaker Add typing. --- diff --git a/bookmaker.py b/bookmaker.py index 65da0a6..b60576d 100755 --- a/bookmaker.py +++ b/bookmaker.py @@ -8,9 +8,12 @@ import argparse 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) @@ -41,7 +44,13 @@ PAGE_ORDER_FOR_NUP4 = (3, 0, 7, 4, 1, 2, 5, 6) 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 @@ -60,26 +69,36 @@ class PageCrop: 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, @@ -90,7 +109,10 @@ class PageCrop: 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 @@ -103,13 +125,20 @@ class Nup4Geometry: 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.') @@ -170,9 +199,15 @@ def parse_args(): 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}') @@ -220,8 +255,8 @@ def validate_args_syntax(args): 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: @@ -247,7 +282,9 @@ def validate_args_syntax(args): 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: @@ -259,7 +296,10 @@ def split_crops_string(c_string): 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) @@ -272,7 +312,10 @@ def parse_page_range(range_string, 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 = [] @@ -305,7 +348,10 @@ def args_to_pagelist(args_input_file, args_page_range): 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: @@ -323,7 +369,10 @@ def validate_ranges(args, pages_to_add): 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: @@ -336,7 +385,9 @@ def rotate_pages(args_rotate_page, pages_to_add): 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: @@ -349,11 +400,14 @@ def pad_pages_to_multiple_of_8(pages_to_add): 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 @@ -394,7 +448,11 @@ def collect_page_croppings(args_crops, 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? @@ -412,12 +470,14 @@ def build_single_pages_output(writer, pages_to_add, page_croppings): 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') @@ -451,11 +511,13 @@ def build_nup4_output(writer, 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: @@ -472,7 +534,12 @@ def resort_pages_for_nup4(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))) @@ -490,7 +557,11 @@ def nup4_inner_page_transform(page, crop, nup4_geometry, nup4_i): 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)) @@ -502,6 +573,7 @@ def nup4_outer_page_transform(page, nup4_geometry, nup4_i): 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 @@ -518,11 +590,13 @@ def nup4_outer_page_transform(page, nup4_geometry, nup4_i): 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 @@ -556,15 +630,19 @@ def ornate_nup4(args_analyze, 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 @@ -575,7 +653,8 @@ def draw_cut(canvas, x_spine_limit, 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)