Handle your images the right way using Django

Author: Darryl Buswell

Dec 29, 2020

Use of media objects, including images, photos and animations are key towards making an engaging and successful website. But unfortunately, not everyone who visits your page is going to have access to a high speed connection. In fact, your typical visitor is likely a mobile user who is browsing your site via a public wifi point or even via their own capped data plan.

So, we have two conflicting priorities here. On one side, we want to make our page visually engaging, but on the other side, we have to think carefully about treating bandwidth as a currency. Where we are making the smartest possible use of image formatting, compression, sizing and serving, so that our users can get the best bang for each transferred bit.

Now, if you are using a fully fledged CMS, you may already be spoilt by features such as automatic image resizing on upload and lazy media loading, all working behind the scenes. But for Django users, we are going to need to take a more proactive approach. Fortunately, with Django being built on Python, we do have direct access to the amazing Pillow library. Pillow is fantastic at converting image formats, resizing, compressing and more. And best of all, it's super fast.

So how can we take advantage of Pillow in our Django project so we can best pre-process the images we want to serve? Well, one simple method is to create a custom image resizing routine using Pillow, which can accept defined image size parameters and output formats. And to use this method via our model's custom save function. So that any time an image is uploaded and saved to the appropriate image field, it is automatically resized, compressed, reformatted and saved to another image field in the same model, ready to be served to our page visitors.

To start things off, make sure that you have included Pillow in your list of project requirements. As of writing this post, it looks like Pillow is on 8.0.1.


Next, let's create a custom helper function, which we can import for use by our Django model. Again, we want this function to be able to accept parameters for resizing and format changes, so that it can use the magic of Pillow to convert our images as needed. By the way, it's not great practice to be capturing all exceptions of a call like this and returning as None, so we recommend you build on this example to better handle any exceptions you may come across.

import os
from io import BytesIO
from PIL import Image as pil
from django.core.files import File

def get_resize_image_or_none(image, size, prefix, format=None):

        im = pil.open(image)
        im.thumbnail(size, pil.ANTIALIAS)

        filename = os.path.basename(image.name)
        basename = os.path.splitext(filename)[0]

        if format in ['jpeg', 'png', 'bmp'] and format != im.format:
            im = im.convert('RGB')
            format = im.format.lower()

        thumb_io = BytesIO()
        im.save(thumb_io, format)

        return File(thumb_io, name=prefix + basename + '.' + format)

       return None

Just a couple of things to point out here. You'll note in the code above that we are only accepting 'jpeg', 'png' and 'bmp' as possible format options for any image type conversions. You may want to extend on this, but do be careful in handling browser compatibility when dealing with other formats. Also note that we have included a parameter for filename prefix, so that we can make some distinction between our raw and processed image files if they are stored in the same location. And finally, note that we are making use of Pillow's thumbnail function for resizing our images. This is important, as we are looking to keep the original image aspect ratio as part of any resizing. So our passed size parameters are in-fact being treated like upper limits on the created image.

Next, we need to include a custom save method in our model so that when an image object is provided for the 'image' field, Django calls our routine, resizes the image, re-formats to jpeg (if necessary), and saves the new image to the 'image_md' or 'image_lg' as appropriate.

class Image(models.Model):
    image = models.ImageField(upload_to=get_upload_path, max_length=255, blank=True, null=True)
    image_md = models.ImageField(upload_to=get_upload_path, max_length=255, blank=True, null=True)
    image_xl = models.ImageField(upload_to=get_upload_path, max_length=255, blank=True, null=True)

    def save(self, *args, **kwargs):

        if self.image:
            self.image_md = get_resize_image_or_none(self.image, size=(800, 600), prefix='md-', format='jpeg')
            self.image_xl = get_resize_image_or_none(self.image, size=(1920, 1440), prefix='xl-', format='jpeg')

            self.image_md, self.image_xl = None, None

        super().save(*args, **kwargs)

And it's as simple as that. Now when you go to serve your image content, you can use the smaller 800x600 image, as stored in 'image_md' for any summary/ thumbnail representation. And save the larger 1920x1440 version as stored in 'image_xl' for any exploded, full-page representations of that image. And you can obviously build on this further depending on your use-case, so that you are resizing and reformatting images as needed for your requirements and keeping total transfer to users as efficient as possible.

As one final hint. Remember that you have the option of lazy loading even your small sized images, so that they aren't loaded by the user until they scroll down the page to include the image in their viewport. There are a number of options out there to handling lazy loading, but the most simple method is to include the loading="lazy" tag in your img object.

<img src="{{ image_md.url }}" alt loading="lazy">

And that's it. You are well on your way to serving your media rich content in an efficient and optimized manner. If you want to learn more, we recommend you take a look at the recommendations and assessment offered by the Google PageSpeed Insights tool. You'll no doubt find recommendations on much more than just media handling, including script prioritization, asset caching and dependency handling. All critical to serving your page in the most efficient manner possible.

Share this post: