home · contact · privacy
Bookmaker: Only handle explicitly defined Exceptions.
[misc] / bookmaker.py
1 #!/usr/bin/env python3
2 """
3 bookmaker.py is a helper for optimizing PDFs of books for the production of small self-printed, self-bound physical books.  Towards this goal it offers various PDF manipulation options that may also be used indepéndently and for other purposes.
4 """
5 help_epilogue = """
6 EXAMPLES:
7
8 Concatenate two PDFs A.pdf and B.pdf to COMBINED.pdf:
9     bookmaker.py --input_file A.pdf --input_file B.pdf --output_file COMBINED.pdf
10
11 Produce OUTPUT.pdf containing all pages of (inclusive) page number range 3-7 from INPUT.pdf:
12     bookmaker.py -i INPUT.pdf --page_range 3-7 -o OUTPUT.pdf
13
14 Produce COMBINED.pdf from A.pdf's first 7 pages, B.pdf's pages except its first two, and all pages of C.pdf:
15     bookmaker.py -i A.pdf -p start-7 -i B.pdf -p 3-end -i C.pdf -o COMBINED.pdf
16
17 Crop each page 5cm from the left, 10cm from the bottom, 2cm from the right, and 0cm from the top:
18     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --crops "5,10,2,0"
19
20 Include all pages from INPUT.pdf, but crop pages 10-20 by 5cm each from bottom and top:
21     bookmaker.py -i INPUT.pdf -c "10-20:0,5,0,5" -o OUTPUT.pdf
22
23 Same crops for pages 10-20, but also crop all pages 30 and later by 3cm each from left and right:
24     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "10-20:0,5,0,5" -c "30-end:3,0,3,0"
25
26 Rotate by 90° pages 3, 5, 7; rotate page 7 once more by 90% (i.e. 180° in total):
27     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --rotate 3 -r 5 -r 7 -r 7
28
29 Initially declare 5cm crop from the left and 1cm crop from right, but alternate direction between even and odd pages:
30     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -c "5,0,1,0" -s
31
32 Quarter each OUTPUT.pdf page to carry 4 pages from INPUT.pdf, draw stencils into inner margins for cuts to carry binding strings:
33     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf --nup4
34
35 Same --nup4, but define a printable-region margin of 1.3cm to limit the space for the INPUT.pdf pages in OUTPUT.pdf page quarters:
36     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --print_margin 1.3
37
38 Same --nup4, but draw lines marking printable-region margins, page quarts, spine margins:
39     bookmaker.py -i INPUT.pdf -o OUTPUT.pdf -n --analyze
40
41 NOTES:
42
43 For arguments like -p, page numbers are assumed to start with 1 (not 0, which is treated as an invalid page number value).
44
45 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).
46
47 For --nup4, the -c cropping instructions do not so much erase content outside the cropped area, but rather zoom into the page in a way that maximes the cropped area as much as possible into the available per-page area between printable-area margins and the borders to the other quartered pages.  If the zoomed cropped area does not fit in neatly into its per-page area, this will preserve additional page content.
48
49 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):
50
51  (front)      (back)
52 +-------+   +-------+
53 | 4 | 1 |   | 2 | 3 |
54 |-------|   |-------|
55 | 8 | 5 |   | 6 | 7 |
56 +-------+   +-------+
57
58 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 if necessary.
59
60 (To turn above double-sided example 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 that 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.)
61 """
62 import argparse
63 import io
64 import os
65 import sys
66
67 def handled_error_exit(msg):
68     print("ERROR:", msg)
69     sys.exit(1)
70
71 try:
72     import pypdf
73 except ImportError:
74     handled_error_exit("Can't run at all without pypdf installed.")
75
76 # some constants
77 POINTS_PER_CM = 10 * 72 / 25.4
78 A4_WIDTH = 21 * POINTS_PER_CM
79 A4_HEIGHT = 29.7 * POINTS_PER_CM
80 A4 = (A4_WIDTH, A4_HEIGHT)
81 CUT_DEPTH = 1.95 * POINTS_PER_CM
82 CUT_WIDTH = 1.05 * POINTS_PER_CM
83 MIDDLE_POINT_DEPTH = 0.4 * POINTS_PER_CM
84 SPINE_LIMIT = 1 * POINTS_PER_CM
85
86 # some helpers
87 class HandledException(Exception):
88     pass
89
90 def validate_page_range(p_string, err_msg_prefix):
91     err_msg = "%s: invalid page range string: %s" % (err_msg_prefix, p_string)
92     if '-' not in p_string:
93         raise HandledException("%s: page range string lacks '-': %s" % (err_msg_prefix, p_string))
94     tokens = p_string.split("-")
95     if len(tokens) > 2:
96         raise HandledException("%s: page range string has too many '-': %s" % (err_msg_prefix, p_string))
97     for i, token in enumerate(tokens):
98         if token == "":
99             continue
100         if i == 0 and token == "start":
101             continue
102         if i == 1 and token == "end":
103             continue
104         try:
105             int(token)
106         except ValueError:
107             raise HandledException("%s: page range string carries values that are neither integer, nor 'start', nor 'end': %s" % (err_msg_prefix, p_string))
108         if int(token) < 1:
109             raise HandledException("%s: page range string may not carry page numbers <1: %s" % (err_msg_prefix, p_string))
110     start = -1
111     end = -1
112     try:
113         start = int(tokens[0])
114         end = int(tokens[1])
115     except ValueError:
116         pass
117     if start > 0 and end > 0 and start > end:
118         raise HandledException("%s: page range starts higher than it ends: %s" % (err_msg_prefix, p_string))
119
120 def split_crops_string(c_string):
121     initial_split = c_string.split(':')
122     if len(initial_split) > 1:
123         page_range = initial_split[0]
124         crops = initial_split[1]
125     else:
126         page_range = None
127         crops = initial_split[0]
128     return page_range, crops
129
130 def parse_page_range(range_string, pages):
131     start_page = 0
132     end_page = len(pages)
133     if range_string:
134         start, end = range_string.split('-')
135         if not (len(start) == 0 or start == "start"):
136             start_page = int(start) - 1
137         if not (len(end) == 0 or end == "end"):
138             end_page = int(end)
139     return start_page, end_page
140
141 def parse_args():
142     parser = argparse.ArgumentParser(description=__doc__, epilog=help_epilogue, formatter_class=argparse.RawDescriptionHelpFormatter)
143     parser.add_argument("-i", "--input_file", action="append", required=True, help="input PDF file")
144     parser.add_argument("-o", "--output_file", required=True, help="output PDF file")
145     parser.add_argument("-p", "--page_range", action="append", help="page range, e.g., '2-9' or '3-end' or 'start-14'")
146     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")
147     parser.add_argument("-r", "--rotate_page", type=int, action="append", help="rotate page of number by 90° (usable multiple times on same page!)")
148     parser.add_argument("-s", "--symmetry", action="store_true", help="alternate horizontal crops between odd and even pages")
149     parser.add_argument("-n", "--nup4", action='store_true', help="puts 4 input pages onto 1 output page, adds binding cut stencil")
150     parser.add_argument("-a", "--analyze", action="store_true", help="in --nup4, print lines identifying spine, page borders")
151     parser.add_argument("-m", "--print_margin", type=float, default=0.43, help="print margin for --nup4 in cm (default 0.43)")
152     args = parser.parse_args()
153
154     # some basic input validation
155     for filename in args.input_file:
156         if not os.path.isfile(filename):
157             raise HandledException("-i: %s is not a file" % filename)
158         try:
159             with open(filename, 'rb') as file:
160                 pypdf.PdfReader(file)
161         except pypdf.errors.PdfStreamError:
162             raise HandledException("-i: cannot interpret %s as PDF file" % filename)
163     if args.page_range:
164         for p_string in args.page_range:
165             validate_page_range(p_string, "-p")
166         if len(args.page_range) > len(args.input_file):
167             raise HandledException("more -p arguments than -i arguments")
168     if args.crops:
169         for c_string in args.crops:
170             initial_split = c_string.split(':')
171             if len(initial_split) > 2:
172                 raise HandledException("-c: cropping string has multiple ':': %s" % c_string)
173             page_range, crops = split_crops_string(c_string)
174             crops = crops.split(",")
175             if page_range:
176                 validate_page_range(page_range, "-c")
177             if len(crops) != 4:
178                 raise HandledException("-c: cropping should contain three ',': %s" % c_string)
179             for crop in crops:
180                 try:
181                     float(crop)
182                 except ValueError:
183                     raise HandledException("-c: non-number crop in %s" % c_string)
184     if args.rotate_page:
185         for r in args.rotate_page:
186             try:
187                 int(r)
188             except ValueError:
189                 raise HandledException("-r: non-integer value: %s" % r)
190             if r < 1:
191                 raise HandledException("-r: value must not be <1: %s" % r)
192     try:
193         float(args.print_margin)
194     except ValueError:
195         raise HandledException("-m: non-float value: %s" % arg.print_margin)
196
197     return args
198
199 def main():
200     args = parse_args()
201     if args.nup4:
202         try:
203             import reportlab.pdfgen.canvas
204         except ImportError:
205             raise HandledException("-n: need reportlab library installed for --nup4")
206
207     # select pages from input files
208     pages_to_add = []
209     opened_files = []
210     new_page_num = 0
211     for i, input_file in enumerate(args.input_file):
212         file = open(input_file, 'rb')
213         opened_files += [file]
214         reader = pypdf.PdfReader(file)
215         range_string = None
216         if args.page_range and len(args.page_range) > i:
217             range_string = args.page_range[i]
218         start_page, end_page = parse_page_range(range_string, reader.pages)
219         if end_page > len(reader.pages):  # no need to test start_page cause start_page > end_page is checked above
220             raise HandledException("-p: page range goes beyond pages of input file: %s" % range_string)
221         for old_page_num in range(start_page, end_page):
222             new_page_num += 1
223             page = reader.pages[old_page_num]
224             pages_to_add += [page]
225             print("-i, -p: read in %s page number %d as new page %d" % (input_file, old_page_num+1, new_page_num))
226
227     # we can do some more input validations now that we know how many pages output should have
228     if args.crops:
229         for c_string in args.crops:
230             page_range, _= split_crops_string(c_string)
231             if page_range:
232                 start, end = parse_page_range(page_range, pages_to_add)
233                 if end > len(pages_to_add):
234                      raise HandledException("-c: page range goes beyond number of pages we're building: %s" % page_range)
235     if args.rotate_page:
236         for r in args.rotate_page:
237             if r > len(pages_to_add):
238                  raise HandledException("-r: page number beyond number of pages we're building: %d" % r)
239
240     # rotate page canvas (as opposed to using PDF's /Rotate command)
241     if args.rotate_page:
242         for rotate_page in args.rotate_page:
243             page = pages_to_add[rotate_page - 1]
244             page.add_transformation(pypdf.Transformation().translate(tx=-A4_WIDTH/2, ty=-A4_HEIGHT/2))
245             page.add_transformation(pypdf.Transformation().rotate(-90))
246             page.add_transformation(pypdf.Transformation().translate(tx=A4_WIDTH/2, ty=A4_HEIGHT/2))
247             print("-r: rotating (by 90°) page", rotate_page)
248
249     # if necessary, pad pages to multiple of 8
250     if args.nup4:
251         mod_to_8 = len(pages_to_add) % 8
252         if mod_to_8 > 0:
253             print("-n: number of input pages %d not multiple of 8, padding to that" % len(pages_to_add))
254             for _ in range(8 - mod_to_8):
255                 new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
256                 pages_to_add += [new_page]
257
258     # normalize all pages to portrait A4
259     for page in pages_to_add:
260         if "/Rotate" in page:
261             page.rotate(360 - page["/Rotate"])
262         page.mediabox.left = 0
263         page.mediabox.bottom = 0
264         page.mediabox.top = A4_HEIGHT
265         page.mediabox.right = A4_WIDTH
266         page.cropbox = page.mediabox
267
268     # determine page crops, zooms, crop symmetry
269     crops_at_page = [(0,0,0,0)]*len(pages_to_add)
270     zoom_at_page = [1]*len(pages_to_add)
271     if args.crops:
272         for c_string in args.crops:
273             page_range, crops = split_crops_string(c_string)
274             start_page, end_page = parse_page_range(page_range, pages_to_add)
275             crop_left_cm, crop_bottom_cm, crop_right_cm, crop_top_cm = [float(x) for x in  crops.split(',')]
276             crop_left = crop_left_cm * POINTS_PER_CM
277             crop_bottom = crop_bottom_cm * POINTS_PER_CM
278             crop_right = crop_right_cm * POINTS_PER_CM
279             crop_top = crop_top_cm * POINTS_PER_CM
280             if args.symmetry:
281                 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))
282             else:
283                 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))
284             cropped_width  = A4_WIDTH - crop_left - crop_right
285             cropped_height = A4_HEIGHT - crop_bottom - crop_top
286             zoom = 1
287             zoom_horizontal = A4_WIDTH / (A4_WIDTH - crop_left - crop_right)
288             zoom_vertical = A4_HEIGHT / (A4_HEIGHT - crop_bottom - crop_top)
289             if (zoom_horizontal > 1 and zoom_vertical < 1) or (zoom_horizontal < 1 and zoom_vertical > 1):
290                 raise HandledException("-c: crops would create opposing zoom directions")
291             elif zoom_horizontal + zoom_vertical > 2:
292                 zoom = min(zoom_horizontal, zoom_vertical)
293             else:
294                 zoom = max(zoom_horizontal, zoom_vertical)
295             for page_num in range(start_page, end_page):
296                 if args.symmetry and page_num % 2:
297                     crops_at_page[page_num] = (crop_right, crop_bottom, crop_left, crop_top)
298                 else:
299                     crops_at_page[page_num] = (crop_left, crop_bottom, crop_right, crop_top)
300                 zoom_at_page[page_num] = zoom
301
302     writer = pypdf.PdfWriter()
303     if not args.nup4:
304         # single-page output
305         print("building 1-input-page-per-output-page book")
306         odd_page = True
307         for i, page in enumerate(pages_to_add):
308             crop_left, crop_bottom, crop_right, crop_top = crops_at_page[i]
309             zoom = zoom_at_page[i]
310             page.add_transformation(pypdf.Transformation().translate(tx=-crop_left, ty=-crop_bottom))
311             page.add_transformation(pypdf.Transformation().scale(zoom, zoom))
312             cropped_width  = A4_WIDTH - crop_left - crop_right
313             cropped_height = A4_HEIGHT - crop_bottom - crop_top
314             page.mediabox.right = cropped_width * zoom
315             page.mediabox.top = cropped_height * zoom
316             writer.add_page(page)
317             odd_page = not odd_page
318             print("built page number %d (of %d)" % (i+1, len(pages_to_add)))
319
320     else:
321         print("-n: building 4-input-pages-per-output-page book")
322         print("-m: applying printable-area margin of %.2fcm" % args.print_margin)
323         if args.analyze:
324             print("-a: drawing page borders, spine limits")
325         n_pages_per_axis = 2
326         printable_margin = args.print_margin * POINTS_PER_CM
327         printable_scale = (A4_WIDTH - 2*printable_margin)/A4_WIDTH
328         half_width = A4_WIDTH / n_pages_per_axis
329         half_height = A4_HEIGHT / n_pages_per_axis
330         section_scale_factor = 1 / n_pages_per_axis
331         spine_part_of_page = (SPINE_LIMIT / half_width) / printable_scale
332         bonus_shrink_factor = 1 - spine_part_of_page
333         new_page_order = []
334         new_i_order = []
335         eight_pack = []
336         i = 0
337         n_eights = 0
338         for page in pages_to_add:
339             if i == 0:
340                 eight_pack = []
341             eight_pack += [page]
342             i += 1
343             if i == 8:
344                 i = 0
345                 new_i_order += [8 * n_eights + 3,
346                                 8 * n_eights + 0,
347                                 8 * n_eights + 7,
348                                 8 * n_eights + 4,
349                                 8 * n_eights + 1,
350                                 8 * n_eights + 2,
351                                 8 * n_eights + 5,
352                                 8 * n_eights + 6]
353                 n_eights += 1
354                 new_page_order += [eight_pack[3]]  # page front, upper left
355                 new_page_order += [eight_pack[0]]  # page front, upper right
356                 new_page_order += [eight_pack[7]]  # page front, lower left
357                 new_page_order += [eight_pack[4]]  # page front, lower right
358                 new_page_order += [eight_pack[1]]  # page back, upper left
359                 new_page_order += [eight_pack[2]]  # page back, upper right
360                 new_page_order += [eight_pack[5]]  # page back, lower left
361                 new_page_order += [eight_pack[6]]  # page back, lower right
362         i = 0
363         page_count = 0
364         front_page = True
365         for j, page in enumerate(new_page_order):
366             if i == 0:
367                 new_page = pypdf.PageObject.create_blank_page(width=A4_WIDTH, height=A4_HEIGHT)
368
369             # in-section transformations: align pages on top, left-hand pages to left, right-hand to right
370             new_i = new_i_order[j]
371             crop_left, crop_bottom, crop_right, crop_top = crops_at_page[new_i]
372             zoom = zoom_at_page[new_i]
373             page.add_transformation(pypdf.Transformation().translate(ty=(A4_HEIGHT / zoom - (A4_HEIGHT - crop_top))))
374             if i == 0 or i == 2:
375                 page.add_transformation(pypdf.Transformation().translate(tx=-crop_left))
376             elif i == 1 or i == 3:
377                 page.add_transformation(pypdf.Transformation().translate(tx=(A4_WIDTH / zoom - (A4_WIDTH - crop_right))))
378             page.add_transformation(pypdf.Transformation().scale(zoom * bonus_shrink_factor, zoom * bonus_shrink_factor))
379             if i == 2 or i == 3:
380                 page.add_transformation(pypdf.Transformation().translate(ty=-2*printable_margin/printable_scale))
381
382             # outer section transformations
383             page.add_transformation(pypdf.Transformation().translate(ty=(1-bonus_shrink_factor)*A4_HEIGHT))
384             if i == 0 or i == 1:
385                 y_section = A4_HEIGHT
386                 page.mediabox.bottom = half_height
387                 page.mediabox.top    = A4_HEIGHT
388             if i == 2 or i == 3:
389                 y_section = 0
390                 page.mediabox.bottom = 0
391                 page.mediabox.top  =   half_height
392             if i == 0 or i == 2:
393                 x_section = 0
394                 page.mediabox.left   = 0
395                 page.mediabox.right  = half_width
396             if i == 1 or i == 3:
397                 page.add_transformation(pypdf.Transformation().translate(tx=(1-bonus_shrink_factor)*A4_WIDTH))
398                 x_section = A4_WIDTH
399                 page.mediabox.left   = half_width
400                 page.mediabox.right  = A4_WIDTH
401             page.add_transformation(pypdf.Transformation().translate(tx=x_section, ty=y_section))
402             page.add_transformation(pypdf.Transformation().scale(section_scale_factor, section_scale_factor))
403             new_page.merge_page(page)
404             page_count += 1
405             print("merged page number %d (of %d)" % (page_count, len(pages_to_add)))
406             i += 1
407             if i > 3:
408                 if args.analyze:
409                     # borders
410                     packet = io.BytesIO()
411                     c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
412                     c.setLineWidth(0.1)
413                     c.line(0, A4_HEIGHT, A4_WIDTH, A4_HEIGHT)
414                     c.line(0, half_height, A4_WIDTH, half_height)
415                     c.line(0, 0, A4_WIDTH, 0)
416                     c.line(0, A4_HEIGHT, 0, 0)
417                     c.line(half_width, A4_HEIGHT, half_width, 0)
418                     c.line(A4_WIDTH, A4_HEIGHT, A4_WIDTH, 0)
419                     c.save()
420                     new_pdf = pypdf.PdfReader(packet)
421                     new_page.merge_page(new_pdf.pages[0])
422                 printable_offset_x = printable_margin
423                 printable_offset_y = printable_margin * A4_HEIGHT / A4_WIDTH
424                 new_page.add_transformation(pypdf.Transformation().scale(printable_scale, printable_scale))
425                 new_page.add_transformation(pypdf.Transformation().translate(tx=printable_offset_x, ty=printable_offset_y))
426                 x_left_SPINE_LIMIT = half_width * bonus_shrink_factor
427                 x_right_SPINE_LIMIT = A4_WIDTH - x_left_SPINE_LIMIT
428                 if args.analyze or front_page:
429                     packet = io.BytesIO()
430                     c = reportlab.pdfgen.canvas.Canvas(packet, pagesize=A4)
431                 if args.analyze:
432                     # # spine lines
433                     c.setLineWidth(0.1)
434                     c.line(x_left_SPINE_LIMIT, A4_HEIGHT, x_left_SPINE_LIMIT, 0)
435                     c.line(x_right_SPINE_LIMIT, A4_HEIGHT, x_right_SPINE_LIMIT, 0)
436                 if front_page:
437                     c.setLineWidth(0.2)
438                     # cut upper left
439                     start_up_left_left_x = x_left_SPINE_LIMIT - 0.5 * CUT_WIDTH
440                     start_up_left_right_x = x_left_SPINE_LIMIT + 0.5 * CUT_WIDTH
441                     middle_point_up_left_y = half_height + MIDDLE_POINT_DEPTH
442                     end_point_up_left_y = half_height + CUT_DEPTH
443                     c.line(start_up_left_right_x, half_height, x_left_SPINE_LIMIT, end_point_up_left_y)
444                     c.line(x_left_SPINE_LIMIT, end_point_up_left_y, x_left_SPINE_LIMIT, middle_point_up_left_y)
445                     c.line(x_left_SPINE_LIMIT, middle_point_up_left_y, start_up_left_left_x, half_height)
446                     # cut lower right
447                     start_down_right_left_x = x_right_SPINE_LIMIT - 0.5 * CUT_WIDTH
448                     start_down_right_right_x = x_right_SPINE_LIMIT + 0.5 * CUT_WIDTH
449                     middle_point_down_right_y = half_height - MIDDLE_POINT_DEPTH
450                     end_point_down_right_y = half_height - CUT_DEPTH
451                     c.line(start_down_right_left_x, half_height, x_right_SPINE_LIMIT, end_point_down_right_y)
452                     c.line(x_right_SPINE_LIMIT, end_point_down_right_y, x_right_SPINE_LIMIT, middle_point_down_right_y)
453                     c.line(x_right_SPINE_LIMIT, middle_point_down_right_y, start_down_right_right_x, half_height)
454                 if args.analyze or front_page:
455                     c.save()
456                     new_pdf = pypdf.PdfReader(packet)
457                     new_page.merge_page(new_pdf.pages[0])
458                 writer.add_page(new_page)
459                 i = 0
460                 front_page = not front_page
461
462     # write and close
463     for file in opened_files:
464         file.close()
465     with open(args.output_file, 'wb') as output_file:
466         writer.write(output_file)
467
468
469 if __name__ == "__main__":
470     try:
471         main()
472     except HandledException as e:
473         handled_error_exit(e)