diff --git a/printready_idphoto/.gitignore b/printready_idphoto/.gitignore new file mode 100644 index 0000000..adb31b9 --- /dev/null +++ b/printready_idphoto/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +*.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Flet +storage/ diff --git a/printready_idphoto/README.md b/printready_idphoto/README.md new file mode 100644 index 0000000..78c7727 --- /dev/null +++ b/printready_idphoto/README.md @@ -0,0 +1,69 @@ +# PrintreadyIdphoto app + +## Run the app + +### uv + +Run as a desktop app: + +```bash +uv run flet run +``` + +Run as a web app: + +```bash +uv run flet run --web +``` + +For more details on running the app, refer to the [Getting Started Guide](https://flet.dev/docs/). + +## Build the app + +### Android + +```bash +flet build apk -v +``` + +For more details on building and signing `.apk` or `.aab`, refer to the [Android Packaging Guide](https://flet.dev/docs/publish/android/). + +### iOS + +```bash +flet build ipa -v +``` + +For more details on building and signing `.ipa`, refer to the [iOS Packaging Guide](https://flet.dev/docs/publish/ios/). + +### macOS + +```bash +flet build macos -v +``` + +For more details on building macOS package, refer to the [macOS Packaging Guide](https://flet.dev/docs/publish/macos/). + +### Linux + +```bash +flet build linux -v +``` + +For more details on building Linux package, refer to the [Linux Packaging Guide](https://flet.dev/docs/publish/linux/). + +### Windows + +```bash +flet build windows -v +``` + +For more details on building Windows package, refer to the [Windows Packaging Guide](https://flet.dev/docs/publish/windows/). + +### Web + +```bash +flet build web -v +``` + +For more details on building Web app, refer to the [Web Packaging Guide](https://flet.dev/docs/publish/web/). diff --git a/printready_idphoto/pyproject.toml b/printready_idphoto/pyproject.toml new file mode 100644 index 0000000..279e69a --- /dev/null +++ b/printready_idphoto/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "printready-idphoto" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.10" +authors = [ + { name = "ShAnku", email = "shanku@example.com" } +] +dependencies = [ + "flet>=0.84.0", + "numpy>=2.2.6", + "pillow>=12.2.0", + +] + +[dependency-groups] +dev = [ + "flet-cli>=0.84.0", + "flet-desktop>=0.84.0", + "flet-web>=0.84.0", +] + +[tool.flet] +desktop_flavor = "light" # Change to "full" to use flet-audio and flet-video +org = "com.shanku" +product = "printready-idphoto" +company = "sbag" +copyright = "Copyright (C) 2023-2026 by ShAnku" + +[tool.flet.app] +path = "src" diff --git a/printready_idphoto/src/assets/icon.png b/printready_idphoto/src/assets/icon.png new file mode 100644 index 0000000..bebc6d5 Binary files /dev/null and b/printready_idphoto/src/assets/icon.png differ diff --git a/printready_idphoto/src/assets/splash_android.png b/printready_idphoto/src/assets/splash_android.png new file mode 100644 index 0000000..8772f3f Binary files /dev/null and b/printready_idphoto/src/assets/splash_android.png differ diff --git a/printready_idphoto/src/main.py b/printready_idphoto/src/main.py new file mode 100644 index 0000000..6f89d1c --- /dev/null +++ b/printready_idphoto/src/main.py @@ -0,0 +1,154 @@ +import time + +import flet as ft +from PIL import Image + +from utils.img_resize import choose_size_options, resize_with +from utils.make_imgrid import border_photo, create_grid + +size_list = [ + "Passport (2x2inch)", + "OCI Passport (3.5x3.5cm)", + "Visa (3.5x4.5cm)", + "PAN Card (2.5x3.5cm)", + "Stamp (2x2.5cm)", +] + + +def main(page: ft.Page): + selected_files = ft.Text() + picker = ft.FilePicker() + save_picker = ft.FilePicker() + selected_img = ft.Image(src="icon.png", height=400, width=400) + output_img = ft.Image(src="icon.png", height=400, width=400) + dropdown = ft.Dropdown( + options=[ft.DropdownOption(key=size, text=size) for size in size_list], + ) + files: list[ft.FilePickerFile] + grid_image = None + save_location = "" + + # =====Clicking on Converter========= + async def handle_button_click(_: ft.Event[ft.Button]): + nonlocal grid_image + print("dropdown value:", dropdown.value or "None") + if files: + with Image.open(str(selected_img.src)) as photo: + photo = resize_with( + photo, + choose_size_options( + PPI=300, + choice=size_list.index(dropdown.value) + 1 + if dropdown.value + else 1, + ), + ) + photo = border_photo(photo) + list_of_pics = [photo for _ in range(10)] + grid = create_grid(list_of_pics) + name = f"demo_grid_a4_{time.time()}.png" + grid.save(name) + output_img.src = name + print("GUI: new image grid created........") + grid_image = grid + output_img.update() + + # ==========select file===== + async def handle_pick_files(e: ft.Event[ft.Button]): + nonlocal files + print("GUI : Clicked on convert..............") + files = await picker.pick_files( + file_type=ft.FilePickerFileType.IMAGE, + ) + if files: + selected_files.value = files[0].name + selected_img.src = f"{files[0].path}" + selected_img.update() + + # ==========opens file dialog to choose save location===== + async def handle_save_file(e): + try: + save_location = await save_picker.save_file( + "choose tee location", + file_name="output_grid.png", + file_type=ft.FilePickerFileType.IMAGE, + ) + if not save_location: + return + print("save_file location--->>>", save_location) + grid_image.save(str(save_location)) + page.update() + except e: + print("error saving ", e) + + # ================================================================== # + # GUI PART # + # ================================================================== # + page.scroll = ft.ScrollMode.AUTO + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.vertical_alignment = ft.MainAxisAlignment.CENTER + page.appbar = ft.AppBar( + title=ft.Text("ID Photo Converter"), + center_title=True, + ) + page.add( + ft.Row( + wrap=True, + controls=[ + ft.Column( + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + controls=[ + ft.Button( + content="Pick files", + icon=ft.Icons.UPLOAD_FILE, + on_click=handle_pick_files, + ), + selected_files, + ft.Card( + ft.InteractiveViewer( + content=selected_img, + boundary_margin=3, + ), + ), + ], + ), + ft.Container( + ft.Column( + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + controls=[ + dropdown, + ft.Button( + content="Convert", + icon=ft.Icons.TRANSFORM, + on_click=handle_button_click, + ), + ], + ), + width=400, + ), + ft.Column( + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + controls=[ + ft.Card( + ft.InteractiveViewer( + content=output_img, + constrained=True, + ), + ), + ft.Button( + content="Save to Custom Location", + icon=ft.Icons.FOLDER_OPEN, + on_click=handle_save_file, + ), + ft.Text( + f"The file is Saved at {save_location}", + ), + ], + ), + ], + ) + ) + + +if __name__ == "__main__": + ft.run(main, assets_dir="assets", upload_dir="assets/uploads") diff --git a/printready_idphoto/src/utils/__init__.py b/printready_idphoto/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/printready_idphoto/src/utils/img_resize.py b/printready_idphoto/src/utils/img_resize.py new file mode 100644 index 0000000..7827315 --- /dev/null +++ b/printready_idphoto/src/utils/img_resize.py @@ -0,0 +1,77 @@ +from PIL import Image + +from utils.print_canvas_size import ( + getsize_oci_passport_35x35_mm, + getsize_pan_25x35_mm, + getsize_passport_2x2_inch, + getsize_stamp_20x25_mm, + getsize_visa_35x45_mm, +) + + +def resize_equally_factor(img: Image.Image, resize_factor=0.5, rev=True): + print("original:", img) + width, height = img.size + if rev: + resize_factor = 1 - resize_factor + img = img.resize( + (int(width * resize_factor), int(height * resize_factor)), + Image.Resampling.LANCZOS, + ) + print("resized:", img) + return img + + +def resize_with(img: Image.Image, template_size: tuple[float, float]): + to_width, to_height = template_size + scale_w = to_width / img.width + scale_h = to_height / img.height + print("scaling:", scale_w, scale_h) + scaling_factor = min(scale_w, scale_h) + img = resize_equally_factor(img, scaling_factor, rev=False) + return img + + +def choose_size_options(PPI, cli=False, choice=None): + choice = choice + if cli: + choice = int( + input(""" What to Convert? ................... + 1) Indian Passport Photo: 2 x 2 inch. + 2) Indian OCI (Overseas Citizenship of India): 3.5 x 3.5 cm. + 3) Indian Visa Photo: 3.5 x 4.5 cm. + 4) Indian PAN Card Photo: 2.5 x 3.5 cm. + 5) Indian Stamp Size Photo: 2 x 2.5 cm. + Enter the format of your id photo:- """) + ) + to_size = (0, 0) + match choice: + case 1: + to_size = getsize_passport_2x2_inch(PPI) + case 2: + to_size = getsize_oci_passport_35x35_mm(PPI) + case 3: + to_size = getsize_visa_35x45_mm(PPI) + case 4: + to_size = getsize_pan_25x35_mm(PPI) + case 5: + to_size = getsize_stamp_20x25_mm(PPI) + case _: + print("Invalid choice. Defaulting to Passport Photo size.") + to_size = getsize_passport_2x2_inch(PPI) + return to_size + + +def main(): + PPI = 300 + path = "./demo_resized.jpg" + + with Image.open(path) as pic: + # pic = resize_equally_factor(pic) + pic = resize_with(pic, choose_size_options(PPI)) + pic.show() + pic.save("demo_resized.jpg") + + +if __name__ == "__main__": + main() diff --git a/printready_idphoto/src/utils/make_imgrid.py b/printready_idphoto/src/utils/make_imgrid.py new file mode 100644 index 0000000..1507b9b --- /dev/null +++ b/printready_idphoto/src/utils/make_imgrid.py @@ -0,0 +1,64 @@ +from PIL import Image, ImageOps + +from utils.img_resize import resize_with +from utils.print_canvas_size import ( + getsize_a4_canvas, + getsize_passport_2x2_inch, + getsize_visa_35x45_mm, + inches_to_pixels, +) + + +def border_photo(photo, border_inches=0.1, color="white"): + return ImageOps.expand( + photo, + inches_to_pixels(border_inches), + color, + ) + + +def create_grid(photos_list, margin=0.5, ppi=300): + print(photos_list[0]) + a4_size = getsize_a4_canvas(ppi=ppi) + canvas = Image.new("RGB", size=a4_size, color="white") + margin_start = inches_to_pixels(margin, ppi=ppi) + pos_x, pos_y = margin_start, margin_start + for pic in photos_list: + if pos_x + pic.width >= a4_size[0] - margin_start: + pos_x, pos_y = margin_start, pos_y + pic.height + if pos_y + pic.height >= a4_size[1] - margin_start: + print("less printed as page overflow") + break + canvas.paste(pic, (int(pos_x), int(pos_y))) + pos_x += pic.width + # canvas.show() + return canvas + + +def main(): + with Image.open("../assets/icon.png") as photo: + photo = resize_with( + photo, + getsize_passport_2x2_inch(ppi=300), + ) + photo2 = resize_with( + photo, + getsize_visa_35x45_mm(ppi=300), + ) + photo = border_photo(photo) + photo2 = border_photo(photo2) + list_of_pics = ( + [photo for _ in range(5)] + + [photo2 for _ in range(5)] + + [photo for _ in range(5)] + ) + photo.show() + photo2.show() + print(photo) + grid = create_grid(list_of_pics) + # grid.show() + grid.save("demo_grid_a4.jpg") + + +if __name__ == "__main__": + main() diff --git a/printready_idphoto/src/utils/print_canvas_size.py b/printready_idphoto/src/utils/print_canvas_size.py new file mode 100644 index 0000000..e083540 --- /dev/null +++ b/printready_idphoto/src/utils/print_canvas_size.py @@ -0,0 +1,103 @@ +""" +Inches = Pixels / PPI +Pixels = Inches × PPI +Inches = Centimeters / 2.54 +1 cm = 0.393701 inches +1 inch = 25.4 mm +Centimeters = Inches × 2.54 +Centimeters = (Pixels / PPI) × 2.54 +Pixels = (Centimeters / 2.54) × PPI +""" + + +def mm_to_inches(mm): + return mm / 25.4 + + +def inches_to_pixels(inches, ppi=96): + return int(inches * ppi) + + +def getsize_a4_canvas(ppi): + """A4 paper size is 210 x 297 mm. + - Width: 210 mm / 25.4 mm/inch ≈ 8.27 inches + - Height: 297 mm / 25.4 mm/inch ≈ 11.69 inches + - Width in pixels: 8.27 inches * 96 PPI ≈ 796 pixels + - Height in pixels: 11.69 inches * 96 PPI ≈ 1120 pixels + + Therefore, the A4 paper size in inches is approximately 8.27 x 11.69 inches, and in pixels (assuming 96 PPI) it would be approximately 796 x 1120 pixels. + """ + # A4 paper size in mm + width_mm = 210 + height_mm = 297 + # Convert mm to inches + width_inches = mm_to_inches(width_mm) + height_inches = mm_to_inches(height_mm) + # Convert inches to pixels + width_pixels = inches_to_pixels(width_inches, ppi) + height_pixels = inches_to_pixels(height_inches, ppi) + + print("A4 paper size in inches:") + print(f"Width: {width_inches:.2f} inches") + print(f"Height: {height_inches:.2f} inches") + + print(f"\nA4 paper size in pixels ({ppi} PPI):") + print(f"Width: {width_pixels} pixels") + print(f"Height: {height_pixels} pixels") + return (width_pixels, height_pixels) + + +def getsize_passport_2x2_inch(ppi): + """Indian Passport Photo + - Size of photo: 2 x 2 inch, 35 x 45 mm or 35 x 35 mm (5 x 5 cm, 3.5 x 4.5 cm, 3.5 x 3.5 cm). + - Color of background: white or off-white. + - The model has to look towards the camera. Tilted head is not allowed. + - Profile or three quarter photo is not allowed. + """ + return ( + inches_to_pixels(2, ppi), + inches_to_pixels(2, ppi), + ) + + +def getsize_oci_passport_35x35_mm(ppi): + """Indian OCI (Overseas Citizenship of India). + - Size of photo: 35x35 mm. (3.5 x 3.5 cm) + - Two ID photos must be enclosed for OCI application. + - Color of background: white is not allowed, usually light blue is used. + """ + return ( + inches_to_pixels(mm_to_inches(35), ppi), + inches_to_pixels(mm_to_inches(35), ppi), + ) + + +def getsize_visa_35x45_mm(ppi): + """Indian Visa Photo + - Size of photo: 3.5 x 4.5 cm. + - Color of background: white or light coloured. + """ + return ( + inches_to_pixels(mm_to_inches(35), ppi), + inches_to_pixels(mm_to_inches(45), ppi), + ) + + +def getsize_pan_25x35_mm(ppi): + """Indian PAN Card Photo + - Size of photo: 2.5 x 3.5 cm. + """ + return ( + inches_to_pixels(mm_to_inches(25), ppi), + inches_to_pixels(mm_to_inches(35), ppi), + ) + + +def getsize_stamp_20x25_mm(ppi): + """Indian Stamp Size Photo photo's size + - Size of Photo: 2 x 2.5 cm. + """ + return ( + inches_to_pixels(mm_to_inches(20), ppi), + inches_to_pixels(mm_to_inches(25), ppi), + )