Create function num_tiles().
[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 num_tiles(paper_size: float, tile_size: float, margin: float, spacing: float) -> int:
46     """Returns the number of tiles that fit in the space described by the parameres.
47     All length units have to be the same (e.g. milimeter or pixel).
48
49     :param paper_size: paper width or paper height
50     :para tile_size: tile width or tile height
51     :param margin: space between paper border and first tile
52     :param spacing: space between tiles
53     """
54     size = paper_size - 2 * margin
55     return max(0, int((size + spacing) // (tile_size + spacing)))
56
57
58 def tile(img: Image, paper_width_mm: float, paper_height_mm: float, margin_mm: float,
59          spacing_mm: float) -> Image:
60     """Create and return image representing paper with specified dimensions and copy the given
61     image as often as possible to the paper.
62
63     :param img: source image that should be copied.
64     :param paper_width_mm: width of the image representing the paper
65     :param paper_height_mm: height of the image representing the paper
66     :param margin_mm: Free space from the paper borders to the edges of the tiled images.
67     :param spacing_mm: Additional space between tiled images.
68     """
69     dpi = img.info['dpi'][0]
70     paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
71     paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
72     margin_pixel = mm_to_pixel(margin_mm, dpi)
73     spacing_pixel = mm_to_pixel(spacing_mm, dpi)
74     paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
75     paper.info['dpi'] = (dpi, dpi)
76     top = margin_pixel
77     for iy in count():
78         bottom = top + img.height
79         if bottom > paper_height_pixel - margin_pixel:
80             break
81         left = margin_pixel
82         for ix in count():
83             right = left + img.width
84             if right > paper_width_pixel - margin_pixel:
85                 break
86             paper.paste(img, (left, top))
87             left = right + spacing_pixel
88         top = bottom + spacing_pixel
89     return paper
90
91
92 def main(paper_width_mm: float, paper_height_mm: float, paper_margin_mm: float,
93          photo_width_mm: float, photo_height_mm: float, photo_spacing_mm: float,
94          source_image: str, dest_image: str):
95     img = Image.open(source_image)
96     photo = make_passport(img, 0.75, photo_width_mm, photo_height_mm)
97     paper = tile(photo, paper_width_mm, paper_height_mm, paper_margin_mm, photo_spacing_mm)
98     paper.save(dest_image, dpi=paper.info['dpi'])
99
100
101 if __name__ == '__main__':
102     description = 'Convert image with alpha mask marking the face to passport image.'
103     parser = argparse.ArgumentParser(description=description)
104     parser.add_argument('--paper-width', type=float, default=150., help='paper width in mm (default: 150)')
105     parser.add_argument('--paper-height', type=float, default=100., help='paper height in mm (default: 100)')
106     parser.add_argument('--paper-margin', type=float, default=4., help='paper margin in mm (default: 4)')
107     parser.add_argument('--photo-width', type=float, default=35., help='passport photo width in mm (default: 35)')
108     parser.add_argument('--photo-height', type=float, default=45., help='passport photo height in mm (default: 45)')
109     parser.add_argument('--photo-spacing', type=float, default=0., help='space between passport photos in mm (default: 0)')
110     parser.add_argument('source', help='sourse image')
111     parser.add_argument('dest', help='destination image')
112     args = parser.parse_args()
113     main(args.paper_width, args.paper_height, args.paper_margin, args.photo_width, args.photo_height,
114          args.photo_spacing, args.source, args.dest)