Subscribe to RSS Feed

Django Thumbnails

There are many examples out there of automatic thumbnail generation in django models so I wanted to show my method which I feel provides a lot of control as well as an automated process.

Below is the full django model for an image which contains the uploaded image as well as a thumbnail version. That’s all very standard but it’s the save method that does the work and so I will break that code up and explain in sections below.

from django.db import models
from const import CROP_SIZE, MAX_PHOTO_SIZE, THUMBNAIL_SIZE
from PIL import Image
from StringIO import StringIO
from django.core.files.base import ContentFile
import os

def updateContent(field, name, img, fmt="JPEG"):
    fp = StringIO()
    img.save(fp, fmt, quality=128)
    cf = ContentFile(fp.getvalue())
    if field:
        os.remove(field.path)
        field.save(name=name, content=cf, save=False)

class Photo(models.Model):
    photo = models.ImageField(upload_to='pics')
    thumbnail = models.ImageField(upload_to='pics/thumbnails', editable=False)

    #area tuple = left, upper, right, lower coordinates
    def save(self, area=CROP_SIZE, crop=False, noResizing=False):

        if noResizing:
            super(Photo, self).save()
            return

        imgFile = Image.open(self.photo.path)
        fmt = imgFile.format

        #Convert to RGB
        if imgFile.mode not in ('L', 'RGB'):
            imgFile = imgFile.convert('RGB')

        # make sure photo doesn't exceed our max photo size for the site
        resizeImg = imgFile.copy()
        resizeImg.thumbnail(MAX_PHOTO_SIZE, Image.ANTIALIAS)
        updateContent(self.photo, self.photo.name, resizeImg, fmt)

        thumbImg = imgFile.copy()
        if crop:
            thumbImg = thumbImg.crop(area)
            thumbImg.load()
            thumbImg = thumbImg.resize(THUMBNAIL_SIZE, Image.ANTIALIAS)
        else:
            thumbImg.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)

        updateContent(self.thumbnail, self.photo.name, thumbImg, fmt)

        super(Photo, self).save()

So there you go. You’ll notice that we have two ImageField values in this model. The field namedphoto is the one that gets an image uploaded to it but the one named thumbnail is set to uneditable, this is because we are going to generate the thumbnail file automatically on save.

Taking a look at the parameters in the save method you’ll notice that by default crop is False andnoResizing is also False. What it is actually doing by default is creating a thumbnail of the photo ImageField. This is useful for the site I created this model for but default actions are easily changed here.

if noResizing:
    super(Photo, self).save()
    return

When the noResizing parameter is set to True then as expecting nothing happens in this custom save method. The super save method just gets called and thats it, this is really useful if we were to add other fields to this model that don’t require any image manipulation to go on.

imgFile = Image.open(self.photo.path)
fmt = imgFile.format

#Convert to RGB
if imgFile.mode not in ('L', 'RGB'):
    imgFile = imgFile.convert('RGB')

So first things first, with this bit of code we want to open up the file that has been uploaded to thephoto ImageField ready for thumbnail manipulation. Whilst we have this file open we can remember it’s format and make sure it’s set to RGB mode for best results.

# make sure photo doesn't exceed our max photo size for the site
resizeImg = imgFile.copy()
resizeImg.thumbnail(MAX_PHOTO_SIZE, Image.ANTIALIAS)
updateContent(self.photo, self.photo.name, resizeImg, fmt)

Next is a step that is also required for the site I made this model for in that we don’t want any images uploaded to be to large in dimension. So by making a thumbnail of the actual photo field we ensure that any image bigger than MAX_PHOTO_SIZE will get scaled down but any image smaller than that will remain the size it was uploaded as. In this case MAX_PHOTO_SIZE is a tuple (500, 500). You’ll also notice we call the function updateContent, I’ll explain this function later.

thumbImg = imgFile.copy()
if crop:
    thumbImg = thumbImg.crop(area)
    thumbImg.load()
    thumbImg = thumbImg.resize(THUMBNAIL_SIZE, Image.ANTIALIAS)
else:
    thumbImg.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)

    updateContent(self.thumbnail, self.photo.name, thumbImg, fmt)

Now we get onto the actual thumbnail creation for our thumbnail field. First we make a copy of the uploaded image, then if we have set crop to True and specified a tuple for the area parameter then we are going to create a thumbnail from the selected area. The area parameter is a tuple specifying left, upper, right, lower coordinates (0,0,200,150). We use the PIL library in order to crop from an area of the image. After cropping we must call load in order to set that image to the newly cropped selection. Once we have cropped we still need to make sure the image is the size of our required thumbnail size THUMBNAIL_SIZETHUMBNAIL_SIZE is a tuple (200,150). The reason we use resizeinstead of thumbnail is because for this site we want to make sure any cropped selection that is smaller than the thumbnail size needed gets scaled up to that size once a crop area is actually specified.

If a crop area has not yet been specified on save then by default we are just going to call thumbnailon the image which will scale the image down if it’s bigger than THUMBNAIL_SIZE or just leave it be if it’s smaller.

def updateContent(field, name, img, fmt="JPEG"):
    fp = StringIO()
    img.save(fp, fmt, quality=128)
    cf = ContentFile(fp.getvalue())
    if field:
        os.remove(field.path)
    field.save(name=name, content=cf, save=False)

After each time we rescale the uploaded image you will notice we call the updateContent function. This function is expecting the field from our model to set the image for, the name of the image file so we know what to save it on disk as, the image itself and the format of the image. This function creates a StringIO and saves the image to that. We then use the method from django.core.files.basecalled ContentFile which gets us the content of the file ready for saving to any sort of FileField in a model. We then check to see if the field has been set before, if so then we want to delete the old file before we save the new one. This is done because when changing the content of an ImageField in a Django model the file on disk doesn’t get deleted, it’s only when you delete the model instance that a file gets deleted. Finally we call save on the field specifying the file name, content and stating that we don’t want to actually save yet. We don’t save yet because the last thing that happens in our custom save method is that we will call the super save which will actually do the saving for us.

So there you go, I find this method of saving useful because it means I get thumbnails straight away but also gives the ability for the user to state what part of their image they actually want to use as a thumbnail if they wish. I hope it helps others out deciding how they want to handle thumbnail creation for their models that handle images.

Leave a Reply