Skip to content

Instantly share code, notes, and snippets.

@nilsreichardt
Created July 11, 2025 09:59
Show Gist options
  • Save nilsreichardt/c5e56898a24eff8ef1ab1b9a2ff258ce to your computer and use it in GitHub Desktop.
Save nilsreichardt/c5e56898a24eff8ef1ab1b9a2ff258ce to your computer and use it in GitHub Desktop.
US Passport Image to German Passport Image
#!/usr/bin/env python3
"""
make_passport_sheet.py
Create a 10 × 15 cm print sheet containing six German-passport-size photos
(35 mm × 45 mm) from a square 2 × 2-inch source image.
Usage:
python make_passport_sheet.py input.jpg output.jpg --dpi 300
"""
import argparse
from pathlib import Path
from math import floor
from PIL import Image
# ----------------------------- constants -----------------------------
MM_PER_IN = 25.4
PASS_MM_W = 35 # passport width mm
PASS_MM_H = 45 # passport height mm
SHEET_MM_W = 100 # sheet width mm (10 cm)
SHEET_MM_H = 150 # sheet height mm (15 cm)
COLS, ROWS = 2, 3 # we can fit 2 × 3 = 6 photos
# ---------------------------------------------------------------------
def mm_to_px(mm: float, dpi: int) -> int:
"""Convert millimetres to integer pixels at the given dpi."""
return round(mm * dpi / MM_PER_IN)
def crop_to_passport_aspect(img: Image.Image) -> Image.Image:
"""Center-crop the image to the 7:9 (35:45) aspect ratio."""
w, h = img.size
target_ar = PASS_MM_W / PASS_MM_H # 0.777…
current_ar= w / h
if current_ar > target_ar: # too wide → crop sides
new_w = round(h * target_ar)
left = (w - new_w) // 2
box = (left, 0, left + new_w, h)
else: # too tall → crop top/bottom
new_h = round(w / target_ar)
top = (h - new_h) // 2
box = (0, top, w, top + new_h)
return img.crop(box)
def build_sheet(cropped: Image.Image, dpi: int) -> Image.Image:
"""Place 6 passport-size copies on a 10 × 15 cm sheet."""
# 1) exact pixel sizes
pass_px_w = mm_to_px(PASS_MM_W, dpi)
pass_px_h = mm_to_px(PASS_MM_H, dpi)
sheet_w = mm_to_px(SHEET_MM_W, dpi)
sheet_h = mm_to_px(SHEET_MM_H, dpi)
# 2) resize the cropped headshot
passport = cropped.resize((pass_px_w, pass_px_h), Image.LANCZOS)
# 3) compute equal gutter margins
# let Gx = horizontal gutter between columns plus 2×outer margins
# Gy = vertical gutter between rows plus 2×outer margins
total_free_x = sheet_w - COLS * pass_px_w
total_free_y = sheet_h - ROWS * pass_px_h
gutter_x = total_free_x // (COLS + 1)
gutter_y = total_free_y // (ROWS + 1)
sheet = Image.new("RGB", (sheet_w, sheet_h), "white")
for r in range(ROWS):
for c in range(COLS):
x = gutter_x + c * (pass_px_w + gutter_x)
y = gutter_y + r * (pass_px_h + gutter_y)
sheet.paste(passport, (x, y))
return sheet
def main():
ap = argparse.ArgumentParser()
ap.add_argument("input", type=Path, help="source 2×2-inch image file")
ap.add_argument("output", type=Path, help="output 10×15 cm sheet file")
ap.add_argument("--dpi", type=int, default=300,
help="print resolution (default: 300 dpi)")
args = ap.parse_args()
# 1) read & crop
img = Image.open(args.input).convert("RGB")
cropped = crop_to_passport_aspect(img)
# 2) build sheet
sheet = build_sheet(cropped, args.dpi)
# 3) embed correct DPI in the saved file
sheet.save(args.output, dpi=(args.dpi, args.dpi), quality=95)
print(f"✅ Saved {args.output} ({COLS*ROWS} photos) at {args.dpi} dpi")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment