From ec2d8f9facaf8cf9872c045541fc90cb4776e705 Mon Sep 17 00:00:00 2001 From: Plom Heller Date: Fri, 10 Apr 2026 02:28:43 +0200 Subject: [PATCH] Do much more with -a. --- bookmaker.py | 174 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 58 deletions(-) diff --git a/bookmaker.py b/bookmaker.py index 6878b94..cfdf5c5 100755 --- a/bookmaker.py +++ b/bookmaker.py @@ -61,13 +61,17 @@ class Page: 'Fuses PdfPage, PageCrop, and PdfTransformation.' def __init__( - self, pypdf_page: PdfPage + self, + pypdf_page: PdfPage ) -> None: self._pypdf = pypdf_page + self.set_box() self.crop = PageCrop() @property - def box(self) -> dict[str, float]: + def box( + self + ) -> dict[str, float]: 'Return dict of mediabox measures.' return {direction: getattr(self._pypdf.mediabox, direction) for direction in ('left', 'bottom', 'right', 'top')} @@ -91,34 +95,84 @@ class Page: self._pypdf.cropbox = self._pypdf.mediabox @classmethod - def new_blank(cls) -> Self: + def new_blank( + cls + ) -> Self: 'Make blank page of A4 measures.' return cls(PdfPage.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)) - def add_to_writer(self, writer: PdfWriter) -> None: + def add_to_writer( + self, + writer: PdfWriter + ) -> None: 'Add self to writer.' writer.add_page(self._pypdf) - def merge_page(self, page: Self) -> None: + def merge_page( + self, + page: Self + ) -> None: 'Merge page .to self.' self._pypdf.merge_page(page._pypdf) - def rotation(self) -> Optional[int]: + def rotation( + self + ) -> Optional[int]: 'Return PDF /Rotate instruction.' return self._pypdf.get(ROTATE_COMMAND, None) - def rotate(self, degrees: int) -> None: + def rotate(self, + degrees: int + ) -> None: 'Wrap PdfTransformation.rotate.' self._pypdf.add_transformation(PdfTransformation().rotate(degrees)) - def translate(self, tx: float = 0.0, ty: float = 0.0) -> None: + def translate( + self, + tx: float = 0.0, + ty: float = 0.0 + ) -> None: 'Wrap PdfTransformation.translate.' self._pypdf.add_transformation(PdfTransformation().translate(tx, ty)) - def scale(self, zoom: float) -> None: + def scale( + self, + zoom: float + ) -> None: 'Wrap PdfTransformation.scale.' self._pypdf.add_transformation(PdfTransformation().scale(zoom, zoom)) + def draw_lines( + self, + lines: tuple[tuple[float, float, float, float], ...], + rgb_color: tuple[float, float, float] = (0.0, 0.0, 0.0), + line_width: float = 2.0, + ) -> None: + 'Draw lines.' + min_x, min_y, max_x, max_y = self._pypdf.mediabox + packet = BytesIO() + c = Canvas(packet, pagesize=(max_x - min_x, max_y - min_y)) + c.setLineWidth(line_width) + c.setStrokeColorRGB(*rgb_color) + for line in lines: + c.line(line[0], line[1], line[2], line[3]) + c.save() + self._pypdf.merge_page(PdfReader(packet).pages[0]) + + def draw_borders( + self, + rgb_color: tuple[float, float, float] = (0.0, 0.0, 0.0), + line_width: float = 4.0, + ) -> None: + 'Draw border lines around mediabox.' + min_x, min_y, max_x, max_y = self._pypdf.mediabox + self.draw_lines(((min_x, min_y, min_x, max_y), # left + (min_x, min_y, max_x, min_y), # bottom + (max_x, min_y, max_x, max_y), # right + (min_x, max_y, max_x, max_y)), # top + rgb_color, + line_width) + class PageCrop: 'Per-page crop instructions as sizes in point and cm, and A4-zoom factor.' @@ -399,7 +453,8 @@ def parse_page_range( def args_to_pagelist( args_input_file: list[str], - args_page_range: 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 = [] @@ -423,8 +478,11 @@ def args_to_pagelist( for old_page_num in range(*parse_page_range(range_string, len(reader.pages))): new_page_num += 1 - pages += [Page.new_blank() if old_page_num >= len(reader.pages) - else Page(reader.pages[old_page_num])] + 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 {filename} page number {old_page_num+1} ' f'as new page {new_page_num}') return pages, opened_files @@ -450,8 +508,9 @@ def validate_ranges( def rotate_pages( + pages: list[Page], args_rotate_page: Optional[list[int]], - pages: list[Page] + arg_analyze: bool ) -> None: 'For pages page numbered in args_rotate_page, rotate by 90°.' if args_rotate_page: @@ -460,6 +519,8 @@ def rotate_pages( page.translate(tx=-A4_WIDTH/2, ty=-A4_HEIGHT/2) page.rotate(-90) page.translate(tx=A4_WIDTH/2, ty=A4_HEIGHT/2) + if arg_analyze: + page.draw_borders((0.75, 0.0, 0.0)) print(f'-r: rotating (by 90°) page {rotate_page}') @@ -477,7 +538,8 @@ def pad_pages_to_multiple_of_8( def normalize_pages_to_a4( - pages: list[Page] + 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) @@ -504,13 +566,15 @@ def normalize_pages_to_a4( 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, - pages: list[Page] + args_symmetry: str ) -> None: 'Calculate individual PageCrops from inputs.' if args_crops: @@ -539,6 +603,7 @@ def collect_page_croppings( 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') @@ -547,6 +612,8 @@ def build_single_pages_output( 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(writer) print(f'built page number {i+1} (of {len(pages)})') @@ -555,12 +622,12 @@ def build_nup4_output( writer: PdfWriter, pages: list[Page], args_print_margin: int, - args_analyze: str, + arg_analyze: str, ) -> 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 args_analyze: + if arg_analyze: print('-a: drawing page borders, spine limits') nup4_geometry = Nup4Geometry(args_print_margin) resort_pages_for_nup4(pages) @@ -576,7 +643,7 @@ def build_nup4_output( print(f'merged page number {page_count} (of {len(pages)})') nup4_i += 1 if nup4_i > 3: - ornate_nup4(args_analyze, is_front_page, new_page, nup4_geometry) + ornate_nup4(arg_analyze, is_front_page, new_page, nup4_geometry) new_page.add_to_writer(writer) nup4_i = 0 new_page = Page.new_blank() @@ -646,52 +713,40 @@ def nup4_outer_page_transform( def ornate_nup4( - args_analyze: str, + arg_analyze: str, is_front_page: bool, new_page: Page, nup4_geometry: Nup4Geometry, ) -> None: 'Apply nup4 line guides onto new_page.' - if args_analyze: - # borders - packet = BytesIO() - c = Canvas(packet, pagesize=A4) - c.setLineWidth(0.1) - c.line(0, A4_HEIGHT, A4_WIDTH, A4_HEIGHT) - c.line(0, A4_HEIGHT/2, A4_WIDTH, A4_HEIGHT/2) - c.line(0, 0, A4_WIDTH, 0) - c.line(0, A4_HEIGHT, 0, 0) - c.line(A4_WIDTH/2, A4_HEIGHT, A4_WIDTH/2, 0) - c.line(A4_WIDTH, A4_HEIGHT, A4_WIDTH, 0) - c.save() - new_pdf = PdfReader(packet) - new_page.merge_page(Page(new_pdf.pages[0])) + if arg_analyze: + new_page.draw_borders((0.5, 1.0, 1.0)) + new_page.draw_lines( + ((0, new_page.box['top']/2, + new_page.box['right'], new_page.box['top']/2), + (new_page.box['right']/2, new_page.box['top'], + new_page.box['right']/2, new_page.box['bottom'])), + line_width=0.1) printable_offset_x = nup4_geometry.margin printable_offset_y = nup4_geometry.margin * A4_HEIGHT / A4_WIDTH new_page.scale(nup4_geometry.shrink_for_margin) new_page.translate(tx=printable_offset_x, ty=printable_offset_y) - if not (args_analyze or is_front_page): + if not (arg_analyze or is_front_page): return x_left_spine_limit = A4_WIDTH/2 * nup4_geometry.shrink_for_spine x_right_spine_limit = A4_WIDTH - x_left_spine_limit - packet = BytesIO() - c = Canvas(packet, pagesize=A4) - if args_analyze: - # spine lines - c.setLineWidth(0.1) - c.line(x_left_spine_limit, A4_HEIGHT, x_left_spine_limit, 0) - c.line(x_right_spine_limit, A4_HEIGHT, x_right_spine_limit, 0) + if arg_analyze: + new_page.draw_lines( + ((x_left_spine_limit, A4_HEIGHT, x_left_spine_limit, 0), + (x_right_spine_limit, A4_HEIGHT, x_right_spine_limit, 0)), + line_width=0.1, rgb_color=(1.0, 0.5, 1.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) - c.save() - new_pdf = PdfReader(packet) - new_page.merge_page(Page(new_pdf.pages[0])) + draw_cut(new_page, x_left_spine_limit, 1) + draw_cut(new_page, x_right_spine_limit, -1) def draw_cut( - canvas: 'Canvas', + page: Page, x_spine_limit: float, direction: int ) -> None: @@ -701,9 +756,11 @@ def draw_cut( inner_start_x = x_spine_limit + directed_half_width middle_point_y = A4_HEIGHT/2 + MIDDLE_POINT_DEPTH * direction end_point_y = A4_HEIGHT/2 + CUT_DEPTH * direction - canvas.line(inner_start_x, A4_HEIGHT/2, x_spine_limit, end_point_y) - canvas.line(x_spine_limit, end_point_y, x_spine_limit, middle_point_y) - canvas.line(x_spine_limit, middle_point_y, outer_start_x, A4_HEIGHT/2) + page.draw_lines( + ((inner_start_x, A4_HEIGHT/2, x_spine_limit, end_point_y), + (x_spine_limit, end_point_y, x_spine_limit, middle_point_y), + (x_spine_limit, middle_point_y, outer_start_x, A4_HEIGHT/2)), + line_width=0.2) def main( @@ -713,20 +770,21 @@ def main( 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) + pages, opened_files = args_to_pagelist( + args.input_file, args.page_range, args.analyze) validate_ranges(args, len(pages)) - rotate_pages(args.rotate_page, pages) + rotate_pages(pages, args.rotate_page, args.analyze) if not args.keep_mediabox: - normalize_pages_to_a4(pages) + normalize_pages_to_a4(pages, args.analyze) if args.nup4: pad_pages_to_multiple_of_8(pages) - collect_page_croppings(args.crops, args.keep_mediabox, args.symmetry, - pages) + collect_page_croppings(pages, + args.crops, args.keep_mediabox, args.symmetry) writer = PdfWriter() if args.nup4: build_nup4_output(writer, pages, args.print_margin, args.analyze) else: - build_single_pages_output(writer, pages) + build_single_pages_output(writer, pages, args.analyze) for file in opened_files: file.close() with open(args.output_file, 'wb') as output_file: -- 2.30.2