DEV Community

Mate Technologies
Mate Technologies

Posted on

Building a Production-Ready Batch Image Converter in Python (JPGify v1.2.0)

In this tutorial, we’ll build a desktop app in Python that converts images to JPG in batches.

By the end, you’ll have a real-world application with:

Drag & drop support

Folder recursion

Batch conversion

Progress bar + ETA

Adjustable JPG quality

Pause / resume

Preserve folder structure

Skip existing files

This project uses:

Tkinter + ttkbootstrap (UI)

Pillow (image processing)

threading + queues (responsiveness)

Let’s build it step by step.

🧰 Prerequisites

Install dependencies:

pip install pillow ttkbootstrap tkinterdnd2 pillow-heif
Enter fullscreen mode Exit fullscreen mode

Imports we’ll use:

import threading
import queue
import time
from pathlib import Path
import tkinter as tk
from PIL import Image
import ttkbootstrap as tb
Enter fullscreen mode Exit fullscreen mode

🪟 Step 1 – Creating the main window

We start with a basic application class.

class JPGifyApp:
    def __init__(self):
        self.root = tk.Tk()
        tb.Style(theme="darkly")

        self.root.title("JPGify")
        self.root.geometry("1120x780")

        self.root.mainloop()
Enter fullscreen mode Exit fullscreen mode

Why class-based?

Easier state management

Cleaner architecture

Scales better as features grow

📦 Step 2 – Supported formats

Define what file types we accept:

SUPPORTED_FORMATS = (
    ".png", ".gif", ".tif", ".tiff",
    ".bmp", ".webp", ".jpeg", ".jpg", ".heic"
)
Enter fullscreen mode Exit fullscreen mode

This lets us quickly filter dropped or selected files.

📂 Step 3 – Adding files and folders

Users can add individual files:

def add_files(self):
    for f in filedialog.askopenfilenames():
        path = Path(f)
        if path.suffix.lower() in SUPPORTED_FORMATS:
            self.listbox.insert(tk.END, path)
Enter fullscreen mode Exit fullscreen mode

And entire folders:

def collect_files(self, folder):
    for path in folder.rglob("*"):
        if path.is_file() and path.suffix.lower() in SUPPORTED_FORMATS:
            self.listbox.insert(tk.END, path)
Enter fullscreen mode Exit fullscreen mode

Using Path.rglob() gives us recursive traversal for free.

👀 Step 4 – Building a conversion preview

Before converting, we map every source file to its destination.

def build_preview(self):
    self.convert_map = {}

    for f in self.listbox.get(0, tk.END):
        src = Path(f)
        dst = self.output_folder / (src.stem + ".jpg")
        self.convert_map[src] = dst
Enter fullscreen mode Exit fullscreen mode

This allows:

Previewing results

Skipping existing files

Preserving folder structure

Think of this as a conversion plan.

🧵 Step 5 – Running conversions in a background thread

Never block the UI.

We launch conversion like this:

threading.Thread(
    target=self.convert_images,
    daemon=True
).start()
Enter fullscreen mode Exit fullscreen mode

Then implement:

def convert_images(self):
    for src, dst in self.convert_map.items():
        with Image.open(src) as img:
            img.convert("RGB").save(dst, "JPEG", quality=90)
Enter fullscreen mode Exit fullscreen mode

This keeps the interface responsive while processing.

📊 Step 6 – Progress bar + ETA

We track timing:

self.start_time = time.time()
Enter fullscreen mode Exit fullscreen mode

Then calculate speed:

elapsed = time.time() - self.start_time
speed = done / elapsed
eta = (total - done) / speed
Enter fullscreen mode Exit fullscreen mode

Displayed using Tk variables:

self.progress_var.set(done)
self.speed_var.set(f"{speed:.2f} files/sec")
Enter fullscreen mode Exit fullscreen mode

This gives users confidence during long batches.

⏸ Step 7 – Pause / Resume

We use threading.Event:

self.pause_event = threading.Event()
self.pause_event.set()
Enter fullscreen mode Exit fullscreen mode

Inside the loop:

self.pause_event.wait()
Enter fullscreen mode Exit fullscreen mode

Toggle:

def toggle_pause(self):
    if self.is_paused:
        self.pause_event.set()
    else:
        self.pause_event.clear()
Enter fullscreen mode Exit fullscreen mode

Simple and effective.

🧵 Step 8 – Thread-safe UI updates

Tkinter isn’t thread-safe.

We use a queue:

self.ui_queue = queue.Queue()

Worker thread pushes updates:

self.ui_queue.put(("progress", done))
Enter fullscreen mode Exit fullscreen mode

Main thread processes them:

def process_ui_queue(self):
    while not self.ui_queue.empty():
        key, value = self.ui_queue.get()
        if key == "progress":
            self.progress_var.set(value)

    self.root.after(100, self.process_ui_queue)
Enter fullscreen mode Exit fullscreen mode

This is a critical pattern for production Tk apps.

🖼 Step 9 – Final image conversion

Actual saving:

rgb_img.save(
    dst,
    "JPEG",
    quality=self.quality_var.get(),
    optimize=True,
    progressive=True
)
Enter fullscreen mode Exit fullscreen mode

We also:

Skip existing JPGs

Auto-create folders

Log failures

Auto-open output directory

✅ Final result

You now have a professional-grade batch image converter:

Drag & drop

Folder recursion

Preview mapping

Pause / resume

ETA + speed

Adjustable quality

Portable design

The full project (JPGify v1.2.0) is available here:

👉 https://gum.new/gum/cmksd0wad000l04k43709amov

🚀 Wrap-up

This project demonstrates how far you can push Tkinter with:

Proper threading

Queues

Class-based design

Thoughtful UX

If you’d like, next steps could include:

EXE packaging with PyInstaller

Multithreaded workers

WebP export

Dark/light themes

Thanks for reading — happy building 🙂

Top comments (0)