Image resizing on file uploads. Doing it the easy way.

Posted by: barbara | Date: Jan 13, 2010 | Category: django python

Let me start with this proviso: Math is not my strong subject (okay, just stop right there with the Barbie jokes). I'll do just about anything to avoid writing complex equations. And most of the time, I don't have to. Most of the time, I can find alternative solutions, dependent on the type of problem I'm trying to solve of course.

In this case, I'm working on a photo gallery app for a new project (which will be up on github shortly - at this point, it's still too WIP to be put on display). One of the features I'm building in is automated image resizing - an admin user will be able to upload an original image of whatever dimension and have two separate additional images created (a thumbnail and a mid-sized "medium" image).

In my models.py, I'm importing from PIL:

from PIL import Image
import sys, time

My Photo class has three fields for handling the paths to these images - "photo_thumb" and "photo_medium" are for storing the paths of the new images once they've been created. And "photo_original" is the file upload field. I'm using a dynamic file path (each original image and its subsequent resized counterparts are stored in a unique folder named for the timestamp, all under the main gallery folder) in anticipation of duplicate file names.

class Photo(models.Model):
    """
    three size sets:
        thumbnail (photo_thumb)
        medium (photo_medium)
        original (photo_original)
    """

    now = str(int(time.time()))
    filepath = 'gallery/'+now+'/'

    photo_original = models.FileField('original file upload', upload_to=filepath)
    photo_medium = models.CharField(max_length=255, blank=True)
    photo_thumb = models.CharField(max_length=255, blank=True)
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    content_date = models.DateField()
    permissions = models.CharField(max_length=255)
    photo_credits = models.CharField(max_length=255, blank=True)
    approved = models.BooleanField(default=False)
    active = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

I've added a few additional methods to return paths to the thumbnail, medium, and original images (these are not so different from get_absolute_url()).

    def get_thumb(self):
        return "/site_media/%s" % self.photo_thumb

    def get_medium(self):
        return "/site_media/%s" % self.photo_medium

    def get_original(self):
        return "/site_media/%s" % self.photo_original

And all the image resizing magic happens here, where I'm overriding the model's save() method.

    def save(self):
        sizes = {'thumbnail': {'height': 100, 'width': 100}, 'medium': {'height': 300, 'width': 300},}

        super(Photo, self).save()
        photopath = str(self.photo_original.path)  # this returns the full system path to the original file
        im = Image.open(photopath)  # open the image using PIL

	# pull a few variables out of that full path
        extension = photopath.rsplit('.', 1)[1]  # the file extension
        filename = photopath.rsplit('/', 1)[1].rsplit('.', 1)[0]  # the file name only (minus path or extension)
        fullpath = photopath.rsplit('/', 1)[0]  # the path only (minus the filename.extension)

        # use the file extension to determine if the image is valid before proceeding
        if extension not in ['jpg', 'jpeg', 'gif', 'png']: sys.exit()

        # create medium image
        im.thumbnail((sizes['medium']['width'], sizes['medium']['height']), Image.ANTIALIAS)
        medname = filename + "_" + str(sizes['medium']['width']) + "x" + str(sizes['medium']['height']) + ".jpg"
        im.save(fullpath + '/' + medname)
        self.photo_medium = self.filepath + medname

        # create thumbnail
        im.thumbnail((sizes['thumbnail']['width'], sizes['thumbnail']['height']), Image.ANTIALIAS)
        thumbname = filename + "_" + str(sizes['thumbnail']['width']) + "x" + str(sizes['thumbnail']['height']) + ".jpg"
        im.save(fullpath + '/' + thumbname)
        self.photo_thumb = self.filepath + thumbname

        super(Photo, self).save()

As I was researching to figure out how I wanted to do this, I came across a number of snippets and code samples that all used comparisons of images' aspect ratios, comparisons ranging from the simple to the complex. Under some circumstances, it's certainly necessary to do that. But in my case, I just need to produce a set of thumbnails that will not exceed a certain height or width, so that they look relatively uniform on a gallery page.

So for my purposes - and hopefully there are similar cases out there - there's no need to compare image dimensions to determine a resize rate. Instead, I'm using PIL's thumbnail() method. Thumbnail() takes a sizes tuple (w, h) and an optional filter as arguments. When thumbnail() resizes, it produces an image no larger than the given dimensions, and maintains the original aspect. Problem solved!

(For more information on thumbnail(), click here, and for PIL in general, click here.)

Just a note - if I were going to be doing anything with aspect ratios, I'd have done something like this - the size attribute returns a tuple containing the image's width and height in pixels:

        pw = im.size[0]
        ph = im.size[1]

And calculating the aspects of the original image and the incoming dimensions is this simple:

        pr = float(pw) / float(ph)
        mr = float(sizes['medium']['width']) / float(sizes['medium']['height'])

Incidentally, in my save() method I'm creating the medium image first and then cascading down to the thumbnail because, as you might have noticed, I'm leaving the temp image assigned to "im" all the way down. If it mattered to you and you needed to create the images in some order other than largest to smallest, you'd need to use different var names. Just something to be aware of.

Also, that sizes dict at the top may not stay there - I'm trying to come up with a sensible way to make the sizes configurable through the admin. At the moment, I'm considering adding a few more fields to the model and passing them in that way. But if you've got a better idea, let me know.

And here's that models.py, all together (minus a few other classes):

from PIL import Image
import sys, time

from django.db import models

class Photo(models.Model):
    """
    three size sets:
        thumbnail (photo_thumb)
        medium (photo_medium)
        original (photo_original)
    """

    now = str(int(time.time()))
    filepath = 'gallery/'+now+'/'

    photo_original = models.FileField('original file upload', upload_to=filepath)
    photo_medium = models.CharField(max_length=255, blank=True)
    photo_thumb = models.CharField(max_length=255, blank=True)
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    content_date = models.DateField()
    permissions = models.CharField(max_length=255)
    photo_credits = models.CharField(max_length=255, blank=True)
    approved = models.BooleanField(default=False)
    active = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    def get_thumb(self):
        return "/site_media/%s" % self.photo_thumb

    def get_medium(self):
        return "/site_media/%s" % self.photo_medium

    def get_original(self):
        return "/site_media/%s" % self.photo_original

    def save(self):
        sizes = {'thumbnail': {'height': 100, 'width': 100}, 'medium': {'height': 300, 'width': 300},}

        super(Photo, self).save()
        photopath = str(self.photo_original.path)  # this returns the full system path to the original file
        im = Image.open(photopath)  # open the image using PIL

	# pull a few variables out of that full path
        extension = photopath.rsplit('.', 1)[1]  # the file extension
        filename = photopath.rsplit('/', 1)[1].rsplit('.', 1)[0]  # the file name only (minus path or extension)
        fullpath = photopath.rsplit('/', 1)[0]  # the path only (minus the filename.extension)

        # use the file extension to determine if the image is valid before proceeding
        if extension not in ['jpg', 'jpeg', 'gif', 'png']: sys.exit()

        # create medium image
        im.thumbnail((sizes['medium']['width'], sizes['medium']['height']), Image.ANTIALIAS)
        medname = filename + "_" + str(sizes['medium']['width']) + "x" + str(sizes['medium']['height']) + ".jpg"
        im.save(fullpath + '/' + medname)
        self.photo_medium = self.filepath + medname

        # create thumbnail
        im.thumbnail((sizes['thumbnail']['width'], sizes['thumbnail']['height']), Image.ANTIALIAS)
        thumbname = filename + "_" + str(sizes['thumbnail']['width']) + "x" + str(sizes['thumbnail']['height']) + ".jpg"
        im.save(fullpath + '/' + thumbname)
        self.photo_thumb = self.filepath + thumbname

        super(Photo, self).save()

    class Meta:
        ordering = ['-content_date']

andrkavr - 2010-01-13 22:26:57

Imagekit.

barbara - 2010-01-13 22:34:28

Ouch. For the record, I had no idea django-imagekit existed. Ha, I'm getting too used to rolling my own - I didn't even bother looking for another solution first.

Rich Leland - 2010-01-13 22:41:55

Always great to hear about how others are tackling this - I tried a similar approach with Imagekit, and you can use sorl-thumbnail in a similar way too. I think it works well for quick, smaller photos because you don't run into much of a delay from a user standpoint.

One thing I ran into with press.discovery.com was that we had such large source images and so many formats to resize for that we ended up creating a ghetto queue with SQL + cron scripts. The user uploads the largest image, the smallest thumbnail is generated, and several other formats are queued up to be resized. Our save method just adds the images to the queue. The script then gets executed every 10 mins from there to generate the rest.

Florent V. - 2010-01-14 00:07:02

I have used sorl-thumbnail, which is nice. One thing i liked was that it's rather feature-rich without being bloated (though your mileage may vary): you can set up your models to generate images on upload, or you can set up your templates to ask for specific thumbnails and have them generated when requested (not for each similar request, obviously).

It's always good to code things yourself if you can afford it. That way, you can review other solutions and understand them better. If your solution is better or more suited to the project's needs, you keep it; if someone else's solution is better, you can adopt it while still knowing how it works.

Brandon - 2010-01-14 01:48:52

I second Florent V on Sorl. It's very easy to install and use. I'm also curious as to why you went the string -> int route to stringify the current date time when you can do:

from datetime import datetime
now = datetime.now()

Cheers

Barbara - 2010-01-14 06:10:50

Actually, I have used sorl before - it didn't come to mind immediately because of the particular needs of this app. I'm creating full-fledged gallery sets with multiple image sizes on upload. In fact, I had originally intended to work with more sizes than just the three I'm showing here. :) I still contend that this save() is the simplest way to get to what I need.

And using time.time() was a design decision - I wanted my folder names using Unix timestamps. datetime.now() returns the current date in a completely different format.

Rasmus Toftdahl Olesen - 2010-01-14 08:54:15

You might consider using os.path's splitext and friends instead of rolling your own filename splitting functions.

And a little extension = extension.lower() might be in order.

I am curious about the call to sys.exit() if the extension doesn't suit you, is that a valid way to do it in Django?

Luke - 2010-01-14 20:49:21

Sorl Thumbnail comes with a really handy DjangoThumbnail class (http://code.google.com/p/sorl-thumbnail/source/browse/sorl/thumbnail/main.py#55), which I've used to do thumbnailing on-demand with sorl-thumbnail in some situations. It doesn't scale very well if you need to thumb 200 or 1000 photos on a page, but it's great for small cases:

@property
def medium(self):
return DjangoThumbnail(self.image, (200, 200))

Antonio Melé - 2010-01-17 12:10:20

I created django-thumbs. It's another simple option and it works with StorageBackends: http://djangothumbnails.com

Leon Matthews - 2010-01-20 19:42:06

If you do image resizing on upload you won't be able to change your mind about your resolutions later -- and your client *will* change their mind for you...

What we do instead is keep the original uploaded image somewhere safe, the produce a resized version on demand. Using a cache of resized images means you lose no speed, and you can change desired resolutions just by deleting the cache...

Henrique Carvalho Alves - 2010-01-21 00:55:47

Or cut all this mess and use ImageKit :)

You also get things like watermarks, cache, and different sizings for free. It IS great.

Neum - 2010-02-26 13:20:57

I somehow had never come across imagekit before, but I have used photologue in many projects for multiple image sizes, watermarking and image management. Works splendidly.

promozione del casinò in rete - 2010-05-10 05:53:52

What do I have to do with class.upload.php? do i have to include it at the beginning of the file-upload.php? It’s not working for me

louis vuitton - 2010-05-20 07:36:09

How to upload the solf ?