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