Working with Django ImageField

My notes for smoother working with this model field. Saving using model forms, Image, deleting and resizing.

Published: Feb. 3, 2021
App Store

Django ImageField is great solution to associate images with your models. It is also a field that I most often have some issues with (because saving images is a bit more complicated than say text or a number). For this reason I decided to write this post as a future reference to myself and hopefully it will help someone else too.

To even work with the ImageField you need the Pillow package.

pip install Pillow

When specifying the field definition, there is an option to select the directory where the images will be stored. For example:

header_image = models.ImageField(upload_to='blog-images/')

This is a relative path inside the media directory. Make sure you have the MEDIA_ROOT configured in your settings file.

Saving images to ImageField

There are numerous ways of saving actual image inside this field. The easier option is to use ModelForm, which you can just save (assuming it is valid) in your view and done.

However, you need to include the request.FILES while creating the form like this:

form = NewImageForm(request.POST, request.FILES)

And for a brief visit to the HTML, the <form> needs special definition too in a form of the enctype attribute.

<form method="post" action="{% url 'new-image' %}" enctype="multipart/form-data">

Saving "raw" images

In my use cases I am frequently saving rendered images which are PIL.Image type. You cannot pass these directly to the ImageField. There are a couple ways to do this, I am using BytesIO to help me together with File type from Django itself.

It looks something like this:

blob = BytesIO()
rendered_image.save(blob, 'PNG')
image_field.save('Image.png', File(blob))

I have removed everything not necessary (like creating an unique filename, the rendering part) to really focus on this workflow.

Another thing to note it that ImageField won't delete previous images. If you store new image using the same name (maybe generating this from the model uuid), then the new filename will have appended random characters so the names don't clash.

If you don't want to keep old images around, you can delete them via the delete method on the ImageField. So if we modify the previous example it looks like this:

blob = BytesIO()
rendered_image.save(blob, 'PNG')
image_field.delete(save=False)
image_field.save('Image.png', File(blob))

Because the field will be saved on the next line, we can opt-out with the save parameter while deleting.

Resizing images

There is no built-in support to resize images in ImageField. You either need to find a 3rd party library with image fields that support resizing (traditionally you will specify the desired width or height as a parameter when defining the field) or create the resizing code yourself.

The PIL.Image has resize method you can use for this purpose. It looks like this:

resized = image.resize((new_width, new_height), Image.ANTIALIAS)

You can also use this method to resize only certain region inside the original image. Check the official docs for more info.

I personally miss the option to specify either width or height and keep the aspect ratio. This is useful if you have target size (say width of 800px) you want to apply to all images and keep their aspect ratio.

The calculation is not complicated. I have expanded the code a bit to include more variable names:

image_width = image.size[0]
image_height = image.size[1]
width_percent = (desired_width / float(image_width))
new_height = int((float(image_height) * float(width_percent)))

Basically we are calculating the percentage of the new desired_width relative to the original and then using this same percentage to calculate appropriate height.

I am long way from Python pro, so if you have improvements to this code, I will be happy to hear it.

Beware user input

If you are accepting images from your user, you need to take extra care. Matthias K mentioned this on Twitter. When resizing user submitted images (or maybe all for good measure) prepare yourself for possible IOError exceptions. You can also check Matthias' package django-imagefield which has these protections built-in and also other extra features.

Bluesky logo

Follow on Bluesky to not miss new posts

Filip Němeček profile photo

WRITTEN BY

Filip Němeček Mastodon

iOS blogger and developer with interest in Python/Django.

iOS blogger and developer with interest in Python/Django.