Wiki

Clone wiki

django-thumbnail / Home

Introducing django-thumbnail.py

Almost all of our Django projects employ some kind of thumbnail creation mechanism.

Initially, the thumbnails we needed were just the easy kind: Either scaled down versions of the initial image, or cropped to a square and then scaled down. Those are easily created in a few lines of Python, and I had created a custom Django template tag to create them. Pretty soon, caching became important, too, and I added that as well.

For our most recent projects, the design necessitated thumbnails that had exactly specified dimensions. Here are some examples}}

Screenshot

The code to create these kinds of thumbnails is a little trickier. Since I found no preexisting solution, I decided to roll my own, and now we’re sharing that solution with the world. I’ll talk a little about the algorithm first, if you just want the code, you can grab it right now. There's also a detailed HOWTO below the technical part.

The Technical Part™

So, how do we create an image that fits the correct dimensions, while still ensuring that as much as possible of the content of the original image fits?

1.png

First I tried comparing the individual sides of the image to those of the thumbnail, but that obviously resulted in nothing but lots and lots of edge cases: How do I have to scale the image if its width is slightly greater than the thumbnail’s but it’s a lot higher? What if it’s the other way round? What if the image is a rectangle and the thumbnail is a square?

The only comparable attribute of the image and thumbnail sizes is their ratio, obtained by dividing their width by their height. The ratio stays the same regardless of the actual width or height and is therefore ideal for determining exactly which part of the original image to crop. The cropped image can then be resized to obtain a thumbnail with exactly the desired dimensions.

2.png

If the thumbnail ratio is greater than the image ratio, the thumbnail is wider than the original image, and we keep the image’s width while cropping top and bottom.

If the thumbnail ratio is less than the image ratio, the thumbnail is taller than the original image, and keep the image’s height while cropping the sides.

(After cropping, we obviously resize the image to the exact thumbnail dimensions.)

3.png

The code for this is as follows (image is the original image, image.size[0] is the original image’s x size, x and y are the thumbnails dimensions):

img_ratio = float(image.size[0]) / image.size[1]
thumb_ratio = float(x) / y
x = int(x); y = int(y)

if(img_ratio > thumb_ratio):
    c_width = x * image.size[1] / y
    c_height = image.size[1]
    originX = image.size[0] / 2 - c_width / 2
    originY = 0
else:
    c_width = image.size[0]
    c_height = y * image.size[0] / x
    originX = 0
    originY = image.size[1] / 2 - c_height / 2

cropBox = (originX, originY, originX + c_width, originY + c_height)
image = image.crop(cropBox)
image.thumbnail([x, y], Image.ANTIALIAS)

Some comments:

  • In the first two lines, we determine the image’s ratio and that of the thumbnail.
  • Then we determine, according to the ratio, how wide the thumbnail would be, if it were as high as the image. (For image ratio > thumbnail ratio. Swap width and height for the other way round.)
  • We create a tuple called cropBox, (line 16) which we fill with the coordinates of four corners of an imaginary box that covers as many of the image’s pixels as possible while retaining the desired ratio of the thumbnail.
  • We crop the image based on the cropbox in line 17.
  • Finally, in line 18, we resize the image, which now has the correct ratio, to the correct size as well, thereby creating the thumbnail.

We have some edges cases as well. An easy one is the ratio of the thumbnail being the same as the original image. In this case, the else statement in line 10 is executed, but it could just as well be the the if. Either way, the dimension that is calculated as a product of ratio and the other dimension will be correct.

Another edge case appears if one or both sides of the original image are smaller than the respective side or sides of the thumbnail. In this case, we use the length of the original image’s side length instead of the length as calculated by the ratio.

4.png

This creates images that might in fact be smaller than the desired dimensions, instead of scaling the image somehow to fit. We decided this would be better in most cases, since images that are enlarged usually appear blocky or mangled.

So we just cut the size off:

if (image.size[0] < x):
    x = image.size[0]
if (image.size[1] < y):
    y = image.size[1]

As for file saving and caching, we create a file name based on the original file’s name plus the dimensions, like image-640x480.png. If such a file already exists and its timestamp is newer than the original files (meaning the original has not been replaced by another version) we skip all the time consuming computations and simply return the existing image.

That’s all the code-explaining I will do today. For more, ask a question on Bitbucket or read the source code yourself.

The Part Where I Explain How To Use This Stuff™

Of course you can simply use the described algorithm to create your own image thumbnail template tag if you want. Or you can use it in your Python application without using Django. Or you can port it to C. Whatever.

Oh, by the way, you can use all the code in this blog entry and in the django-thumbnail code repository for free, regardless of wether you make money with it or not. No, you don’t have to make your code open to the general public or to us. You can think of breakthesystem when you need a serious website, a cool iPhone app or an awesome Mac application, though. Or you can buy me a beer. Just contact me for any of that.

And you should follow me on Twitter, obviously. :)

Okay, let’s go. I’m assuming you’ll have a model in your Django application that looks a bit like this, by the way:

class Picture(models.Model):
    title = models.CharField(max_length=255)
    image = models.ImageField(upload_to = "images/%Y-%m-%d")

First, install the template tag. To do that , download the file thumbnail.py and place it in YourDjangoProject/your_django_app/templatetags/tumbnail.py. You’ll probably have to create the templatetags folder. If you’ve just created it, you’ll also have to create an empty file called __init__.py in the templatetags folder.

In your template, add the statement {% load thumbnail %} somewhere before you actually use the thumbnail tag. You’ll need that statement only once, so I recommend placing it at the very top of your template. Then use the tag like this:

<a href="{{ picture.image.get_url }}">
    <img src='{{ picture.image|thumbnail:"320x240" }}' />
</a>

This will create a thumbnail of 320 by 240 pixels that links to the original picture.

You can also do this:

<a href="{{ picture.image.get_url }}">
    <img src='{{ picture.image|thumbnail:"320x0" }}' />
</a>

The above code will create a thumbnail that is 320 pixels wide. In other words, this is the image scaled down to a width of 320px. You can also use something like thumbnail:0x240 for a variable width and fixed height.

Finally, thumbnail.py can do one more trick:

<a href="{{ picture.image.get_url }}">
    <img src='{{ picture.image|thumbnail_with_max_side:"320" }}' />
</a>

The above code will create a scaled down version of the image that is 320 pixels on the longer side, whichever one that is.

And that’s it. I hope you like it. You should now g have a look at the current source code, or comment, or follow the project. Or god forbid, use it. Have fun! :)

Updated