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.
- Click the
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()