From: Plom Heller Date: Wed, 30 Sep 2026 21:43:37 +0000 (+0200) Subject: Some refactoring. X-Git-Url: https://plomlompom.com/repos/process_descriptions?a=commitdiff_plain;h=341ccef40ac127785565b3f8ae42b485698c5e5c;p=bookmaker Some refactoring. --- diff --git a/src/bookmaker/misc.py b/src/bookmaker/misc.py index ad986c5..d5a8d9e 100755 --- a/src/bookmaker/misc.py +++ b/src/bookmaker/misc.py @@ -416,10 +416,12 @@ def parse_args( type=float, default=0.43, help='print margin for --nup4 in cm (default 0.43)') - return parser.parse_args() + args = parser.parse_args() + _validate_args_syntax(args) + return args -def validate_args_syntax( +def _validate_args_syntax( args: ArgsNamespace ) -> None: 'Check command args against general syntax expectations.' @@ -532,70 +534,79 @@ def parse_page_range( return idx_start, idx_after -def args_to_pagelist( - args_input_file: list[str], - args_page_range: list[str], - arg_analyze: bool - ) -> tuple[list[Page], list[BufferedReader]]: - 'Follow args_input_file ranged by args_page_range into pages, open files.' - pages = [] - opened_files = [] - new_page_num = 0 - for i, path in enumerate([Path(filename) for filename in args_input_file]): - if not path.is_file(): - raise ArgFail('i', f'{path} is not a file') - file = path.open('rb') - opened_files += [file] - try: - reader = PdfReader(file) - except PdfStreamError as e: - for to_close in opened_files: - to_close.close() - raise ArgFail('i', f'cannot interpret {path} as PDF file') from e - range_string = None - if args_page_range and len(args_page_range) > i: - range_string = args_page_range[i] - for old_page_num in range(*parse_page_range(range_string, - len(reader.pages))): - new_page_num += 1 - page = (Page.new_blank() if old_page_num >= len(reader.pages) - else Page(reader.pages[old_page_num])) - pages += [page] - if arg_analyze: - page.draw_borders((0.75, 0.0, 0.0)) - print(f'-i, -p: read in {path} page number {old_page_num+1} ' - f'as new page {new_page_num}') - return pages, opened_files - - -def validate_ranges( - args: ArgsNamespace, - len_pages: int - ) -> None: - 'Check command args\' ranges fits into len_pages.' - if args.crops: - for c_string in args.crops: - if (page_range := split_crops_string(c_string)[0])\ - and parse_page_range(page_range, len_pages)[1] > len_pages: - raise ArgFail('c', - 'page range goes beyond number of pages ' - f'we\'re building: {page_range}') - if args.rotate_page: - for r in [r for r in args.rotate_page if r > len_pages]: - raise ArgFail('r', - 'page number beyond number of ' - f'pages we\'re building: {r}') +class Book: + 'Collection of pages.' + def __init__( + self, + input_filenames: list[str], + args_page_range: list[str], + do_analyze: bool + ) -> None: + self.pages: list[Page] = [] + self._opened_files: list[BufferedReader] = [] + new_page_num = 0 + for i, path in enumerate([Path(filename) + for filename in input_filenames]): + if not path.is_file(): + raise ArgFail('i', f'{path} is not a file') + file = path.open('rb') + self._opened_files += [file] + try: + reader = PdfReader(file) + except PdfStreamError as e: + for to_close in self._opened_files: + to_close.close() + raise ArgFail('i', + f'cannot interpret {path} as PDF file') from e + range_string = None + if args_page_range and len(args_page_range) > i: + range_string = args_page_range[i] + for old_page_num in range(*parse_page_range(range_string, + len(reader.pages))): + new_page_num += 1 + page = (Page.new_blank() if old_page_num >= len(reader.pages) + else Page(reader.pages[old_page_num])) + self.pages += [page] + if do_analyze: + page.draw_borders((0.75, 0.0, 0.0)) + print(f'-i, -p: read in {path} page number {old_page_num+1} ' + f'as new page {new_page_num}') + self._writer = PdfWriter() + + def __len__( + self + ) -> int: + return len(self.pages) -def rotate_pages( - pages: list[Page], - args_rotate_page: Optional[list[int]], - arg_analyze: bool - ) -> None: - 'For pages page numbered in args_rotate_page, rotate by 90°.' - if args_rotate_page: - for rotate_page in args_rotate_page: - page = pages[rotate_page - 1] + def validate_ranges( + self, + args_crops: tuple[str, ...], + args_rotate_page: Optional[list[int]], + ) -> None: + 'Check command args\' ranges fits into len_pages.' + len_ = len(self) + if args_crops: + for c_string in args_crops: + if (page_range := split_crops_string(c_string)[0])\ + and parse_page_range(page_range, len_)[1] > len_: + raise ArgFail('c', + 'page range goes beyond number of pages ' + f'we\'re building: {page_range}') + if args_rotate_page: + for r in [r for r in args_rotate_page if r > len_]: + raise ArgFail('r', + 'page number beyond number of ' + f'pages we\'re building: {r}') + + def rotate_pages( + self, + args_rotate_page: Optional[list[int]], + arg_analyze: bool + ) -> None: + 'For .pages numbered in args_rotate_page, rotate by 90°.' + for rotate_page in args_rotate_page or []: + page = self.pages[rotate_page - 1] page.translate(tx=-A4_WIDTH/2, ty=-A4_HEIGHT/2) page.rotate(-90) page.translate(tx=A4_WIDTH/2, ty=A4_HEIGHT/2) @@ -603,75 +614,75 @@ def rotate_pages( page.draw_borders((0.75, 0.0, 0.0)) print(f'-r: rotating (by 90°) page {rotate_page}') + def pad_pages_to_multiple_of_8( + self, + ) -> None: + 'To .pages add blank pages until its size is multiple of 8.' + old_len = len(self) + mod_to_8 = old_len % 8 + if mod_to_8 > 0: + for _ in range(8 - mod_to_8): + self.pages += [Page.new_blank()] + print(f'-n: number of input pages {old_len} not required multiple ' + f'of 8, padded to {len(self)}') + + def normalize_pages_to_a4( + self, + arg_analyze: bool + ) -> None: + 'Zoom and adjust to A4 in self.pages, enact /Rotate.' + max_x = max(page.box['right'] for page in self.pages) + max_y = max(page.box['top'] for page in self.pages) + zooms = (A4_WIDTH / max_x, A4_HEIGHT / max_y) + offsets = {'tx': 0.0, 'ty': 0.0} + zoom_by_x = zooms[0] < zooms[1] + if zoom_by_x: + zoom = zooms[0] + offsets['ty'] = (A4_HEIGHT - max_y * zoom) / 2 + else: + zoom = zooms[1] + offsets['tx'] = (A4_WIDTH - max_x * zoom) / 2 + zoom_offset = 0.01 + offsets['tx'] / POINTS_PER_CM + zoom_crop = PageCrop( + left_cm=0 if zoom_by_x else zoom_offset, + right_cm=0 if zoom_by_x else zoom_offset, + bottom_cm=zoom_offset if zoom_by_x else 0, + top_cm=zoom_offset if zoom_by_x else 0) + for page in self.pages: + page.scale(zoom) + page.translate(**offsets) + if (rotation := page.rotation()): + page.rotate(360 - rotation) + page.set_box(0, 0, A4_WIDTH, A4_HEIGHT) + page.crop.add(zoom_crop) + if arg_analyze: + page.draw_borders((0.0, 0.75, 0.0)) -def pad_pages_to_multiple_of_8( - pages: list[Page] - ) -> None: - 'To pages add blank pages until its size is multiple of 8.' - old_len = len(pages) - mod_to_8 = old_len % 8 - if mod_to_8 > 0: - for _ in range(8 - mod_to_8): - pages += [Page.new_blank()] - print(f'-n: number of input pages {old_len} not required multiple ' - f'of 8, padded to {len(pages)}') - - -def normalize_pages_to_a4( - pages: list[Page], - arg_analyze: bool - ) -> None: - 'Zoom and adjust to A4 in pages, enact /Rotate.' - max_x = max(page.box['right'] for page in pages) - max_y = max(page.box['top'] for page in pages) - zooms = (A4_WIDTH / max_x, A4_HEIGHT / max_y) - offsets = {'tx': 0.0, 'ty': 0.0} - zoom_by_x = zooms[0] < zooms[1] - if zoom_by_x: - zoom = zooms[0] - offsets['ty'] = (A4_HEIGHT - max_y * zoom) / 2 - else: - zoom = zooms[1] - offsets['tx'] = (A4_WIDTH - max_x * zoom) / 2 - zoom_offset = 0.01 + offsets['tx'] / POINTS_PER_CM - zoom_crop = PageCrop( - left_cm=0 if zoom_by_x else zoom_offset, - right_cm=0 if zoom_by_x else zoom_offset, - bottom_cm=zoom_offset if zoom_by_x else 0, - top_cm=zoom_offset if zoom_by_x else 0) - for page in pages: - page.scale(zoom) - page.translate(**offsets) - if (rotation := page.rotation()): - page.rotate(360 - rotation) - page.set_box(0, 0, A4_WIDTH, A4_HEIGHT) - page.crop.add(zoom_crop) - if arg_analyze: - page.draw_borders((0.0, 0.75, 0.0)) - - -def collect_page_croppings( - pages: list[Page], - args_crops: str, - args_keep_mediabox: bool, - args_symmetry: str - ) -> None: - 'Calculate individual PageCrops from inputs.' - if args_crops: - for c_string in args_crops: - page_range, crops = split_crops_string(c_string) - idx_start, idx_after = parse_page_range(page_range, len(pages)) - prefix = '-c, -t' if args_symmetry else '-c' - suffix = (' (but alternating left and right crop ' - 'between even and odd pages)') if args_symmetry else '' - page_crop = PageCrop(*(float(crop) for crop in crops.split(','))) - print(f'{prefix}: to pages {idx_start + 1}:{idx_after} ' - f'applying crop: {page_crop.format_in_cm}{suffix}') - for idx in range(idx_start, idx_after): - pages[idx].crop.add(page_crop, args_symmetry and idx % 2) - elif args_keep_mediabox: - for page in pages: - page.crop.add( + def collect_page_croppings( + self, + args_crops: str, + args_keep_mediabox: bool, + args_symmetry: str + ) -> None: + 'Calculate individual PageCrops from inputs.' + if args_crops: + for c_string in args_crops: + page_range, crops = split_crops_string(c_string) + idx_start, idx_after = parse_page_range(page_range, len(self)) + prefix = '-c, -t' if args_symmetry else '-c' + suffix = ((' (but alternating left and right crop ' + 'between even and odd pages)') if args_symmetry + else '') + page_crop = PageCrop(*(float(crop) + for crop in crops.split(','))) + print(f'{prefix}: to pages {idx_start + 1}:{idx_after} ' + f'applying crop: {page_crop.format_in_cm}{suffix}') + for idx in range(idx_start, idx_after): + self.pages[idx].crop.add(page_crop, + args_symmetry and idx % 2) + elif args_keep_mediabox: + for page in self.pages: + page.crop.add( PageCrop( page.box['left'] / POINTS_PER_CM, page.box['bottom'] / POINTS_PER_CM, @@ -679,72 +690,73 @@ def collect_page_croppings( (0.01 + A4_HEIGHT - page.box['top']) / POINTS_PER_CM), args_symmetry and not idx % 2) + def build_single_pages_output( + self, + arg_analyze: bool + ) -> None: + 'On each of .pages apply its page_croppings, then ._writer.add_page.' + print('building 1-input-page-per-output-page book') + for i, page in enumerate(self.pages): + page.translate(tx=-page.crop.left, ty=-page.crop.bottom) + page.scale(page.crop.zoom) + page.set_box(right=page.crop.remaining_width * page.crop.zoom) + page.set_box(top=page.crop.remaining_height * page.crop.zoom) + if arg_analyze: + page.draw_borders((0.0, 0.0, 0.75)) + page.add_to_writer(self._writer) + print(f'built page number {i+1} (of {len(self)})') -def build_single_pages_output( - writer: PdfWriter, - pages: list[Page], - arg_analyze: bool - ) -> None: - 'On each of pages apply its page_croppings, then writer.add_page.' - print('building 1-input-page-per-output-page book') - for i, page in enumerate(pages): - page.translate(tx=-page.crop.left, ty=-page.crop.bottom) - page.scale(page.crop.zoom) - page.set_box(right=page.crop.remaining_width * page.crop.zoom) - page.set_box(top=page.crop.remaining_height * page.crop.zoom) + def build_nup4_output( + self, + args_print_margin: int, + arg_analyze: bool, + ) -> 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') if arg_analyze: - page.draw_borders((0.0, 0.0, 0.75)) - page.add_to_writer(writer) - print(f'built page number {i+1} (of {len(pages)})') - - -def build_nup4_output( - writer: PdfWriter, - pages: list[Page], - args_print_margin: int, - arg_analyze: bool, - ) -> 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') - if arg_analyze: - print('-a: drawing page borders, spine limits') - nup4_geometry = Nup4Geometry(args_print_margin) - resort_pages_for_nup4(pages) - nup4_i = 0 - page_count = 0 - is_front_page = True - new_page = Page.new_blank() - for page in pages: - page.nup4_transforms(nup4_geometry, nup4_i) - new_page.merge_page(page) - page_count += 1 - print(f'merged page number {page_count} (of {len(pages)})') - nup4_i += 1 - if nup4_i > 3: - new_page.ornate_nup4(is_front_page, nup4_geometry, arg_analyze) - new_page.add_to_writer(writer) - nup4_i = 0 - new_page = Page.new_blank() - is_front_page = not is_front_page - - -def resort_pages_for_nup4( - pages: list[Page] - ) -> None: - 'Adapt pages towards PAGE_ORDER_FOR_NUP4.' - new_page_order = [] - eight_pack: list[Page] = [] - i = 0 - n_eights = 0 - for page in pages: - if i == 0: - eight_pack = [] - eight_pack += [page] - i += 1 - if i == 8: - i = 0 - for n in PAGE_ORDER_FOR_NUP4: - new_page_order += [eight_pack[n]] - n_eights += 1 - pages[:] = new_page_order[:] + print('-a: drawing page borders, spine limits') + nup4_geometry = Nup4Geometry(args_print_margin) + + new_page_order = [] + eight_pack: list[Page] = [] + i = 0 + n_eights = 0 + for page in self.pages: + if i == 0: + eight_pack = [] + eight_pack += [page] + i += 1 + if i == 8: + i = 0 + for n in PAGE_ORDER_FOR_NUP4: + new_page_order += [eight_pack[n]] + n_eights += 1 + self.pages[:] = new_page_order[:] + + nup4_i = 0 + page_count = 0 + is_front_page = True + new_page = Page.new_blank() + for page in self.pages: + page.nup4_transforms(nup4_geometry, nup4_i) + new_page.merge_page(page) + page_count += 1 + print(f'merged page number {page_count} (of {len(self)})') + nup4_i += 1 + if nup4_i > 3: + new_page.ornate_nup4(is_front_page, nup4_geometry, arg_analyze) + new_page.add_to_writer(self._writer) + nup4_i = 0 + new_page = Page.new_blank() + is_front_page = not is_front_page + + def finalize( + self, + output_filename, + ) -> None: + 'Close inputs, write output.' + for file in self._opened_files: + file.close() + with Path(output_filename).open('wb') as output_file: + self._writer.write(output_file) diff --git a/src/run.py b/src/run.py index 81abd80..84cf033 100755 --- a/src/run.py +++ b/src/run.py @@ -6,25 +6,15 @@ options that may also be used indepéndently and for other purposes. """ # standard libs -from pathlib import Path from sys import exit as sys_exit # ourselves from plomlib.setup import dependency_hint try: from bookmaker.misc import ( ArgFail, + Book, GOT_CANVAS, - PdfWriter, - args_to_pagelist, - build_nup4_output, - build_single_pages_output, - collect_page_croppings, - normalize_pages_to_a4, - pad_pages_to_multiple_of_8, parse_args, - rotate_pages, - validate_args_syntax, - validate_ranges, ) except ModuleNotFoundError as e: dependency_hint(e) @@ -34,31 +24,21 @@ def main( ) -> None: 'Full program run to be wrapped into ArgFail catcher.' args = parse_args() - validate_args_syntax(args) if args.nup4 and not GOT_CANVAS: raise ArgFail('n', 'need reportlab.pdfgen.canvas installed for --nup4') - - pages, opened_files = args_to_pagelist( - args.input_file, args.page_range, args.analyze) - validate_ranges(args, len(pages)) - - rotate_pages(pages, args.rotate_page, args.analyze) + book = Book(args.input_file, args.page_range, args.analyze) + book.validate_ranges(args.crops, args.rotate_page) + book.rotate_pages(args.rotate_page, args.analyze) if not args.keep_mediabox: - normalize_pages_to_a4(pages, args.analyze) + book.normalize_pages_to_a4(args.analyze) if args.nup4: - pad_pages_to_multiple_of_8(pages) - collect_page_croppings(pages, - args.crops, args.keep_mediabox, args.symmetry) - writer = PdfWriter() + book.pad_pages_to_multiple_of_8() + book.collect_page_croppings(args.crops, args.keep_mediabox, args.symmetry) if args.nup4: - build_nup4_output(writer, pages, args.print_margin, args.analyze) + book.build_nup4_output(args.print_margin, args.analyze) else: - build_single_pages_output(writer, pages, args.analyze) - - for file in opened_files: - file.close() - with Path(args.output_file).open('wb') as output_file: - writer.write(output_file) + book.build_single_pages_output(args.analyze) + book.finalize(args.output_file) if __name__ == '__main__':