Automatic Thumbnail Image Creation for Django

Django comes with a lot of batteries included in the core code including an admin interface that is incredibly easy to use as well as being easily extensible. One of the missing batteries is a method to automatically create thumbnail images for media being uploaded through the application. This is my current solution for the thumbnail creation which I use on a couple of sites. This allows me to upload an image with or without a thumbnail file and have the system create a thumbnail when needed.

This solution requires the Python Image Library (PIL) for the image processing. The PIL library may not be available automatically for you hosting package. You will then add the create_thumb definition either in the models.py file directly or through an import statement. The final requirement is to adjust your "media" model to override for the default save() method used by all models so the function will run when a new media file is saved.

create_thumb() definition

def create_thumb(media, width, height):
	from django.conf import settings
	import Image, os
	ALLOWED_EXTS = {'JPG':'JPEG','JPEG':'JPEG','PNG':'PNG','GIF':'GIF','TIF':'TIFF','TIFF':'TIFF'}
	EXT = str(media.name).rsplit('.',1)[1].upper()
	if EXT in ALLOWED_EXTS:
		CREATE_THUMBNAIL = True
		thumb = '%s' % ('T.'.join(str(media.name).rsplit('.',1)))
	else:
		CREATE_THUMBNAIL = False
		thumb = 'img/default.%s.png' % (EXT.lower())
	if CREATE_THUMBNAIL:
		path = settings.MEDIA_ROOT
		t = Image.open(media.path)
		w, h = t.size
		if float(w)/h < float(width)/height:
			t = t.resize((width, h*width/w), Image.ANTIALIAS)
		else:
			t = t.resize((w*height/h, height), Image.ANTIALIAS)
		w, h = t.size
		t = t.crop( ((w-width)/2, (h-height)/4, (w-width)/2+width, (h-height)/4+height))
		t.save(path + thumb, ALLOWED_EXTS[EXT])
		os.chmod(path + thumb, 0644)
	return thumb

create_thumb() used in a Model class definition

class Media(models.Model):
	...
	media = models.FileField(upload_to="%Y/%m")
	thumb = models.FileField(upload_to="%Y/%m", blank=True, null=True)
	...
	# ++++++++ ++++++++ ++++++++ ++++++++
	def save(self):
		if self.media and not self.thumb:
			super(Media, self).save()
			self.thumb = create_thumb(self.media, 125, 125)
			super(Media, self).save()
		else:
			super(Media, self).save()

Stepping through the code:

First the Media model uses two FileFields. One for the main media and one for the thumb. The thumb field is optional (blank=True, null=True) to allow the main media to be saved without an error. I choose the FileField instead of the ImageField because I also wanted to be able to upload more than images to the media server.

class Media(models.Model):
	...
	media = models.FileField(upload_to="%Y/%m")
	thumb = models.FileField(upload_to="%Y/%m", blank=True, null=True)
	...

The next part overrides or extends the save() method used by Django model classes. The create_thumb() is only called when a thumbnail image is not provided (and not self.thumb:). The first super().save() is needed because the create_thumb() function uses the file object created when the media file is saved. This also has the benefit of saving the main media file before attempting to create the thumbnail so if the thumbnail process encounters an error, the original media has already been saved to the server.

The create_thumb() then runs creating and saving a new file as well as returning a portion of file's media path which is saved by the second super().save() line. It is important to note that a full file object is not saved for the thumbnail. If one deletes the record for the Media object through the admin, Django will only delete the media file and not the thumb file because the full file path is not saved.

I am looking for a better method to all this madness but I have yet to tinker with this any further since it meets my needs.

class Media(models.Model):
	...
	def save(self):
		if self.media and not self.thumb:
			super(Media, self).save()
			self.thumb = create_thumb(self.media, 125, 125)
			super(Media, self).save()
		else:
			super(Media, self).save()

The dictionary at the beginning of the create_thumb() definition is used for translating the correct library function PIL uses for saving the thumbnail file as the same media type as the original. JPEG and TIFF files can have three and four letter file extensions but the Image library uses the four letter description to save the file in the correct format.

def create_thumb(media, width, height):
	ALLOWED_EXTS = {'JPG':'JPEG','JPEG':'JPEG','PNG':'PNG','GIF':'GIF','TIF':'TIFF','TIFF':'TIFF'}
	...
	t.save(path + thumb, ALLOWED_EXTS[EXT])

I defined the media field as a FileField so more than images could be added to the media library. The create_thumb() definition will default to a file path when the media file does not have a matching file extension. I have pre-saved thumbnail images for some of the common files extensions so the system can use default.tgz.png for tar guzip archives or default.pdf.png for PDF files.

else:
	CREATE_THUMBNAIL = False
	thumb = 'img/default.%s.png' % (EXT.lower())

The create_thumb() definition will use the file system path set in MEDIA_ROOT as the base path for the thumbnail image.


	path = settings.MEDIA_ROOT

The final step should not be needed as the creation mask for the web server should not be 777 but just incase this is true, after saving the thumbnail image create_thumb() will set the permissions to 644 (-rw-r--r--) for the file.


	os.chmod(path + thumb, 0644)

I only needed one thumbnail size 125x125px but you could loop through a couple of blocks of code to create a few sizes the way Flickr does for on their service. Any number of modifications can be made including changing the storage system, meta data changes, what ever your needs may be. The last thing you'll want to do is test-test-test. You can make your own test files or you can download the test files I used at: http://media.projectmouse.org/2009/10/djangoTestImgs.bz2

About RSS Feed Search