Resizing and Compressing Images Using Python

If you’ve ever been in charge of a blog, you know how annoying it is to handle images.

Unless you want your website to become a slow, bloated piece of garbage over time, you need to resize and optimize them.

And if you’re dealing with a blog that’s focused on visuals, this can take ages. Tools like TinyJPG help, but you still have to resize them manually and it has an upload limit.

Python, however, can speed things up a lot.

In this article, we explore how to use Python to automatically resize and compress images so they’re the best quality for your blog.

How to optimize images using Python

First of all, we’ll need to import some modules so we can work with images and find all the files we need. We’ll be using the following modules:

  • Pillow, to manipulate the images.
  • Glob, to gather all the files.
  • OS, to create folders.
  • Path, to work with file paths.
#imports
import glob
import os
from PIL import Image
from pathlib import Path

Because we don’t want to edit the script every time we have a new folder with images, we’re using user input to ask for the source folder. We’re also asking the user for the maximum width of the images and whether or not it needs to overwrite previously optimized images. If the images just need to be optimized, we want to leave the max-width empty.

Since I’m the only one using this, I’m not building in any validation of inputs.

file_types = ('.jpg','.png', '.jpeg')
    all_images = []
    in_folder = input("Enter the path to the folder containing the images:")
    out_folder = in_folder + '/Optimized'
    
    # If we just want to optimize the images without resizing them, we can
    # leave the max width input empty
    try:
        img_max_width = int(input("How wide do you want the images to be (in px):"))
    except ValueError:
        img_max_width = 0

    overwrite_img = input("Overwrite previously optimized images? Y/N:")

Instead of putting all our optimized images in the same folder as our unoptimized images, we’re creating a folder called Optimized. If it already exists, print a statement saying it already exists.

#Create ouput folder
try: 
    os.mkdir(out_folder)
    print('Folder created: ' + str(out_folder))
except FileExistsError:
    print('Folder already exists: ' + str(out_folder))

Next, we’ll scan our input folder and add any .png.jpg, or .jpeg files to our image array.

#Get all images from input folder
for file_type in file_types: 
    all_images.extend(glob.glob(in_folder + '/*' + file_type))

Now the real work begins. For each image in our array we’ll do the following:

  • Get the file path of the image
  • Construct our output file path
  • Check if the output file (the optimized image) already exists so we can skip it if we don’t want to overwrite images
  • Resize the image to our specifications
# Resize all files and optimize
# Enumerate the list so we can track what image it is on
for index, img in enumerate(sorted(all_images)):
    img_path = Path(img)
    file_name = img_path.name
    outF = out_folder + '/' + file_name

    #Communicate what image we're working on
    print('Working on optimizing ' + str(img)) 

    # Check if the file exists and if we want to overwrite it.
    # If not, move on to the next image
    if Path(outF).is_file() and overwrite_img.upper() == "N":
        print('Image already optimized, skipping...')
        continue
    else:
        with Image.open(img) as inF:
            orig_width, orig_height = inF.size

            if img_max_width is 0:
                inF.thumbnail((orig_width,orig_height), reducing_gap= 2.0)
                inF.save(outF,inF.format)
            else:
                #Calculate ratio so we can keep the height proportionate to the width
                ratio = inF.size[0] / img_max_width 
                inF.thumbnail((img_max_width,inF.size[1]*ratio), reducing_gap= 2.0)
                inF.save(outF,inF.format)

        print(str(index + 1) + ' out of ' + str(len(all_images)) + ' images done')

We can change the reducing_gap to fine-tune the quality. But for now, we’re going to leave it at 2.

And that’s the script. But because I don’t want to open the command line, go to the source folder, and run the script every time I need to optimize images, we’ll create a .bat file on the desktop.

C:/PathToPython/python.exe "D:/Path/To/Python/Script/ImageOptimizer.py"

Now that we have this, we just have to run the .bat file, put in the path to the folder with all our images, put in the max-width of our images, and if we want to overwrite existing ones. Hooray for technology!

How well does this compress images?

To test how well this script optimizes images, I’m going to optimize 10 images using both TinyJPG and this script.

To prep this test, I’ve taken 10 images from Unsplash (1,2,3,4,5,6,7,8,9,10) and scaled them down to 700px wide using Affinity Photo.

Our script can scale the images down as well, but it wouldn’t be a good test.

ImageOriginal sizeAfter TinyJPGAfter our script
Image 1536 KB74 KB77 KB
Image 2320 KB62 KB45 KB
Image 3474 KB63 KB52 KB
Image 4627 KB251 KB108 KB
Image 51182 KB425 KB168 KB
Image 6335 KB66 KB52 KB
Image 7566 KB108 KB73 KB
Image 8234 KB70 KB23 KB
Image 9681 KB121 KB88 KB
Image 10395 KB104 KB62 KB
Total savings4006 KB4602 KB

The file size is one thing, but how’s the quality of our optimized images? To compare, I’m using the image we compressed the most (image 5).

From afar, there isn’t much of a difference — and you could argue that’s good enough.

Once you zoom in, though, our script shows a slight drop in quality. The trees get a bit blurry after using our script.

To fix this, we could increase the reducing_gap to a level where the drop is less noticeable.

But that’s our script. It has saved me a ton of time having to manually resize the images we use at Piktochart. If you want the full script, you can find it below:

Full code:

#imports
import glob
import os
from PIL import Image
from pathlib import Path


def main():
    file_types = ('.jpg','.png', '.jpeg')
    all_images = []
    in_folder = input("Enter the path to the folder containing the images:")
    out_folder = in_folder + '/Optimized'
    
    # If we just want to optimize the images without resizing them, we can
    # leave the max width input empty
    try:
        img_max_width = int(input("How wide do you want the images to be (in px):"))
    except ValueError:
        img_max_width = 0

    overwrite_img = input("Overwrite previously optimized images? Y/N:")
    
    #Create ouput folder
    try: 
        os.mkdir(out_folder)
        print('Folder created: ' + str(out_folder))
    except FileExistsError:
        print('Folder already exists: ' + str(out_folder))

    #Get all images from input folder
    for file_type in file_types: 
        all_images.extend(glob.glob(in_folder + '/*' + file_type))

    #Communicate how many images are found so we can double check manually if needed
    print(str(len(all_images)) + ' image(s) found')

    # Resize all files if needed and optimize
    # Enumerate the list so we can track what image it is on
    for index, img in enumerate(sorted(all_images)):
        img_path = Path(img)
        file_name = img_path.name
        outF = out_folder + '/' + file_name
        
        #Communicate what image we're working on
        print('Working on optimizing ' + str(img)) 

        # Check if the file exists and if we want to overwrite it.
        # If not, move on to the next image
        if Path(outF).is_file() and overwrite_img.upper() == "N":
            print('Image already optimized, skipping...')
            continue
        else:
            with Image.open(img) as inF:
                orig_width, orig_height = inF.size
                
                if img_max_width == 0:
                    inF.thumbnail((orig_width,orig_height), reducing_gap= 2.0)
                    inF.save(outF,inF.format)
                else:
                    #Calculate ratio so we can keep the height proportionate to the width
                    ratio = inF.size[0] / img_max_width 
                    inF.thumbnail((img_max_width,inF.size[1]*ratio), reducing_gap= 2.0)
                    inF.save(outF,inF.format)

            print(str(index + 1) + ' out of ' + str(len(all_images)) + ' images done')


if __name__ == '__main__':
    main()