5 from reportlab.lib.pagesizes import A4
6 a4_width, a4_height = A4
7 points_per_cm = 10 * 72 / 25.4
8 cut_depth = 1.95 * points_per_cm
9 cut_width = 1.05 * points_per_cm
10 middle_point_depth = 0.4 * points_per_cm
11 spine_limit = 1 * points_per_cm
13 desc = """bookmaker.py is a helper for optimizing PDFs of books for the production of small self-printed, self-bound physical books To this goal it offers various PDF manipulation options potentially that can also be used indepéndently and for other purposes.
18 Concatenate two PDFs A.pdf and B.pdf to COMBINED.pdf:
19 bookmaker.py --input_file A.pdf --input_file B.pdf --output_file COMBINED.pdf
21 Produce OUTPUT.pdf containing all pages of (inclusive) page number range 3-7 from INPUT.pdf:
22 bookmaker.py -i INPUT.pdf --page_range 3-7 -o OUTPUT.pdf
24 Produce COMBINED-pdf from A.pdf's first 7 pages, B.pdf's pages except its first two, and all pages of C.pdf:
25 bookmaker.py -i A.pdf -p start-7 -i B.pdf -p 3-end -i C.pdf -o COMBINED.pdf
27 Crop each page 5cm from the left, 10cm from the bottom, 2cm from the right, and 0cm from the top:
28 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --crops "5,10,2,0"
30 Include all pages from INPUT.pdf, but crop pages 10-20 by 5cm each from bottom and top:
31 bookmaker.py -i INPUT.pdf -c "10-20:0,5,0,5" -o OUTPUT.pdf
33 Same crops from on pages 10-20, but also crop all pages 30 and later by 3cm each from left and right:
34 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "10-20:0,5,0,5" -c "30-end:3,0,3,0"
36 Rotate by 90° pages 3, 5, 7; rotate page 7 once more by 90% (i.e. 180° in total):
37 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --rotate 3 -r 5 -r 7 -r 7
39 Initially declare 5cm crop from the left and 1cm crop from right, but alternate direction between even and odd pages:
40 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "5,0,1,0" -s
42 Quarter each OUTPUT.pdf page to carry 4 pages from INPUT.pdf, draw stencils into inner margins for cuts to carry binding strings:
43 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --nup4
45 Same as --nup4, but define a printable-region margin of 1.3cm to limit the space for the INPUT.pdf pages in OUTPUT.pdf page quarters:
46 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --print_margin 1.3
48 Same as -n, but draw lines marking printable-region margins, page quarts, spine margins:
49 bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --analyze
53 The target page shape so far is assumed to be A4 in portrait orientation; bookmaker.py normalizes all pages to this format before applying crops, and removes any source PDF /Rotate commands (for their production of landscape orientations).
55 The --nup4 quartering puts pages into a specific order optimized for no-tumble duplex print-outs that can easily be folded and cut into pages of a small A6 book. Each unit of 8 pages from the source PDF is mapped thus onto two subsequent pages (i.e. front and back of a printed A4 paper):
64 To facilitate this layout, --nup4 also pads the input PDF pages to a total number that is a multiple of 8, by adding empty pages.
66 (To turn this page into a tiny 8-page book, cut the paper in two on its horizontal middle line. Fold the two halves by their vertical middle lines, with pages 3-2 and 7-6 on the folds' insides. This creates two 4-page books of pages 1-4 and pages 5-8. Fold them both closed and (counter-intuitively) put the book of pages 5-8 on top of the other one (creating a temporary page order of 5,6,7,8,1,2,3,4). A binding cut stencil should be visible on the top left of this stack – cut it out (with all pages folded together) to add the same inner-margin upper cut to each page. Turn around your 8-pages stack to find the mirror image of aforementioned stencil on the stack's back's bottom, and cut it out too. Each page now has binding cuts on top and bottom of its inner margins. Swap the order of both books (back to the final page order of 1,2,3,4,5,6,7,8), and you now have an 8-pages book that can be "bound" in its binding cuts through a rubber band or the like. Repeat with the next 8-pages double-page, et cetera. (Actually, with just 8 pages, the paper may curl under the pressure of a rubber band – but go up to 32 pages or so, and the result will become quite stable.)
69 # parser = argparse.ArgumentParser(description="build print-ready book PDF")
70 parser = argparse.ArgumentParser(description=desc, epilog=epilogue, formatter_class=argparse.RawDescriptionHelpFormatter)
71 parser._optionals.title = "OPTIONS"
72 parser.add_argument("-i", "--input_file", action="append", required=True, help="input PDF file")
73 parser.add_argument("-o", "--output_file", required=True, help="output PDF file")
74 parser.add_argument("-p", "--page_range", action="append", help="page range, e.g., '3-end'")
75 parser.add_argument("-c", "--crops", action="append", help="cm crops left, bottom, right, top – e.g., '10,10,10,10'; prefix with ':'-delimited page range to limit effect")
76 parser.add_argument("-r", "--rotate_page", type=int, action="append", help="rotate page of number by 90° (usable multiple times on same page!)")
77 parser.add_argument("-s", "--symmetry", action="store_true", help="alternate horizontal crops between odd and even pages")
78 parser.add_argument("-n", "--nup4", action='store_true', help="puts 4 input pages onto 1 output page, adds binding cut stencil")
79 parser.add_argument("-a", "--analyze", action="store_true", help="in --nup4, print lines identifying spine, page borders")
80 parser.add_argument("-m", "--print_margin", type=float, default=0.43, help="print margin for --nup4 in cm (default 0.43)")
81 parser.add_argument("-H", "--long_help", action="store_true", help="show examples, explanations, additional usage notes")
82 args = parser.parse_args()
84 # select pages from input files
85 def parse_page_range(range_string, pages):
89 start, end = range_string.split('-')
90 if not (len(start) == 0 or start == "start"):
91 start_page = int(start) - 1
92 if not (len(end) == 0 or end == "end"):
94 return start_page, end_page
98 for i, input_file in enumerate(args.input_file):
99 file = open(input_file, 'rb')
100 opened_files += [file]
101 reader = pypdf.PdfReader(file)
103 if args.page_range and len(args.page_range) > i:
104 range_string = args.page_range[i]
105 start_page, end_page = parse_page_range(range_string, reader.pages)
106 for old_page_num in range(start_page, end_page):
108 page = reader.pages[old_page_num]
109 pages_to_add += [page]
110 print("-i, -p: read in %s page number %d as new page %d" % (input_file, old_page_num+1, new_page_num))
112 # if necessary, pad pages to multiple of 8
114 mod_to_8 = len(pages_to_add) % 8
116 print("-n: number of input pages %d not multiple of 8, padding to that" % len(pages_to_add))
117 for _ in range(8 - mod_to_8):
118 new_page = pypdf.PageObject.create_blank_page(width=a4_width, height=a4_height)
119 pages_to_add += [new_page]
123 for rotate_page in args.rotate_page:
124 page = pages_to_add[rotate_page - 1]
125 page.add_transformation(pypdf.Transformation().translate(tx=-a4_width/2, ty=-a4_height/2))
126 page.add_transformation(pypdf.Transformation().rotate(-90))
127 page.add_transformation(pypdf.Transformation().translate(tx=a4_width/2, ty=a4_height/2))
128 print("-r: rotating (by 90°) page", rotate_page)
130 # normalize all pages to portrait A4
131 for page in pages_to_add:
132 if "/Rotate" in page:
133 page.rotate(360 - page["/Rotate"])
134 page.mediabox.left = 0
135 page.mediabox.bottom = 0
136 page.mediabox.top = a4_height
137 page.mediabox.right = a4_width
138 page.cropbox = page.mediabox
140 # determine page crops, zooms, crop symmetry
141 crops_at_page = [(0,0,0,0)]*len(pages_to_add)
142 zoom_at_page = [1]*len(pages_to_add)
144 for crops in args.crops:
145 initial_split = crops.split(':')
146 if len(initial_split) > 1:
147 page_range = initial_split[0]
148 crops = initial_split[1]
151 crops = initial_split[0]
152 start_page, end_page = parse_page_range(page_range, pages_to_add)
153 crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm = [float(x) for x in crops.split(',')]
154 crop_left = crop_left_cm * points_per_cm
155 crop_bottom = crop_bottom_cm * points_per_cm
156 crop_right = crop_right_cm * points_per_cm
157 crop_top = crop_top_cm * points_per_cm
159 print("-c, -t: to pages %d to %d applying crops: left %.2fcm, bottom %.2fcm, right %.2fcm, top %.2fcm (but alternating left and right crop between even and odd pages)" % (start_page + 1, end_page, crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm))
161 print("-c: to pages %d to %d applying crops: left %.2fcm, bottom %.2fcm, right %.2fcm, top %.2fcm" % (start_page + 1, end_page, crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm))
162 cropped_width = a4_width - crop_left - crop_right
163 cropped_height = a4_height - crop_bottom - crop_top
165 zoom_horizontal = a4_width / (a4_width - crop_left - crop_right)
166 zoom_vertical = a4_height / (a4_height - crop_bottom - crop_top)
167 if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
168 raise ValueError("crops would create opposing zoom directions")
169 elif zoom_horizontal + zoom_vertical > 2:
170 zoom = min(zoom_horizontal, zoom_vertical)
172 zoom = max(zoom_horizontal, zoom_vertical)
173 for page_num in range(start_page, end_page):
174 if args.symmetry and page_num % 2:
175 crops_at_page[page_num] = (crop_right, crop_bottom, crop_left, crop_top)
177 crops_at_page[page_num] = (crop_left, crop_bottom, crop_right, crop_top)
178 zoom_at_page[page_num] = zoom
180 writer = pypdf.PdfWriter()
183 print("building 1-input-page-per-output-page book")
185 for i, page in enumerate(pages_to_add):
186 crop_left, crop_bottom, crop_right, crop_top = crops_at_page[i]
187 zoom = zoom_at_page[i]
188 page.add_transformation(pypdf.Transformation().translate(tx=-crop_left, ty=-crop_bottom))
189 page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
190 cropped_width = a4_width - crop_left - crop_right
191 cropped_height = a4_height - crop_bottom - crop_top
192 page.mediabox.right = cropped_width * zoom
193 page.mediabox.top = cropped_height * zoom
194 writer.add_page(page)
195 odd_page = not odd_page
196 print("built page number %d (of %d)" % (i+1, len(pages_to_add)))
199 print("-n: building 4-input-pages-per-output-page book")
200 print("-m: applying printable-area margin of %.2fcm" % args.print_margin)
202 print("-a: drawing page borders, spine limits")
204 printable_margin = args.print_margin * points_per_cm
205 printable_scale = (a4_width - 2*printable_margin)/a4_width
206 half_width = a4_width / n_pages_per_axis
207 half_height = a4_height / n_pages_per_axis
208 section_scale_factor = 1 / n_pages_per_axis
209 spine_part_of_page = (spine_limit / half_width) / printable_scale
210 bonus_shrink_factor = 1 - spine_part_of_page
216 for page in pages_to_add:
223 new_i_order += [8 * n_eights + 3,
232 new_page_order += [eight_pack[3]] # page front, upper left
233 new_page_order += [eight_pack[0]] # page front, upper right
234 new_page_order += [eight_pack[7]] # page front, lower left
235 new_page_order += [eight_pack[4]] # page front, lower right
236 new_page_order += [eight_pack[1]] # page back, upper left
237 new_page_order += [eight_pack[2]] # page back, upper right
238 new_page_order += [eight_pack[5]] # page back, lower left
239 new_page_order += [eight_pack[6]] # page back, lower right
243 for j, page in enumerate(new_page_order):
245 new_page = pypdf.PageObject.create_blank_page(width=a4_width, height=a4_height)
247 # in-section transformations: align pages on top, left-hand pages to left, right-hand to right
248 new_i = new_i_order[j]
249 crop_left, crop_bottom, crop_right, crop_top = crops_at_page[new_i]
250 zoom = zoom_at_page[new_i]
251 page.add_transformation(pypdf.Transformation().translate(ty=(a4_height / zoom - (a4_height - crop_top))))
253 page.add_transformation(pypdf.Transformation().translate(tx=-crop_left))
254 elif i == 1 or i == 3:
255 page.add_transformation(pypdf.Transformation().translate(tx=(a4_width / zoom - (a4_width - crop_right))))
256 page.add_transformation(pypdf.Transformation().scale(zoom * bonus_shrink_factor, zoom * bonus_shrink_factor))
258 page.add_transformation(pypdf.Transformation().translate(ty=-2*printable_margin/printable_scale))
260 # outer section transformations
261 page.add_transformation(pypdf.Transformation().translate(ty=(1-bonus_shrink_factor)*a4_height))
263 y_section = a4_height
264 page.mediabox.bottom = half_height
265 page.mediabox.top = a4_height
268 page.mediabox.bottom = 0
269 page.mediabox.top = half_height
272 page.mediabox.left = 0
273 page.mediabox.right = half_width
275 page.add_transformation(pypdf.Transformation().translate(tx=(1-bonus_shrink_factor)*a4_width))
277 page.mediabox.left = half_width
278 page.mediabox.right = a4_width
279 page.add_transformation(pypdf.Transformation().translate(tx=x_section, ty=y_section))
280 page.add_transformation(pypdf.Transformation().scale(section_scale_factor, section_scale_factor))
281 new_page.merge_page(page)
283 print("merged page number %d (of %d)" % (page_count, len(pages_to_add)))
286 from reportlab.pdfgen import canvas
289 packet = io.BytesIO()
290 c = canvas.Canvas(packet, pagesize=A4)
292 c.line(0, a4_height, a4_width, a4_height)
293 c.line(0, half_height, a4_width, half_height)
294 c.line(0, 0, a4_width, 0)
295 c.line(0, a4_height, 0, 0)
296 c.line(half_width, a4_height, half_width, 0)
297 c.line(a4_width, a4_height, a4_width, 0)
299 new_pdf = pypdf.PdfReader(packet)
300 new_page.merge_page(new_pdf.pages[0])
301 printable_offset_x = printable_margin
302 printable_offset_y = printable_margin * a4_height / a4_width
303 new_page.add_transformation(pypdf.Transformation().scale(printable_scale, printable_scale))
304 new_page.add_transformation(pypdf.Transformation().translate(tx=printable_offset_x, ty=printable_offset_y))
305 x_left_spine_limit = half_width * bonus_shrink_factor
306 x_right_spine_limit = a4_width - x_left_spine_limit
307 if args.analyze or front_page:
308 packet = io.BytesIO()
309 c = canvas.Canvas(packet, pagesize=A4)
313 c.line(x_left_spine_limit, a4_height, x_left_spine_limit, 0)
314 c.line(x_right_spine_limit, a4_height, x_right_spine_limit, 0)
318 start_up_left_left_x = x_left_spine_limit - 0.5 * cut_width
319 start_up_left_right_x = x_left_spine_limit + 0.5 * cut_width
320 middle_point_up_left_y = half_height + middle_point_depth
321 end_point_up_left_y = half_height + cut_depth
322 c.line(start_up_left_right_x, half_height, x_left_spine_limit, end_point_up_left_y)
323 c.line(x_left_spine_limit, end_point_up_left_y, x_left_spine_limit, middle_point_up_left_y)
324 c.line(x_left_spine_limit, middle_point_up_left_y, start_up_left_left_x, half_height)
326 start_down_right_left_x = x_right_spine_limit - 0.5 * cut_width
327 start_down_right_right_x = x_right_spine_limit + 0.5 * cut_width
328 middle_point_down_right_y = half_height - middle_point_depth
329 end_point_down_right_y = half_height - cut_depth
330 c.line(start_down_right_left_x, half_height, x_right_spine_limit, end_point_down_right_y)
331 c.line(x_right_spine_limit, end_point_down_right_y, x_right_spine_limit, middle_point_down_right_y)
332 c.line(x_right_spine_limit, middle_point_down_right_y, start_down_right_right_x, half_height)
334 if args.analyze or front_page:
336 new_pdf = pypdf.PdfReader(packet)
337 new_page.merge_page(new_pdf.pages[0])
338 writer.add_page(new_page)
340 front_page = not front_page
343 for file in opened_files:
345 with open(args.output_file, 'wb') as output_file:
346 writer.write(output_file)