How To Resize Images For Your Django Project using Pillow (Blog Example)

As a Django developer, you might want to prevent users from uploading large images.

If the uploaded images are wider than the container they will be displayed in, then they are too big. For you, the developer, it means having to pay for more storage. For users, their pages load slowly because the page has to download oversized images.

The solution is to resize images on upload. This doesn't stop the user from uploading a 4MB image but it means you don't have to store it. Instead, you save a resized copy.

To do this, we need to intercept the image before it gets saved. Pillow, a Python package for processing images, has a function that will resize images. We just have to provide the image and the dimensions.

The example I am going to show you is for a blogging application. In a previous tutorial, we added a feature image to the blog post model (tutorial).

In this tutorial, we will work on the same codebase. We will limit the width of the image to 1100px.

This is what we are going to do:

  1. Write a function to calculate the new image dimensions. This will ensure the new image has the same aspect ratio as the original one.

  2. Write a function that will resize the image.

  3. Update our view to resize and save images before the post is committed to the database.

You can access the code for this tutorial on GitHub:

Before: https://github.com/ctrlz-blog/basic-django-blog/tree/%2321-how-to-resize-images--before

After: https://github.com/ctrlz-blog/basic-django-blog/tree/%2321-how-to-resize-images--after

1. Write a function to calculate the new image size.

Pillow needs the desired dimensions to resize the image. We will supply the desired width but we need to write the logic that will calculate the correct height that will preserve the aspect ratio.

It is also important that we only shrink our images, never enlarge them. Therefore, we will need to check if the original dimensions are larger than the new ones. If the original image is smaller, then we just want to keep the original image.

I have put the function in a new file called utils.py

The inputs are the size of the image (a tuple of height and width) and the maximum width of the desired image. It calculates the height and returns the new dimensions. If the original dimensions are smaller, then it returns the original dimensions.

This is the code:

# blog/utils.py

from typing import Tuple

def get_new_image_dimensions(
    original_dimensions: Tuple[int, int], new_width: int
) -> Tuple[int, int]:
    original_width, original_height = original_dimensions

    if original_width < new_width:
        return original_dimensions

    aspect_ratio = original_height / original_width

    new_height = round(new_width * aspect_ratio)

    return (new_width, new_height)

2. Write a function to resize the images

Now that we have our logic to calculate the image dimensions, we can go about resizing the image.

To do this, we will make use of Image, a class provided in the Pillow library.

This function will open and resize an image and return the resized image, which is an instance of Image.

# blog/utils.py

from PIL import Image

def resize_image(original_image: ImageFieldFile, width: int) -> Image:

    image = Image.open(original_image)

    new_size = get_new_image_dimensions(image.size, width)

    if new_size == image.size:
        return

    return image.resize(new_size, Image.ANTIALIAS)

3. Update the view

Image Width Constant

I have created another file called constants.py. I have two views (add_post and edit_post) to update. To avoid repeating myself, I have decided to store the width of my resized images in a constant which can be used in multiple places.

# blog/constants.py

class ImageWidth:
    THUMBNAIL = 150
    LARGE = 1100

The view

The logic to resize the image must go before the blog post is committed to the database. However, we still need the blog post instance so we can access the feature image.

To get around this, we use commit=False when saving the form. This creates the post instance but doesn’t commit it to the database.

def add_post(request: HttpRequest) -> HttpResponse:

    if request.method == "POST":
        form = PostForm(data=request.POST, files=request.FILES)

        if form.is_valid():
            # form.save() creates a post from the form
            post: Post = form.save(commit=False)

            if post.feature_image:
                img: Image = utils.resize_image(
                    post.feature_image, constants.ImageWidth.LARGE
                )
                img.save(post.feature_image.path)

            post.save()

            return redirect("post_detail", slug=post.slug)

    else:
        form = PostForm()

    context = {"form": form, "edit_mode": False}

    return render(request, "post_form.html", context)

The resize_image function we wrote in Step 2 returns an instance of Image. Our next step is to save the image.

Remember this:

Databases do not store images.

Saving the image has nothing to do with the post. The only reason we need to post is to access the path to save the image to. The database stores the path to the image.

img.save( saves the image to the supplied path. The image is not saved to the post.

After saving the image, the post can be committed to the database.

Conclusion

Resizing images before upload can save storage space and speed up pages by not using unnecessarily large images.

Image resizing is done using Pillow, a Python library. Pillow provides a class called Image, which has a method called resize.

Resizing the image should be done before committing the blog post to the database. In this tutorial, we resized the image in the view using functions written in utils.py.

Once an image is resized, we save the image to the same path as the original image. Images are not stored in the database, so the resized image is not bound to the blog post instance.