Python: Making a Bulk Image Compression Script

There are very little tools to do this offline without using unknown executables, so follow this guide to learn in Python!

Python: Making a Bulk Image Compression Script

For some reason, there is very little tools avaliable to do this offline. So here is a very simple way to do this in Python 3!

Libraries

  • Pillow - pip install Pillow
  • sys
  • os

Requirements

We need this code to:

  • Compress any image file
  • Use command line arguments
  • Recursively search input folder
  • Export all files to a folder
  • Losslessly compress

Step-by-Step Code

For this script we want the basic usage to be:

python script.py <format to compress> <format to output> <input folder> <output folder>

We will begin by importing sys, os and Pillow.

from PIL import Image
import sys
import os

# python script.py <format to compress> <format to output> <input folder> <output folder>

To make it easier for us to re-use, lets make a simple class called BulkImageCompress. We need to setup some variables for compression settings and to pass our arguments to.

class BulkImageCompress:
    quality:int = 85
    kwargs:dict = {"progressive": True, 
                   "optimize": True}

    def __init__(self, in_type, out_type, in_folder, out_folder):
        self.in_type = in_type
        self.out_type = out_type
        self.in_folder = in_folder
        self.out_folder = out_folder

bic = BulkImageCompress(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

Now that we have our class setup, we need a method to get all the files from our directory recursively. We will call this get_files.

def get_files(self):
    self.file_list = []
    for (directory, directory_names, filenames) in os.walk(self.in_folder):
        for filename in filenames:
            if filename.endswith(f".{self.in_type}"): 
                # Append to list as full path
                self.file_list.append(os.sep.join([directory, filename]))

Recursive directory walk

Awesome, so now we have a method that creates a list of image files according to our input type and input folder.

So now we need to make a method which will compress our images according to our input type/format. It needs to be able to support .webp files, so in order to do this we need to change our kwargs passed to the Image.save function.

def compress_image(self, file_path):
    img = Image.open(file_path)
    # Get just file name
    file_name = os.path.basename(file_path)

    if self.out_type in ("jpg", "jpeg"):
        img = img.convert('RGB') # Remove transparency
        self.out_type = "jpeg" 

    # Create path with output folder, type and file name
    output_name = file_name.replace(f".{self.in_type}", f"_compressed.  {self.out_type}")

    img.save(os.sep.join([self.out_folder, output_name]), # Create full path
            self.out_type,
            quality=self.quality, # Lossless quality setting
            **self.kwargs)   # Dynamic arguments based on output type

So this now lets compress a single file, but how do we bulk compress all the files we stored in the variable self.file_list? We create a simple method called compress_all(). All this needs to do is loop through our file list and run the compress_image() each iteration.

def compress_all(self):
    for file in self.file_list:
        self.compress_image(file)

And thats it, now all you need to do is run the get_files() and compress_all() methods in that order.

Full Script

from PIL import Image
import sys
import os

# python script.py <format to compress> <format to output> <input folder> <output folder>
class BulkImageCompress:
    quality:int = 85
    kwargs:dict = {"progressive": True, 
                   "optimize": True}

    def __init__(self, in_type, out_type, in_folder, out_folder):
        self.in_type = in_type
        self.out_type = out_type
        self.in_folder = in_folder
        self.out_folder = out_folder

        if out_type == "webp":
            self.kwargs = {"method": 6, "lossless": False}

    def get_files(self):
        self.file_list = []
        for (directory, directory_names, filenames) in os.walk(self.in_folder):
            for filename in filenames:
                if filename.endswith(f".{self.in_type}"): 
                    # Append to list as full path
                    self.file_list.append(os.sep.join([directory, filename]))

    def compress_all(self):
        for file in self.file_list:
            self.compress_image(file)

    def compress_image(self, file_path):
        img = Image.open(file_path)
        # Get just file name
        file_name = os.path.basename(file_path)

        if self.out_type in ("jpg", "jpeg"):
            img = img.convert('RGB') # Remove transparency
            self.out_type = "jpeg" 

        # Create path with output folder, type and file name
        output_name = file_name.replace(f".{self.in_type}", f"_compressed.{self.out_type}")

        img.save(os.sep.join([self.out_folder, output_name]), # Create full path
                self.out_type,
                quality=self.quality, # Lossless quality setting
                **self.kwargs)   # Dynamic arguments based on output type

if __name__ == "__main__":
    bic = BulkImageCompress(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
    bic.get_files()
    bic.compress_all()

Feel free to improve on this script on my github, it's a great way to improve your Python and git usage (whilst also building history on your GitHub account!).