Oct 30

A Django image thumbnail filter

Django filters are a pretty cool idea. I needed to be able to generate image thumbnails. I found some existing code, and fixed it up to, well, work, and also behave better with custom upload_to locations.

Django's filters are pretty cool. Django comes with a bunch of useful default filters, letting you do stuff like lowercase text, centre text in a block, and so on. I was quite surprised to find that there wasn't an image thumbnail filter. This seemed like a pretty common requirement.

Always loth to write code when I don't have to, I set about searching for an existing filter. I came across a post about something else completely that happened to contain such a filter.

Naturally, it didn't quite work for me in its existing form. In particular:

  • It assumes your thumbnails directory is directly under your MEDIA_ROOT
  • It's hardcoded to use UNIX-style path separators
  • It needlessly recomputes an image URL twice
  • There's a bug so that if a thumbnail already exists, then it won't return any URL at all.

I've fixed all these problems, and you can find my code below (I'll also post it on djangosnippets.org, since Google hasn't visited my blog for a while now!).

I'm not 100% happy with the solution: I don't like the way that you have to effectively repeat the upload_to in the parameters that you have already coded in your model. I also don't like the way that multiple parameters are encoded in the single string parameter (although that's more a consequence of the way Django filters work: you can only have zero or one parameters. I guess it prevents you from being tempted to put too much logic in these things).

Anyway, here's the code. It seems to work for me, let me know if you find any bugs.

(Apologies if some of the lines are truncated. You should still be able to highlight them for copy and pasting. One day I'll fix the site's CSS...!)

THUMBNAILS = 'thumbnails'
SCALE_WIDTH = 'w'
SCALE_HEIGHT = 'h'

def scale(max_x, pair):
x, y = pair
new_y = (float(max_x) / x) * y
return (int(max_x), int(new_y))

# Thumbnail filter based on code from http://batiste.dosimple.ch/blog/2007-05-13-1/
@register.filter
def thumbnail(original_image_path, arg):
if not original_image_path:
return ''

if arg.find(','):
size, upload_path = [a.strip() for a in arg.split(',')]
else:
size = arg
upload_path = ''

if (size.lower().endswith('h')):
mode = 'h'
else:
mode = 'w'

# defining the size
size = size[:-1]
max_size = int(size.strip())

# defining the filename and the miniature filename
basename, format = original_image_path.rsplit('.', 1)
basename, name = basename.rsplit(os.path.sep, 1)

miniature = name + '_' + str(max_size) + mode + '.' + format
thumbnail_path = os.path.join(basename, THUMBNAILS)
if not os.path.exists(thumbnail_path):
os.mkdir(thumbnail_path)

miniature_filename = os.path.join(thumbnail_path, miniature)
miniature_url = '/'.join((settings.MEDIA_URL, upload_path, THUMBNAILS, miniature))

# if the image wasn't already resized, resize it
if not os.path.exists(miniature_filename) \
or os.path.getmtime(original_image_path) > os.path.getmtime(miniature_filename):
image = Image.open(original_image_path)
image_x, image_y = image.size

if mode == SCALE_HEIGHT:
image_y, image_x = scale(max_size, (image_y, image_x))
else:
image_x, image_y = scale(max_size, (image_x, image_y))


image = image.resize((image_x, image_y), Image.ANTIALIAS)

image.save(miniature_filename, image.format)

return miniature_url

I've disabled comments for now due to spam problems - I'll turn them back on when I've fixed it!

This won't be published anywhere, it's just in case I need to contact you.

You can use Markdown in your comments. Be sensible!

Sorry about this, but I don't want spam comments.