Home » Tutorials » How to Compress Images in Python Using Tkinter and PIL

How to Compress Images in Python Using Tkinter and PIL

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

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, and ttk 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!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top