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
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
🪟 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()
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"
)
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)
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)
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
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()
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)
This keeps the interface responsive while processing.
📊 Step 6 – Progress bar + ETA
We track timing:
self.start_time = time.time()
Then calculate speed:
elapsed = time.time() - self.start_time
speed = done / elapsed
eta = (total - done) / speed
Displayed using Tk variables:
self.progress_var.set(done)
self.speed_var.set(f"{speed:.2f} files/sec")
This gives users confidence during long batches.
⏸ Step 7 – Pause / Resume
We use threading.Event:
self.pause_event = threading.Event()
self.pause_event.set()
Inside the loop:
self.pause_event.wait()
Toggle:
def toggle_pause(self):
if self.is_paused:
self.pause_event.set()
else:
self.pause_event.clear()
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))
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)
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
)
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)