home · contact · privacy
To DIY book maker, add option to append second PDF.
[misc] / bookmaker.py
1 #!/usr/bin/env python3
2 import pypdf 
3 import argparse
4 import io
5 from reportlab.lib.pagesizes import A4
6 a4_width, a4_height = A4
7
8 parser = argparse.ArgumentParser(description="build print-ready book PDF")
9 parser.add_argument("-i", "--input", dest="input_file", required=True, help="input PDF file")
10 parser.add_argument("-o", "--output", dest="output_file", required=True, help="output PDF file")
11 parser.add_argument("-p", "--pages", dest="page_range", help="page range, e.g., '3-end'")
12 parser.add_argument("-c", "--crop", dest="crop_range", help="crops left, bottom, right, top – e.g., '10,10,10,10'")
13 parser.add_argument("-n", "--nup4", dest="nup4", action='store_true', help="puts 4 input pages onto 1 output page")
14 parser.add_argument("-a", "--analyze", dest="analyze", action="store_true", help="print lines identifying spine, page borders")
15 parser.add_argument("-t", "--symmetry", dest="symmetry", action="store_true", help="alternate horizontal crops between odd and even pages")
16 parser.add_argument("-s", "--second", dest="second", help="append second file as input to append")
17 args = parser.parse_args()
18
19 with open(args.input_file, 'rb') as file:
20     reader = pypdf.PdfReader(file)
21
22     # determine page range
23     start_page = 0
24     end_page = len(reader.pages)
25     if args.page_range:
26         start, end = args.page_range.split('-')
27         if not (len(start) == 0 or start == "start"):
28             start_page = int(start) - 1 
29         if not (len(end) == 0 or end == "end"):
30             end_page = int(end)
31     pages_to_add = []
32     for page_num in range(start_page, end_page):
33         page = reader.pages[page_num]
34         pages_to_add += [page]
35         print("read in page number", page_num+1)
36
37     # add pages of second PDF
38     if args.second:
39         file2 = open(args.second, 'rb')
40         reader2 = pypdf.PdfReader(file2)
41         page_num = 1
42         for page in reader2.pages:
43             pages_to_add += [page]
44             print("read second PDF's page number", page_num)
45             page_num += 1
46
47     # normalize all pages to A4
48     for page in pages_to_add:
49         page.mediabox.left = 0
50         page.mediabox.bottom = 0
51         page.mediabox.top = a4_height 
52         page.mediabox.right = a4_width
53         page.cropbox = page.mediabox
54
55     # determine page crop
56     crop_left, crop_bottom, crop_right, crop_top = 0, 0, 0, 0
57     if args.crop_range:
58         crop_left, crop_bottom, crop_right, crop_top = [float(x) for x in  args.crop_range.split(',')]
59     cropped_width  = a4_width - crop_left - crop_right
60     cropped_height = a4_height - crop_bottom - crop_top  
61     zoom = 1
62     if args.crop_range:
63         zoom_horizontal = a4_width / (a4_width - crop_left - crop_right)
64         zoom_vertical = a4_height / (a4_height - crop_bottom - crop_top)
65         if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
66             print("Error: opposing zooms.")
67             exit(1)
68         elif zoom_horizontal + zoom_vertical > 2:
69             zoom = min(zoom_horizontal, zoom_vertical) 
70         else:
71             zoom = max(zoom_horizontal, zoom_vertical) 
72
73     writer = pypdf.PdfWriter()
74     if not args.nup4:
75         odd_page = True
76         for page in pages_to_add:
77             if args.symmetry and odd_page:
78                 page.add_transformation(pypdf.Transformation().translate(tx=-crop_left, ty=-crop_bottom))
79             else:
80                 page.add_transformation(pypdf.Transformation().translate(tx=-crop_right, ty=-crop_bottom))
81             page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
82             page.mediabox.right = cropped_width * zoom
83             page.mediabox.top = cropped_height * zoom
84             writer.add_page(page)
85             odd_page = not odd_page
86     else:
87         n_pages_per_axis = 2
88         points_per_mm = 2.83465
89         printable_margin = 4.3 * points_per_mm
90         printable_scale = (a4_width - 2*printable_margin)/a4_width
91         spine_limit = 10 * points_per_mm
92         half_width = a4_width / n_pages_per_axis 
93         half_height = a4_height / n_pages_per_axis 
94         section_scale_factor = 1 / n_pages_per_axis 
95         spine_part_of_page = (spine_limit / half_width) / printable_scale
96         bonus_shrink_factor = 1 - spine_part_of_page
97         new_page_order = []
98         eight_pack = []
99         mod_to_8 = len(pages_to_add) % 8
100         if mod_to_8 > 0:
101             for _ in range(8 - mod_to_8):
102                 new_page = pypdf.PageObject.create_blank_page(width=a4_width, height=a4_height)
103                 pages_to_add += [new_page]
104         i = 0
105         for page in pages_to_add:
106             if i == 0:
107                 eight_pack = []
108             eight_pack += [page]
109             i += 1
110             if i == 8:
111                 i = 0
112                 new_page_order += [eight_pack[3]]  # page front, upper left
113                 new_page_order += [eight_pack[0]]  # page front, upper right
114                 new_page_order += [eight_pack[7]]  # page front, lower left
115                 new_page_order += [eight_pack[4]]  # page front, lower right
116                 new_page_order += [eight_pack[1]]  # page back, upper left
117                 new_page_order += [eight_pack[2]]  # page back, upper right
118                 new_page_order += [eight_pack[5]]  # page back, lower left
119                 new_page_order += [eight_pack[6]]  # page back, lower right
120         i = 0
121         page_count = 0
122         front_page = True
123         for page in new_page_order:
124             if i == 0:
125                 new_page = pypdf.PageObject.create_blank_page(width=a4_width, height=a4_height)
126
127             # in-section transformations: align pages on top, left-hand pages to left, right-hand to right
128             page.add_transformation(pypdf.Transformation().translate(ty=(a4_height / zoom - (a4_height - crop_top))))
129             if i == 0 or i == 2:
130                 if args.symmetry:
131                     page.add_transformation(pypdf.Transformation().translate(tx=-crop_right))
132                 else:
133                     page.add_transformation(pypdf.Transformation().translate(tx=-crop_left))
134             elif i == 1 or i == 3:
135                 page.add_transformation(pypdf.Transformation().translate(tx=(a4_width / zoom - (a4_width - crop_right))))
136             page.add_transformation(pypdf.Transformation().scale(zoom * bonus_shrink_factor, zoom * bonus_shrink_factor))
137             if i == 2 or i == 3:
138                 page.add_transformation(pypdf.Transformation().translate(ty=-2*printable_margin/printable_scale))
139
140             # outer section transformations
141             page.add_transformation(pypdf.Transformation().translate(ty=(1-bonus_shrink_factor)*a4_height))
142             if i == 0 or i == 1:
143                 y_section = a4_height
144                 page.mediabox.bottom = half_height
145                 page.mediabox.top    = a4_height 
146             if i == 2 or i == 3:
147                 y_section = 0
148                 page.mediabox.bottom = 0 
149                 page.mediabox.top  =   half_height 
150             if i == 0 or i == 2:
151                 x_section = 0
152                 page.mediabox.left   = 0
153                 page.mediabox.right  = half_width
154             if i == 1 or i == 3:
155                 page.add_transformation(pypdf.Transformation().translate(tx=(1-bonus_shrink_factor)*a4_width))
156                 x_section = a4_width
157                 page.mediabox.left   = half_width 
158                 page.mediabox.right  = a4_width
159             page.add_transformation(pypdf.Transformation().translate(tx=x_section, ty=y_section))
160             page.add_transformation(pypdf.Transformation().scale(section_scale_factor, section_scale_factor))
161             new_page.merge_page(page)
162             page_count += 1
163             print("merged page number", page_count)
164             i += 1
165             if i > 3:
166                 from reportlab.pdfgen import canvas
167                 if args.analyze:
168                     # borders
169                     packet = io.BytesIO()
170                     c = canvas.Canvas(packet, pagesize=A4)
171                     c.setLineWidth(0.1)
172                     c.line(0, a4_height, a4_width, a4_height)
173                     c.line(0, half_height, a4_width, half_height)
174                     c.line(0, 0, a4_width, 0)
175                     c.line(0, a4_height, 0, 0)
176                     c.line(half_width, a4_height, half_width, 0)
177                     c.line(a4_width, a4_height, a4_width, 0)
178                     c.save()
179                     new_pdf = pypdf.PdfReader(packet)
180                     new_page.merge_page(new_pdf.pages[0])
181                 printable_offset_x = printable_margin
182                 printable_offset_y = printable_margin * a4_height / a4_width
183                 new_page.add_transformation(pypdf.Transformation().scale(printable_scale, printable_scale))
184                 new_page.add_transformation(pypdf.Transformation().translate(tx=printable_offset_x, ty=printable_offset_y))
185                 x_left_spine_limit = half_width * bonus_shrink_factor
186                 x_right_spine_limit = a4_width - x_left_spine_limit
187                 if args.analyze or front_page:
188                     packet = io.BytesIO()
189                     c = canvas.Canvas(packet, pagesize=A4)
190                 if args.analyze:
191                     # # spine lines
192                     c.setLineWidth(0.1)
193                     c.line(x_left_spine_limit, a4_height, x_left_spine_limit, 0)
194                     c.line(x_right_spine_limit, a4_height, x_right_spine_limit, 0)
195                 if front_page:
196                     c.setLineWidth(0.2)
197                     cut_depth = 19.5 * points_per_mm
198                     cut_width = 10.5 * points_per_mm
199                     middle_point_depth = 4 * points_per_mm 
200
201                     start_up_left_left_x = x_left_spine_limit - 0.5 * cut_width
202                     start_up_left_right_x = x_left_spine_limit + 0.5 * cut_width
203                     middle_point_up_left_y = half_height + middle_point_depth 
204                     end_point_up_left_y = half_height + cut_depth
205                     c.line(start_up_left_right_x, half_height, x_left_spine_limit, end_point_up_left_y)
206                     c.line(x_left_spine_limit, end_point_up_left_y, x_left_spine_limit, middle_point_up_left_y)
207                     c.line(x_left_spine_limit, middle_point_up_left_y, start_up_left_left_x, half_height)
208
209                     start_down_right_left_x = x_right_spine_limit - 0.5 * cut_width
210                     start_down_right_right_x = x_right_spine_limit + 0.5 * cut_width
211                     middle_point_down_right_y = half_height - middle_point_depth 
212                     end_point_down_right_y = half_height - cut_depth
213                     c.line(start_down_right_left_x, half_height, x_right_spine_limit, end_point_down_right_y)
214                     c.line(x_right_spine_limit, end_point_down_right_y, x_right_spine_limit, middle_point_down_right_y)
215                     c.line(x_right_spine_limit, middle_point_down_right_y, start_down_right_right_x, half_height)
216
217                 if args.analyze or front_page:
218                     c.save()
219                     # packet.seek(0)
220                     new_pdf = pypdf.PdfReader(packet)
221                     new_page.merge_page(new_pdf.pages[0])
222                 writer.add_page(new_page)
223                 i = 0
224                 front_page = not front_page 
225
226     with open(args.output_file, 'wb') as output_file:
227         writer.write(output_file)
228     if args.second:
229         file2.close()