Support parameter --photo-spacing in main() and command line.
[toast/gimp_passport.git] / gimp_passport.py
1 import argparse
2 from itertools import count
3 from numbers import Integral
4 from PIL import Image
5
6
7 MM_PER_INCH = 25.4
8
9
10 def mm_to_pixel(mm: float, dpi: Integral) -> int:
11     return int(round(mm / MM_PER_INCH * dpi))
12
13
14 def make_passport(img_rgba: Image, fpr: float, width_mm: float, height_mm: float) -> Image:
15     """
16     :param fpr: face to picture ratio, e.g. 2/3
17     """
18     # get bounding box of head
19     bands = img_rgba.getbands()
20     if 'A' not in bands:
21         raise ValueError('Image has no transparency (needed for marking the head).')
22     alpha = img_rgba.getdata(bands.index('A'))
23     bbox_head = alpha.getbbox()  # bounding box of head
24
25     # get bounding box of passport image ("pic")
26     left, top, right, bottom = bbox_head
27     height = bottom - top
28     width = right - left
29     pic_height = int(round(height / fpr))
30     pic_width = int(round(pic_height * width_mm / height_mm))
31     pic_top = top - (pic_height - height) // 2
32     pic_bottom = pic_top + pic_height
33     pic_left = left - (pic_width - width) // 2
34     pic_right = pic_left + pic_width
35     bbox_pic = pic_left, pic_top, pic_right, pic_bottom
36
37     # cut image
38     img = img_rgba.convert('RGB')  # drop alpha channel
39     pic = img.crop(bbox_pic)
40     dpi = int(round(pic_height / (height_mm / MM_PER_INCH)))
41     pic.info['dpi'] = (dpi, dpi)
42     return pic
43
44
45 def tile(img: Image, paper_width_mm: float, paper_height_mm: float, margin_mm: float,
46          spacing_mm: float) -> Image:
47     """Create and return image representing paper with specified dimensions and copy the given
48     image as often as possible to the paper.
49
50     :param img: source image that should be copied.
51     :param paper_width_mm: width of the image representing the paper
52     :param paper_height_mm: height of the image representing the paper
53     :param margin_mm: Free space from the paper borders to the edges of the tiled images.
54     :param spacing_mm: Additional space between tiled images.
55     """
56     dpi = img.info['dpi'][0]
57     paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
58     paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
59     margin_pixel = mm_to_pixel(margin_mm, dpi)
60     spacing_pixel = mm_to_pixel(spacing_mm, dpi)
61     paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
62     paper.info['dpi'] = (dpi, dpi)
63     top = margin_pixel
64     for iy in count():
65         bottom = top + img.height
66         if bottom > paper_height_pixel - margin_pixel:
67             break
68         left = margin_pixel
69         for ix in count():
70             right = left + img.width
71             if right > paper_width_pixel - margin_pixel:
72                 break
73             paper.paste(img, (left, top))
74             left = right + spacing_pixel
75         top = bottom + spacing_pixel
76     return paper
77
78
79 def main(paper_width_mm: float, paper_height_mm: float, paper_margin_mm: float,
80          photo_width_mm: float, photo_height_mm: float, photo_spacing_mm: float,
81          source_image: str, dest_image: str):
82     img = Image.open(source_image)
83     photo = make_passport(img, 0.75, photo_width_mm, photo_height_mm)
84     paper = tile(photo, paper_width_mm, paper_height_mm, paper_margin_mm, photo_spacing_mm)
85     paper.save(dest_image, dpi=paper.info['dpi'])
86
87
88 if __name__ == '__main__':
89     description = 'Convert image with alpha mask marking the face to passport image.'
90     parser = argparse.ArgumentParser(description=description)
91     parser.add_argument('--paper-width', type=float, default=150., help='paper width in mm (default: 150)')
92     parser.add_argument('--paper-height', type=float, default=100., help='paper height in mm (default: 100)')
93     parser.add_argument('--paper-margin', type=float, default=4., help='paper margin in mm (default: 4)')
94     parser.add_argument('--photo-width', type=float, default=35., help='passport photo width in mm (default: 35)')
95     parser.add_argument('--photo-height', type=float, default=45., help='passport photo height in mm (default: 45)')
96     parser.add_argument('--photo-spacing', type=float, default=0., help='space between passport photos in mm (default: 0)')
97     parser.add_argument('source', help='sourse image')
98     parser.add_argument('dest', help='destination image')
99     args = parser.parse_args()
100     main(args.paper_width, args.paper_height, args.paper_margin, args.photo_width, args.photo_height,
101          args.photo_spacing, args.source, args.dest)