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.'
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)
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,
(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)