Post

Image Compression in Python

How to compress images in Python?

Image Compression in Python

Recently I had to write a script to compress batches of images for this personal blog; this is a part of the efforts to improve SEO. Here I am keeping some notes about the general ideas and important details of the script:

The Big Picture

Why

Images displayed on a website are slowing down loading web pages in your browser. Depending on how many images a site has and how big they are, this might hinder the user experience of the website.

In addition to that, compressed images in most cases are indiscernible in quality for most users, even with a staggering 40% size-reduction lossy compression (see Does Size Matter?)!

Therefore, it makes sense to compress images of my personal website to improve UX without sacrificing the perceived image quality1.

What

Many images used by my site are JPEG images; they can be easily further compressed. The goal is to keep the quality level at around 65% of the original images and keep only 15% quality level for the LQIP version of the original images.

The choice of 65% and 15% was not random; I did some experiments to find out the ‘best’ compression rate that does not produce visually perceptible quality degradation on my screen.

I do not want to manually compress images because I have many pictures on the site; I’d like to compress them in one go by a script. So, I need to find software tools that support JPEG image compression and can control compression quality level (how much data is kept after being compressed).

How

After some simple research, I found popular tools like ImageMagick and the Python library pillow are viable solutions. In the end, I chose to use Python because I enjoy writing Python programs more 😏.

The Code

🎉 Here you go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def compress_at(dir: Path) -> None:
    '''Function to compress photos in a directory `dir`'''

    for img in dir.iterdir():
        with (
            Image.open(img) as im,
            # Apply original image orientation (stored in EXIF metadata of the original image)
            # to the new image before compression and saved (by calling `.save()`)
            ImageOps.exif_transpose(im) as cim,
            ImageOps.exif_transpose(im) as lqip
        ):
            # Remember size before compression and report to the user
            im_size = img.stat().st_size / 1000

            ...

            # Construct filenames
            compressed_file = dir / f'compressed-{img.name}'
            lqip_file       = dir / f'lqip-{img.name}'

            # Compress and save images
            cim.save(compressed_file, 'jpeg', quality=65)
            lqip.save(lqip_file, 'jpeg', quality=15)

            # Remember size after compression and report to the user
            cim_size  = compressed_file.stat().st_size / 1000
            lqip_size = lqip_file.stat().st_size       / 1000

            ...

Important Details

  • Pay attention to the following code:
1
2
3
4
    # Apply original image orientation (stored in EXIF metadata of the original image)
    # to the new image before compression and saved (by calling `.save()`)
    ImageOps.exif_transpose(im) as cim,
    ImageOps.exif_transpose(im) as lqip

This code is necessary; otherwise, the image after compression might get the wrong orientation. Orientation is a field in EXIF metadata. When saving a new image after compression (like using the .save() method in the code), the EXIF of the original image is automatically discarded; therefore, we need a way to keep the original EXIF information and write it to the newly compressed image.

After reading this post and checking pillow’s documentation on the exif_transpose method, I realized that we can use the exif_transpose function to apply the original orientation to the new image, just as hinted in the documentation:

If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data.

  • Images can be compressed by using the .save() method directly and with the quality keyword parameter to control the quality level of the compressed image, like the following:
1
2
    # Compress and save images
    cim.save(compressed_file, 'jpeg', quality=65)

Footnotes

  1. Pay attention! Image compression only makes sense when applying to or converting to lossy compression formats, like JPEG (PNG is a lossless image format!). ↩︎

This post is licensed under CC BY 4.0 by the author.