In today’s digital age, images play a significant role in communication and technology. However, dealing with large image files can be cumbersome, whether you’re a developer or just someone looking to save storage space. That’s where image compression comes in—helping reduce file sizes without compromising quality.
Today, you’ll learn how to compress images in Python using Tkinter and PIL. We’ll guide you step by step in creating an interactive application where users can select an image, specify a target size, and compress it efficiently. Let’s dive in!
Table of Contents
- Getting Started
- Displaying File Sizes in Easy-to-Read Formats
- Image Compression with Dynamic Quality Adjustment
- Browsing and Previewing Images for Compression
- Starting the Compression Process
- Setting Up the Tkinter Window for Image Compression
- Example
- Full Code
Getting Started
Let’s get started by preparing the essentials. First, we’ll make sure all the required libraries are installed. Open your terminal and run the following commands:
$ pip install tk
$ pip install tkinterdnd2
$ pip install pillow
Imports
import os
from tkinter import Tk, Label, Button, Entry, filedialog, IntVar, messagebox, ttk, Canvas
from PIL import Image, ImageTk
from tkinterdnd2 import TkinterDnD, DND_FILES
We begin by bringing in the tools we need. Let’s import our dependable libraries:
- os: This library helps us work with file sizes and paths.
- Tkinter: Our trusty sidekick for building the graphical interface. It provides elements like buttons and widgets. We’ll also use
filedialog
for browsing and saving files,messagebox
for feedback via pop-up messages, andttk
for styled widgets. - PIL (Pillow): The star of the show, responsible for resizing, filtering, and compressing images.
- tkinterdnd2: This library adds an extra spark by enabling drag-and-drop functionality for adding images to the GUI.
Displaying File Sizes in Easy-to-Read Formats
def get_size_format(b, factor=1024, suffix="B"):
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if b < factor:
return f"{b:.2f}{unit}{suffix}"
b /= factor
return f"{b:.2f}Y{suffix}"
Now that we have our tools ready, let’s move on to formatting byte sizes into something more readable. What do I mean by that? Well, imagine this: you ask your computer about the size of your picture and it gives you a response like 5242880 bytes. Quite a mouthful, right? Our get_size_format()
function can simplify that number and turn it into 5.00 MB—much clearer, don’t you think?
So, how does it work? It’s actually pretty simple. The function takes the raw image size in bytes and checks if it’s smaller than 1024. If it is, it formats the number into a string with two decimal places and attaches the appropriate unit. But if it’s larger than 1024, it divides the number by 1024 and assigns the correct unit, giving us a much easier-to-read result.
Image Compression with Dynamic Quality Adjustment
With image sizes clearer, Let’s dive into the heart of the operation: image compression! The function we’ll use to do this is called compress_img()
. First, we need to open the image and get its original size. It also prints the original size in a more readable format using the get_size_format()
function.
img = Image.open(image_path)
original_size = os.path.getsize(image_path)
print("[*] Original size:", get_size_format(original_size))
Next, if we provided a desired width and height, the function will resize the image while preserving its quality. The Image.LANCZOS
filter ensures the image remains smooth and detailed during resizing.
if width and height:
img = img.resize((width, height), Image.LANCZOS)
Now, we set the initial quality to 85, which is usually a great balance between quality and file size. The image is saved with this quality setting and optimized. After that, it checks the compressed size.
quality = 85
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
Then, we enter a loop where the function tries to get the image size just right. If the image is still too large, it reduces the quality in steps of 5 until the size is within the target. If it’s too small, the quality is increased by 1, all while keeping the image optimized.
while compressed_size > target_size and quality > 10:
quality -= 5
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
while compressed_size < target_size and quality < 95:
quality += 1
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
Finally, it calculates how much space was saved and returns the details—such as the output path, original size, compressed size, and the amount of space saved. If anything goes wrong during this process, an error message pops up.
saving_diff = compressed_size - original_size
return output_file, original_size, compressed_size, saving_diff
And here’s the error handling just in case something goes wrong:
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
return None, None, None, None
Browsing and Previewing Images for Compression
File Browsing
Alright, let’s keep the momentum going! The next step is giving the user the ability to select an image. We accomplish this with the browse_file()
function, which uses filedialog
to allow the user to choose the image they want to compress. Once the user selects the image, the function updates the entry box with the image path and calls the preview_image()
function to show a preview.
def browse_file():
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.bmp;*.gif")])
if file_path:
entry_path.delete(0, "end")
entry_path.insert(0, file_path)
preview_image(file_path)
Image Preview
Speaking of the preview_image()
function, its job is to display a preview of the selected image. Here’s how it works: It opens the chosen image, resizes it into a thumbnail using img.thumbnail()
, and then converts it into an ImageTk.PhotoImage
object that’s ready to be displayed on the canvas_preview
widget. It also updates the label_original_size
label to show the original image size before compression.
def preview_image(image_path):
try:
img = Image.open(image_path)
img.thumbnail((200, 200))
img_tk = ImageTk.PhotoImage(img)
canvas_preview.config(width=img_tk.width(), height=img_tk.height())
canvas_preview.create_image(0, 0, anchor="nw", image=img_tk)
canvas_preview.image = img_tk
original_size = os.path.getsize(image_path)
label_original_size.config(text=f"Original Size: {get_size_format(original_size)}")
except Exception as e:
messagebox.showerror("Error", f"Could not load image preview: {e}")
Output File Browsing
Now that the user has selected the image they want to compress, it’s time to let them choose where to save the compressed file. Our browse_output_file()
function helps with that by allowing the user to pick a location, name the file, and save it with the correct extension (we’ve set “.jpg” by default).
def browse_output_file():
file_path = filedialog.asksaveasfilename(defaultextension=".jpg", filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png")])
if file_path:
entry_output_file.delete(0, "end")
entry_output_file.insert(0, file_path)
Starting the Compression Process
With everything set up, all that’s left is to trigger the compression process. That’s where the start_compression()
function comes in. It gathers all of the user’s inputs and checks if both the image and output file paths are valid. If either one is invalid, it will display an error message. It also checks for width and height inputs. Once everything is good to go, it calls the compress_and_display_results()
function and starts the progress bar to show the process in action.
def start_compression():
image_path = entry_path.get()
output_file = entry_output_file.get()
target_size = target_size_var.get() * 1024
if not image_path or not os.path.isfile(image_path):
messagebox.showerror("Error", "Please select a valid image file.")
return
if not output_file:
messagebox.showerror("Error", "Please select a valid output file path.")
return
width = int(entry_width.get()) if entry_width.get() else None
height = int(entry_height.get()) if entry_height.get() else None
progress_bar.start(10)
root.after(100, lambda: compress_and_display_results(image_path, output_file, target_size, width, height))
Displaying Results
Once the image is compressed, the compress_and_display_results()
function takes over. It stops the progress bar and updates the labels to display the new image size and the amount of space saved. It also celebrates the success of the compression with a pop-up message, notifying the user of the saved image and its new size.
def compress_and_display_results(image_path, output_file, target_size, width, height):
new_filepath, original_size, new_size, saving_diff = compress_img(image_path, output_file, target_size, width, height)
progress_bar.stop()
if new_filepath:
result = (
f"Original size: {get_size_format(original_size)}\n"
f"New size: {get_size_format(new_size)}\n"
f"Size change: {saving_diff / original_size * 100:.2f}%"
)
label_result.config(text=result)
preview_image(new_filepath)
messagebox.showinfo("Success", f"Image saved at {new_filepath}")
Setting Up the Tkinter Window for Image Compression
Enabling Drag-and-Drop for Image Selection
This section adds drag-and-drop functionality to the image path input field. The on_drop()
function handles the event when a file is dropped onto the entry widget, verifying the file path and updating the input field with the image’s location. The tkinterDnD2 library is used to enable this feature, making it easier for users to select images by simply dragging and dropping them into the application.
def on_drop(event):
file_path = event.data.strip()
if os.path.isfile(file_path):
entry_path.delete(0, "end")
entry_path.insert(0, file_path)
preview_image(file_path)
Now, let’s walk through how we set up the visual interface for our image compressor, making it both intuitive and functional. We begin by creating the main window for our app. Here, we initialize the window and set its title and dimensions. To make it more user-friendly, we also give it drag-and-drop functionality, so users can easily drop an image file to start the process. The TkinterDnD.Tk()
function helps us add this feature seamlessly. With the window set, we add a few essential elements like entry fields and buttons to guide the user in selecting files and setting options.
# Setting up the TkinterDnD window
root = TkinterDnD.Tk()
root.title("Image Compressor - The Pycodes")
root.geometry("800x500")
Next, we focus on allowing users to either drag and drop an image file or use a “browse” button to select one. The entry widget is where the image path will appear once a file is selected or dropped. To make this even easier, we use the drag-and-drop feature by binding the on_drop()
function to the entry box. On top of that, we add a “browse” button next to the entry box, which calls the browse_file()
function when clicked, making it even more flexible for the user.
# Enable drag-and-drop functionality for the entry widget
entry_path = Entry(root, width=50)
entry_path.grid(row=0, column=1, padx=5, pady=5)
entry_path.drop_target_register(DND_FILES)
entry_path.dnd_bind('<<Drop>>', on_drop)
Label(root, text="Select Image:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
Button(root, text="Browse", command=browse_file).grid(row=0, column=2, padx=5, pady=5)
Afterward, we continue adding input fields for the user to set the parameters for the compression. This includes an entry field for the output file path, where the compressed image will be saved, and a “browse” button to make selecting the output path easier. We also give users the ability to specify the target size for the compressed image and allow them to input desired width and height.
To tie everything together, we add a “Compress Image” button that triggers the compression process, a progress bar to show the ongoing task, and a canvas to display the image thumbnail. Lastly, we use labels to show the original and new sizes of the image. And with all that in place, we call root.mainloop()
to keep the window active and responsive to user input.
Label(root, text="Output File:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
entry_output_file = Entry(root, width=50)
entry_output_file.grid(row=1, column=1, padx=5, pady=5)
Button(root, text="Browse", command=browse_output_file).grid(row=1, column=2, padx=5, pady=5)
Label(root, text="Target Size (KB):").grid(row=2, column=0, padx=5, pady=5, sticky="e")
target_size_var = IntVar(value=500)
Entry(root, textvariable=target_size_var).grid(row=2, column=1, padx=5, pady=5)
Label(root, text="Width:").grid(row=3, column=0, padx=5, pady=5, sticky="e")
entry_width = Entry(root)
entry_width.grid(row=3, column=1, padx=5, pady=5)
Label(root, text="Height:").grid(row=4, column=0, padx=5, pady=5, sticky="e")
entry_height = Entry(root)
entry_height.grid(row=4, column=1, padx=5, pady=5)
Button(root, text="Compress Image", command=start_compression).grid(row=5, column=1, padx=5, pady=20)
progress_bar = ttk.Progressbar(root, mode='indeterminate')
progress_bar.grid(row=6, column=1, padx=5, pady=5)
canvas_preview = Canvas(root, width=200, height=200)
canvas_preview.grid(row=0, column=3, rowspan=5, padx=10, pady=10)
label_original_size = Label(root, text="Original Size: ")
label_original_size.grid(row=7, column=0, columnspan=3, padx=5, pady=5)
label_result = Label(root, text="", justify="left")
label_result.grid(row=8, column=0, columnspan=3, padx=5, pady=5)
root.mainloop()
Example
Full Code
import os
from tkinter import Tk, Label, Button, Entry, filedialog, IntVar, messagebox, ttk, Canvas
from PIL import Image, ImageTk
from tkinterdnd2 import TkinterDnD, DND_FILES
def get_size_format(b, factor=1024, suffix="B"):
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if b < factor:
return f"{b:.2f}{unit}{suffix}"
b /= factor
return f"{b:.2f}Y{suffix}"
def compress_img(image_path, output_file, target_size, width=None, height=None):
try:
img = Image.open(image_path)
original_size = os.path.getsize(image_path)
print("[*] Original size:", get_size_format(original_size))
if width and height:
img = img.resize((width, height), Image.LANCZOS)
quality = 85
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
while compressed_size > target_size and quality > 10:
quality -= 5
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
while compressed_size < target_size and quality < 95:
quality += 1
img.save(output_file, quality=quality, optimize=True)
compressed_size = os.path.getsize(output_file)
saving_diff = compressed_size - original_size
return output_file, original_size, compressed_size, saving_diff
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
return None, None, None, None
def browse_file():
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.bmp;*.gif")])
if file_path:
entry_path.delete(0, "end")
entry_path.insert(0, file_path)
preview_image(file_path)
def preview_image(image_path):
try:
img = Image.open(image_path)
img.thumbnail((200, 200))
img_tk = ImageTk.PhotoImage(img)
canvas_preview.config(width=img_tk.width(), height=img_tk.height())
canvas_preview.create_image(0, 0, anchor="nw", image=img_tk)
canvas_preview.image = img_tk
original_size = os.path.getsize(image_path)
label_original_size.config(text=f"Original Size: {get_size_format(original_size)}")
except Exception as e:
messagebox.showerror("Error", f"Could not load image preview: {e}")
def browse_output_file():
file_path = filedialog.asksaveasfilename(defaultextension=".jpg", filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png")])
if file_path:
entry_output_file.delete(0, "end")
entry_output_file.insert(0, file_path)
def start_compression():
image_path = entry_path.get()
output_file = entry_output_file.get()
target_size = target_size_var.get() * 1024
if not image_path or not os.path.isfile(image_path):
messagebox.showerror("Error", "Please select a valid image file.")
return
if not output_file:
messagebox.showerror("Error", "Please select a valid output file path.")
return
width = int(entry_width.get()) if entry_width.get() else None
height = int(entry_height.get()) if entry_height.get() else None
progress_bar.start(10)
root.after(100, lambda: compress_and_display_results(image_path, output_file, target_size, width, height))
def compress_and_display_results(image_path, output_file, target_size, width, height):
new_filepath, original_size, new_size, saving_diff = compress_img(image_path, output_file, target_size, width,
height)
progress_bar.stop()
if new_filepath:
result = (
f"Original size: {get_size_format(original_size)}\n"
f"New size: {get_size_format(new_size)}\n"
f"Size change: {saving_diff / original_size * 100:.2f}%"
)
label_result.config(text=result)
preview_image(new_filepath)
messagebox.showinfo("Success", f"Image saved at {new_filepath}")
def on_drop(event):
file_path = event.data.strip()
if os.path.isfile(file_path):
entry_path.delete(0, "end")
entry_path.insert(0, file_path)
preview_image(file_path)
# Setting up the TkinterDnD window
root = TkinterDnD.Tk()
root.title("Image Compressor - The Pycodes")
root.geometry("800x500")
# Enable drag-and-drop functionality for the entry widget
entry_path = Entry(root, width=50)
entry_path.grid(row=0, column=1, padx=5, pady=5)
entry_path.drop_target_register(DND_FILES)
entry_path.dnd_bind('<<Drop>>', on_drop)
Label(root, text="Select Image:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
Button(root, text="Browse", command=browse_file).grid(row=0, column=2, padx=5, pady=5)
Label(root, text="Output File:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
entry_output_file = Entry(root, width=50)
entry_output_file.grid(row=1, column=1, padx=5, pady=5)
Button(root, text="Browse", command=browse_output_file).grid(row=1, column=2, padx=5, pady=5)
Label(root, text="Target Size (KB):").grid(row=2, column=0, padx=5, pady=5, sticky="e")
target_size_var = IntVar(value=500)
Entry(root, textvariable=target_size_var).grid(row=2, column=1, padx=5, pady=5)
Label(root, text="Width:").grid(row=3, column=0, padx=5, pady=5, sticky="e")
entry_width = Entry(root)
entry_width.grid(row=3, column=1, padx=5, pady=5)
Label(root, text="Height:").grid(row=4, column=0, padx=5, pady=5, sticky="e")
entry_height = Entry(root)
entry_height.grid(row=4, column=1, padx=5, pady=5)
Button(root, text="Compress Image", command=start_compression).grid(row=5, column=1, padx=5, pady=20)
progress_bar = ttk.Progressbar(root, mode='indeterminate')
progress_bar.grid(row=6, column=1, padx=5, pady=5)
canvas_preview = Canvas(root, width=200, height=200)
canvas_preview.grid(row=0, column=3, rowspan=5, padx=10, pady=10)
label_original_size = Label(root, text="Original Size: ")
label_original_size.grid(row=7, column=0, columnspan=3, padx=5, pady=5)
label_result = Label(root, text="", justify="left")
label_result.grid(row=8, column=0, columnspan=3, padx=5, pady=5)
root.mainloop()
Happy Coding!