fix a typo in the docstring
[toast/gimp_passport.git] / gimp_passport.py
index da046ee..245024f 100644 (file)
@@ -1,12 +1,21 @@
 import argparse
-from PIL import Image
+from itertools import count
+from numbers import Integral
+from typing import Tuple
+from PIL import Image, ImageDraw
 
-def make_passport(image_filename: str, fpr: float, width_mm: float, height_mm: float) -> Image:
+
+MM_PER_INCH = 25.4
+
+
+def mm_to_pixel(mm: float, dpi: Integral) -> int:
+    return int(round(mm / MM_PER_INCH * dpi))
+
+
+def make_passport(img_rgba: Image, fpr: float, width_mm: float, height_mm: float) -> Image:
     """
     :param fpr: face to picture ratio, e.g. 2/3
     """
-    img_rgba = Image.open(image_filename)
-
     # get bounding box of head
     bands = img_rgba.getbands()
     if 'A' not in bands:
@@ -14,7 +23,6 @@ def make_passport(image_filename: str, fpr: float, width_mm: float, height_mm: f
     alpha = img_rgba.getdata(bands.index('A'))
     bbox_head = alpha.getbbox()  # bounding box of head
 
-
     # get bounding box of passport image ("pic")
     left, top, right, bottom = bbox_head
     height = bottom - top
@@ -30,19 +38,142 @@ def make_passport(image_filename: str, fpr: float, width_mm: float, height_mm: f
     # cut image
     img = img_rgba.convert('RGB')  # drop alpha channel
     pic = img.crop(bbox_pic)
+    dpi = int(round(pic_height / (height_mm / MM_PER_INCH)))
+    pic.info['dpi'] = (dpi, dpi)
     return pic
 
 
-def main(source_image: str, dest_image: str):
-    pic = make_passport(source_image, 0.75, 35., 45.)
-    pic.save(dest_image)
+def num_tiles(paper_size: float, tile_size: float, margin: float, spacing: float) -> int:
+    """Returns the number of tiles that fit in the space described by the parameres.
+    All length units have to be the same (e.g. milimeter or pixel).
+
+    :param paper_size: paper width or paper height
+    :para tile_size: tile width or tile height
+    :param margin: space between paper border and first tile
+    :param spacing: space between tiles
+    """
+    size = paper_size - 2 * margin
+    return max(0, int((size + spacing) // (tile_size + spacing)))
+
+
+def num_tiles_xy(paper_size: Tuple[float, float], tile_size: Tuple[float, float],
+                 margin: float, spacing: float) -> Tuple[int, int]:
+    """Returns the number of tiles that fit in the space described by the parameters.
+    All length units have to be the same (e.g. milimeter or pixel).
+
+    :param paper_size: tuple with paper width and height
+    :param tile_size: tuple with tile width and height
+    :param margin: space between paper border and first tile
+    :param spacing: space between tiles
+    """
+    return tuple(num_tiles(ps, ts, margin, spacing) for ps, ts in zip(paper_size, tile_size))
+
+
+def line(img: Image, pos: int, axis: int) -> Image:
+    """Creates a horizontal (axis == 0) or vertical (axis == 1) line over the whole
+    width or height of the image.
+
+    :param img: Image that is modified inline.
+    :param pos: Position in pixel from left or top (0 based)
+    :param axis: 0 means horizontal, 1 means vertical
+    """
+    draw = ImageDraw.ImageDraw(img)
+    color = 'black'
+    if axis == 0:
+        draw.line(((0, pos), (img.width, pos)), color=color)
+    else:
+        draw.line(((pos, 0), (pos, img.height)), color=color)
+
+
+def make_cut_lines(img: Image, count_x: int, count_y: int, width: int, margin: int, spacing: int, 
+                   margin_mm: float,
+         spacing_mm: float) -> Image:
+    """Create and return image representing paper with specified dimensions and copy the given
+    image as often as possible to the paper.
+
+    :param img: source image that should be copied.
+    :param paper_width_mm: width of the image representing the paper
+    :param paper_height_mm: height of the image representing the paper
+    :param margin_mm: Free space from the paper borders to the edges of the tiled images.
+    :param spacing_mm: Additional space between tiled images.
+    """
+    dpi = img.info['dpi'][0]
+    paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
+    paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
+    margin_pixel = mm_to_pixel(margin_mm, dpi)
+    spacing_pixel = mm_to_pixel(spacing_mm, dpi)
+    paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
+    paper.info['dpi'] = (dpi, dpi)
+    top = margin_pixel
+    for iy in count():
+        bottom = top + img.height
+        if bottom > paper_height_pixel - margin_pixel:
+            break
+        left = margin_pixel
+        for ix in count():
+            right = left + img.width
+            if right > paper_width_pixel - margin_pixel:
+                break
+            paper.paste(img, (left, top))
+            left = right + spacing_pixel
+        top = bottom + spacing_pixel
+    return paper
+
+
+def tile(img: Image, paper_width_mm: float, paper_height_mm: float, margin_mm: float,
+         spacing_mm: float) -> Image:
+    """Create and return image representing paper with specified dimensions and copy the given
+    image as often as possible to the paper.
+
+    :param img: source image that should be copied.
+    :param paper_width_mm: width of the image representing the paper
+    :param paper_height_mm: height of the image representing the paper
+    :param margin_mm: Free space from the paper borders to the edges of the tiled images.
+    :param spacing_mm: Additional space between tiled images.
+    """
+    dpi = img.info['dpi'][0]
+    paper_width_pixel = mm_to_pixel(paper_width_mm, dpi)
+    paper_height_pixel = mm_to_pixel(paper_height_mm, dpi)
+    margin_pixel = mm_to_pixel(margin_mm, dpi)
+    spacing_pixel = mm_to_pixel(spacing_mm, dpi)
+    paper = Image.new('RGB', (paper_width_pixel, paper_height_pixel), 'white')
+    paper.info['dpi'] = (dpi, dpi)
+    top = margin_pixel
+    for iy in count():
+        bottom = top + img.height
+        if bottom > paper_height_pixel - margin_pixel:
+            break
+        left = margin_pixel
+        for ix in count():
+            right = left + img.width
+            if right > paper_width_pixel - margin_pixel:
+                break
+            paper.paste(img, (left, top))
+            left = right + spacing_pixel
+        top = bottom + spacing_pixel
+    return paper
+
+
+def main(paper_width_mm: float, paper_height_mm: float, paper_margin_mm: float,
+         photo_width_mm: float, photo_height_mm: float, photo_spacing_mm: float,
+         source_image: str, dest_image: str):
+    img = Image.open(source_image)
+    photo = make_passport(img, 0.75, photo_width_mm, photo_height_mm)
+    paper = tile(photo, paper_width_mm, paper_height_mm, paper_margin_mm, photo_spacing_mm)
+    paper.save(dest_image, dpi=paper.info['dpi'])
 
 
 if __name__ == '__main__':
     description = 'Convert image with alpha mask marking the face to passport image.'
     parser = argparse.ArgumentParser(description=description)
+    parser.add_argument('--paper-width', type=float, default=150., help='paper width in mm (default: 150)')
+    parser.add_argument('--paper-height', type=float, default=100., help='paper height in mm (default: 100)')
+    parser.add_argument('--paper-margin', type=float, default=4., help='paper margin in mm (default: 4)')
+    parser.add_argument('--photo-width', type=float, default=35., help='passport photo width in mm (default: 35)')
+    parser.add_argument('--photo-height', type=float, default=45., help='passport photo height in mm (default: 45)')
+    parser.add_argument('--photo-spacing', type=float, default=0., help='space between passport photos in mm (default: 0)')
     parser.add_argument('source', help='sourse image')
     parser.add_argument('dest', help='destination image')
     args = parser.parse_args()
-    main(args.source, args.dest)
-
+    main(args.paper_width, args.paper_height, args.paper_margin, args.photo_width, args.photo_height,
+         args.photo_spacing, args.source, args.dest)