home · contact · privacy
Some refactoring. master
authorPlom Heller <plom@plomlompom.com>
Wed, 30 Sep 2026 21:43:37 +0000 (23:43 +0200)
committerPlom Heller <plom@plomlompom.com>
Wed, 30 Sep 2026 21:43:37 +0000 (23:43 +0200)
src/bookmaker/misc.py
src/run.py

index ad986c53615a0165c896fdb4681426ef34df3889..d5a8d9ecc1cdc6654c6d6bc33414f1dc08a02a91 100755 (executable)
@@ -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)
index 81abd80d0778565c7e2e9b8287a0a04f2eb29799..84cf03349363253040388cfb57d567db2ae1f2b0 100755 (executable)
@@ -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__':