Image Resizer

This project was inspired by Avery Horvath's image compression program.

To use the least storage space as possible in my GitLab repo, I need to compress my JPG images. Since the online websites were very sketchy and inconvenient for shrinking lots of images, I decided to write my own program. After some google searches about compressing JPGs using the pillow package in Python, I wrote the below program, using the tkinter library for the GUI.

How To Use It In Your Project

You must have Python installed

  • Go to Image Resizer in my repo and download the ZIP file OR click here to download
  • Download the folder (next to Clone button)
  • Unzip the directory
  • Option A:
    • In the terminal, navigate to the directory you downloaded
    • Either run the resizer.cmd script from the command line or write the following:
pip install -r requirements.txt
python main.pyw
  • Option B:
    • Click the resizer.cmd file from a file explorer and the script will execute!
      • If an antivirus software stops you from running the script, use Option A.

Virutal Environment

I used the following commands to setup my virutal environment and install dependencies for the project.

python -m venv venv
venv\Scripts\activate
pip install pillow
pip install tk

Rotation Troubles

The only problem I encountered was that pillow would rotate some of the JPGs when saving the shrinked images. I found the solution in this forum and adding the following line solved the problem.

im = ImageOps.exif_transpoes(im)

Final Script

Here is the final script for the project. I understand that the code is not the prettiest, but it gets the job done. Using this tool, I was able to reduce the size of my images by over 50% with a barely noticible difference in quality.

from tkinter.filedialog import askopenfilenames, askdirectory
from tkinter.font import Font
from tkinter import *

from PIL import Image, ImageOps

def resize(filepaths, output_directory):
    for filepath in filepaths:
        im = Image.open(filepath)
        im = ImageOps.exif_transpose(im)
        imResize = im.resize((500,int(im.size[1] * (500/im.size[0]))), Image.LANCZOS)
        pth = output_directory + "/" + ".".join(filepath.split("/")[-1].split(".")[:-1]) + sv.get() + '.jpg'
        print(pth)
        imResize.save(pth, 'JPEG', quality=50)

def resize_jpgs():
    filenames = askopenfilenames(title = "Open JPGs") 
    if not filenames:
        return
    output_directory = askdirectory(title = "Ouput Directory")
    if not output_directory:
        return
    resize(filenames, output_directory)

def OnEntryClick(event):
    value=sv.get()
    l.config(text = "example_file" + value + ".jpg")

top = Tk()
top.geometry("500x500")
top.title("JPG Resizer -- Adam Stone")

myFont = Font(family='Helvetica', size="36")
B = Button(top, text ="Resize JPGs", command = resize_jpgs, font=myFont, bg="green")
sv = StringVar()
ent = Entry(top, textvariable=sv)
ent.insert(END, "-resized")
l = Label(top, text="example_file-resized.jpg")

ent.bind("<KeyRelease>", OnEntryClick)

B.place(relx=0.5, rely=0.5, anchor=CENTER)
ent.place(relx=0.75, rely=0.75, anchor=CENTER)
l.place(relx=0.25, rely=0.75, anchor=CENTER)

if __name__ == "__main__":
    top.mainloop()

In the future I'd like to make the quality and image dimensions customizable in the GUI, but it's not necessary for my usage right now.

Command Line Script

To make this project easily usable for anyone to download, I created the following command line script saved in the resizer.cmd file. (I also renamed main.py to main.pyw so that the Python terminal wouldn't appear when the GUI was running - there is still a black window that appears in the final project, but I'm not concerned about removing it).

pip install -r requirements.txt
start python main.pyw

I then had to create the requirements.txt file. To do so, I ran the following, with help from this discussion board.

venv\Scripts\activate
pip freeze > requirements.txt

That generated the following:

Pillow==9.4.0
tk==0.1.0

Then, I added everything in this file to my project's .gitignore file so it wouldn't upload all of the downloaded dependencies to GitLab - that's what the requirements.txt file is for!

Lastly, I added the image-resizer directory to my repo!

Refactoring

I slightly modified the program to where it now can accept any file type and the quality and dimensions are adjustable from the GUI. I also linked my Fab Academy page. Here is the final program:

from tkinter.filedialog import askopenfilenames, askdirectory
from tkinter.font import Font
from tkinter import messagebox
from tkinter import *
import webbrowser
from PIL import Image, ImageOps

ERROR_TITLE = "Image Resizer: Error"
ANCHOR = CENTER
EXAMPLE_FILENAME = "example_file"
EXAMPLE_ADJUNCT = "-resized"
EXAMPLE_EXTENTION = "jpg"

def assemble_adjunct_filename(filename, adjunct, extention): # centralizes adjunct system
    return f"{filename}{adjunct}.{extention}"

def resize_files(filepaths, output_directory, desired_width):
    for filepath in filepaths:
        im = Image.open(filepath)
        im = ImageOps.exif_transpose(im) # fixes rotation

        # current dimensions
        current_width, current_height = im.size

        # if input dimensions are higher than current dimensions, throw an error
        if desired_width > current_width: 
            messagebox.showwarning(ERROR_TITLE, "Increasing dimensions of image") # can still work, so not returning after

        # multiply this by a current dimension and it will output the desired dimension
        ratio_desired_to_current = desired_width/current_width
        # apply ratio
        desired_dimensions = ((desired_width, int(current_height * ratio_desired_to_current))) 

        # execute resize
        im_resized = im.resize(desired_dimensions, Image.LANCZOS)

        # this ensures only text after the last "." in the filepath is cutoff
        # (so if your file name is "my.file.jpg" isn't renamed it to just "my-resized.jpg")
        old_filename_no_extention = ".".join(filepath.split("/")[-1].split(".")[:-1])

        # user-indicated appendage to attach to files once shrunk
        # (ex: "my-file.jpg" -> "my-file-resized.jpg")
        output_filename_adjunct = adjunct_entry_value.get()

        # extract file extention
        file_extention = filepath.split(".")[-1]

        # construct output path
        path = f"{output_directory}/{old_filename_no_extention}{output_filename_adjunct}.{file_extention}"

        print(f"Output to: {path}")

        # save resized image
        im_resized.save(path, quality=quality_scale.get())

def run_resize():
    # if the input width in the GUI isn't an integer, throw an error
    try:
        desired_width = int(width_entry.get().strip())
    except:
        messagebox.showerror(ERROR_TITLE, "Error: Width is not an integer")
        return

    try: 
        filenames = askopenfilenames(title = "Open Images") # ask for input filepaths
        if not filenames: # if there aren't any files selected, stop
            return

        output_directory = askdirectory(title = "Ouput Directory") # ask for output directory
        if not output_directory: # if there isn't a directory selected, stop
            return

        resize_files(filenames, output_directory, desired_width)
    except Exception as e: # if there's any error, display it in the GUI
        messagebox.showerror(ERROR_TITLE, e)    

def adjunct_entry_update_callback(event): # called when filename adjunct is updated
    adjunct = adjunct_entry_value.get() # store adjunct input
    output_file_label.config(text=assemble_adjunct_filename(EXAMPLE_FILENAME, adjunct, EXAMPLE_EXTENTION)) # update output file label
    # this doesn't mean the file has to be jpg, it's just an example of a file name after adding the adjunct

top = Tk()
top.geometry("500x500")
top.title("JPG Resizer -- Adam Stone")

resize_button_font = Font(family='Helvetica', size="36")
small_font = Font(family='Hevetica', size="10", weight="bold", slant="italic")
resize_button = Button(top, text ="Resize Image", command=run_resize, font=resize_button_font, bg="green")
adjunct_entry_value = StringVar()
adjunct_entry = Entry(top, textvariable=adjunct_entry_value)
adjunct_entry.insert(END, "-resized") # set default value
output_file_label = Label(top, text=assemble_adjunct_filename(EXAMPLE_FILENAME, EXAMPLE_ADJUNCT, EXAMPLE_EXTENTION))
quality_label = Label(top, text="Quality:")
quality_scale = Scale(top, from_=1, to=100, orient=HORIZONTAL)
quality_scale.set(50)
width_label = Label(top, text="Width (px):")
width_entry = Entry(top)
width_entry.insert(END, "500") # set default value

credit = Label(top, text="Fab Academy 2023 Charlotte Latin Adam Stone", fg="blue", font=small_font)

adjunct_entry.bind("<KeyRelease>", adjunct_entry_update_callback)

# place all elements on GUI
resize_button.place(relx=0.5, rely=0.3, anchor=ANCHOR)
adjunct_entry.place(relx=0.75, rely=0.55, anchor=ANCHOR)
output_file_label.place(relx=0.25, rely=0.55, anchor=ANCHOR)
quality_label.place(relx=0.3, rely=0.65, anchor=ANCHOR)
quality_scale.place(relx=0.5, rely=0.65, anchor=ANCHOR)
width_label.place(relx=0.3, rely=0.75, anchor=ANCHOR)
width_entry.place(relx=0.5, rely=0.75, anchor=ANCHOR)
credit.place(relx=0.5, rely=0.95, anchor=ANCHOR)

if __name__ == "__main__":
    top.mainloop()