fix a typo in the docstring
[toast/gimp_passport.git] / gimp_passport.py
1 import argparse
2 from itertools import count
3 from numbers import Integral
4 from typing import Tuple
5 from PIL import Image, ImageDraw
6
7
8 MM_PER_INCH = 25.4
9
10
11 def mm_to_pixel(mm: float, dpi: Integral) -> int:
12     return int(round(mm / MM_PER_INCH * dpi))
13
14
15 def make_passport(img_rgba: Image, fpr: float, width_mm: float, height_mm: float) -> Image:
16     """
17     :param fpr: face to picture ratio, e.g. 2/3
18     """
19     # get bounding box of head
20     bands = img_rgba.getbands()
21     if 'A' not in bands:
22         raise ValueError('Image has no transparency (needed for marking the head).')
23     alpha = img_rgba.getdata(bands.index('A'))
24     bbox_head = alpha.getbbox()  # bounding box of head
25
26     # get bounding box of passport image ("pic")
27     left, top, right, bottom = bbox_head
28     height = bottom - top
29     width = right - left
30     pic_height = int(round(height / fpr))
31     pic_width = int(round(pic_height * width_mm / height_mm))
32     pic_top = top - (pic_height - height) // 2
33     pic_bottom = pic_top + pic_height
34     pic_left = left - (pic_width - width) // 2
35     pic_right = pic_left + pic_width
36     bbox_pic = pic_left, pic_top, pic_right, pic_bottom
37
38     # cut image
39     img = img_rgba.convert('RGB')  # drop alpha channel
40     pic = img.crop(bbox_pic)
41     dpi = int(round(pic_height / (height_mm / MM_PER_INCH)))
42     pic.info['dpi'] = (dpi, dpi)
43     return pic
44
45
46 def num_tiles(paper_size: float, tile_size: float, margin: float, spacing: float) -> int:
47     """Returns the number of tiles that fit in the space described by the parameres.
48     All length units have to be the same (e.g. milimeter or pixel).
49
50     :param paper_size: paper width or paper height
51     :para tile_size: tile width or tile height
52     :param margin: space between paper border and first tile
53     :param spacing: space between tiles
54     """
55     size = paper_size - 2 * margin
56     return max(0, int((size + spacing) // (tile_size + spacing)))
57
58
59 def num_tiles_xy(paper_size: Tuple[float, float], tile_size: Tuple[float, float],
60                  margin: float, spacing: float) -> Tuple[int, int]:
61     """Returns the number of tiles that fit in the space described by the parameters.
62     All length units have to be the same (e.g. milimeter or pixel).
63
64     :param paper_size: tuple with paper width and height
65     :param tile_size: tuple with tile width and height
66     :param margin: space between paper border and first tile
67     :param spacing: space between tiles
68     """
69     return tuple(num_tiles(ps, ts, margin, spacing) for ps, ts in zip(paper_size, tile_size))
70
71
72 def line(img: Image, pos: int, axis: int) -> Image:
73     """Creates a horizontal (axis == 0) or vertical (axis == 1) line over the whole
74     width or height of the image.
75
76     :param img: Image that is modified inline.
77     :param pos: Position in pixel from left or top (0 based)
78     :param axis: 0 means horizontal, 1 means vertical
79     """
80     draw = ImageDraw.ImageDraw(img)
81     color = 'black'
82     if axis == 0:
83         draw.line(((0, pos), (img.width, pos)), color=color)
84     else:
85         draw.line(((pos, 0), (pos, img.height)), color=color)
86
87
88 def make_cut_lines(img: Image, count_x: int, count_y: int, width: int, margin: int, spacing: int, 
89                    margin_mm: float,
90          spacing_mm: float) -> Image:
91     """Create and return image representing paper with specified dimensions and copy the given
92     image as often as possible to the paper.
93
94     :param img: source image that should be copied.
95     :param paper_width_mm: width of the image representing the paper
96     :param paper_height_mm: height of the image representing the paper
97     :param margin_mm: Free space from the paper borders to the edges of the tiled images.
98     :param spacing_mm: Additional space between tiled images.
99     """
100     dpi = img.info['dpi'][0]
101     paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
102     paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
103     margin_pixel = mm_to_pixel(margin_mm, dpi)
104     spacing_pixel = mm_to_pixel(spacing_mm, dpi)
105     paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
106     paper.info['dpi'] = (dpi, dpi)
107     top = margin_pixel
108     for iy in count():
109         bottom = top + img.height
110         if bottom > paper_height_pixel - margin_pixel:
111             break
112         left = margin_pixel
113         for ix in count():
114             right = left + img.width
115             if right > paper_width_pixel - margin_pixel:
116                 break
117             paper.paste(img, (left, top))
118             left = right + spacing_pixel
119         top = bottom + spacing_pixel
120     return paper
121
122
123 def tile(img: Image, paper_width_mm: float, paper_height_mm: float, margin_mm: float,
124          spacing_mm: float) -> Image:
125     """Create and return image representing paper with specified dimensions and copy the given
126     image as often as possible to the paper.
127
128     :param img: source image that should be copied.
129     :param paper_width_mm: width of the image representing the paper
130     :param paper_height_mm: height of the image representing the paper
131     :param margin_mm: Free space from the paper borders to the edges of the tiled images.
132     :param spacing_mm: Additional space between tiled images.
133     """
134     dpi = img.info['dpi'][0]
135     paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
136     paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
137     margin_pixel = mm_to_pixel(margin_mm, dpi)
138     spacing_pixel = mm_to_pixel(spacing_mm, dpi)
139     paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
140     paper.info['dpi'] = (dpi, dpi)
141     top = margin_pixel
142     for iy in count():
143         bottom = top + img.height
144         if bottom > paper_height_pixel - margin_pixel:
145             break
146         left = margin_pixel
147         for ix in count():
148             right = left + img.width
149             if right > paper_width_pixel - margin_pixel:
150                 break
151             paper.paste(img, (left, top))
152             left = right + spacing_pixel
153         top = bottom + spacing_pixel
154     return paper
155
156
157 def main(paper_width_mm: float, paper_height_mm: float, paper_margin_mm: float,
158          photo_width_mm: float, photo_height_mm: float, photo_spacing_mm: float,
159          source_image: str, dest_image: str):
160     img = Image.open(source_image)
161     photo = make_passport(img, 0.75, photo_width_mm, photo_height_mm)
162     paper = tile(photo, paper_width_mm, paper_height_mm, paper_margin_mm, photo_spacing_mm)
163     paper.save(dest_image, dpi=paper.info['dpi'])
164
165
166 if __name__ == '__main__':
167     description = 'Convert image with alpha mask marking the face to passport image.'
168     parser = argparse.ArgumentParser(description=description)
169     parser.add_argument('--paper-width', type=float, default=150., help='paper width in mm (default: 150)')
170     parser.add_argument('--paper-height', type=float, default=100., help='paper height in mm (default: 100)')
171     parser.add_argument('--paper-margin', type=float, default=4., help='paper margin in mm (default: 4)')
172     parser.add_argument('--photo-width', type=float, default=35., help='passport photo width in mm (default: 35)')
173     parser.add_argument('--photo-height', type=float, default=45., help='passport photo height in mm (default: 45)')
174     parser.add_argument('--photo-spacing', type=float, default=0., help='space between passport photos in mm (default: 0)')
175     parser.add_argument('source', help='sourse image')
176     parser.add_argument('dest', help='destination image')
177     args = parser.parse_args()
178     main(args.paper_width, args.paper_height, args.paper_margin, args.photo_width, args.photo_height,
179          args.photo_spacing, args.source, args.dest)