'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')}
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.'
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 = []
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
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:
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}')
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)
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:
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')
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)})')
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)
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()
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:
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(
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: