Thursday 5 November 2009

Django: Resize Image on Upload to ImageField

Over the last couple of weeks I've been really interested in using django to develop new websites.  Django is the first web framework I've ever tried and to be honest I've been pleasantly surprised at how good it is.  The system makes development so fast and easy I don't understand why everyone isn't using it!  Well of course there are a few issues - hosting for one is more difficult to find (I've found) and must have some requirements which aren't always obviously listed on the providers website.  But the reason I've come to love django is that it uses python which I've found incredibly easy to get stuck into, much easier than other web-based languages.  I would compare django to Delphi (in my earlier days I played with the Delphi IDE), it allows rapid development of rich applications. 

The structure of django is extremely flexible, allowing a great deal of customisation whilst still holding a basic framework structure.  The initial problem I came across as an absolute django newbie was that the model framework didn't resize images (ImageFields) on upload. I wanted the images to be resized when uploaded, if they were too large, to the maximum allowed dimensions whilst keeping them in proportion. This seemed rather daunting to begin with as I didn't quite understand the way django is built and I never managed to find one article detailing a simple solution to my problem.  Therefore the answer was I should probably write one (if only to remind myself in future).

Prerequisite: this method requires you to have PIL (Python Image Library) installed.

Firstly a few things I found over at the django website (here), is that "save()" is called to create or edit a model and "delete()" is called when a model is deleted (pretty straight forward eh).  This is the gateway to really customising django models.  The save method is the key method to override (obviously) as this is called to save the object, which we want to resize.  So first we have to override the save method... here's my example:

from PIL import Image as img
import math

...

class TestModel (models.Model):
     image = models.ImageField(upload_to='images')
     def save(self, force_insert=False, force_update=False):
          super(TestModel, self).save(force_insert, force_update)

Initially we import Image and math, these are the required libraries to do the resizing and the maths for the calculations.  I import it as "img" so I don't get confused between my field "image" and the python library.  The class shown above is the basic model (TestModel) with an image field and the save method over-written.  In the new save method we've got the call to the original save method - it's important we still actually save the model. There may be times when saving the model is not required in which case don't call the super-method.  This is my base template for overriding the predefined model methods.

Next, we need to get the uploaded images width and height (in order to decide which dimension is larger and if either are larger than the maximum allowed...).  We also want to set our maximum sizes (I've hard-coded mine in but it's just as easy to load them from elsewhere).

          pw = self.image.width
          ph = self.image.height
          mw = 800
          mh = 600

Where the present width, present height, maximum width and maximum height are pw, ph, mw and mh respectively.  All the sizes are in pixels and are integer values (this will be important shortly when dealing with floats).

Next we should decide if we're going to resize the file or not, if one of the dimensions is bigger than our maximum:

          if (pw > mw) or (ph > mh):
               # we require a resize
               # load the image
               filename = str(self.image.path)
               imageObj = img.open(filename)
               ratio = 1

As we can if the size of the image is too large, we'll open the image and prepare to resize it.  We use img.open() (or Image.open() depending on how PIL was imported) to open the file and then imageObj will be our image object from now on.  We also define the default ratio  as 1 - note a ratio of 1 will not increase or decrease the image size.  Now we need to find which dimension is too large and create a ratio of original size to new size, I'll explain more about this after showing the code:

               if (pw > mw):
                    ratio = mw / float(pw)
                    pw = mw
                    ph = int(math.floor(float(ph) * ratio))

Here we see that if the width is too large we calculate the correct ratio and then set the new present width and height.  We over-write the old values now in case (in one specific case) the height is still too large even after reducing the width to our maximum.  In this case we will further reduce the size until the image is our largest dimension whilst still in proportion.  The height is then dealt with in the same way as the width (with possibility that the default ratio has already been changed during the first calculation).

               if (ph > mh):
                   ratio = ratio * (mh / float(ph)
                   ph = mh
                   pw = int(math.floor(float(ph) * ratio))

This does the same as the calculation of ratio for the width but it multiplies the previous ratio calculated for the width (if there is one - otherwise it's set to 1) by the ratio for the height.  The calculation makes sure that the dimensions are kept in proportion and that the image does not exceed the maximum dimensions.

Finally we need to resize the image and overwrite the original file.

               imageObj = imageObj.resize((pw, ph), img.ANTIALIAS)
               imageObj.save(filename)

This is the coding done.  Here is the full example:

from PIL import Image as img
import math

...

class TestModel (models.Model):
     image = models.ImageField(upload_to='images')
     def save(self, force_insert=False, force_update=False):
          super(TestModel, self).save(force_insert, force_update)

          pw = self.image.width
          ph = self.image.height
          mw = 800
          mh = 600

          if (pw > mw) or (ph > mh):
               # we require a resize
               # load the image
               filename = str(self.image.path)
               imageObj = img.open(filename)
               ratio = 1

               if (pw > mw):
                    ratio = mw / float(pw)
                    pw = mw
                    ph = int(math.floor(float(ph) * ratio))

               if (ph > mh):
                   ratio = ratio * (mh / float(ph)
                   ph = mh
                   pw = int(math.floor(float(ph) * ratio))

               imageObj = imageObj.resize((pw, ph), img.ANTIALIAS)
               imageObj.save(filename)

Wednesday 4 November 2009

First Blog: Setting the Scene

This will be the first ever online blog, so, where to start? Perhaps a little introduction...
My names Will, I'm studying Artificial Intelligence at Edinburgh University. I enjoy going out with the lads and having a few drinks as much as I enjoy staying in and working on personal projects (usually computer based).

Initially I was optimistic about creating a blog for myself, regardless of previous failures, but after a little researching (avec google), it seems a more daunting challenge.  I should probably point out at the start that my writing skills leave a lot to be desired; I've never really managed to express myself properly in writing before and these will be my attempts to improve.

Whilst reading up on google about the basics of the content of ones first blog, I came across an interesting take on the categories of blogs (here).  In this the author claims a blog can be split into 6 possible categories, in summary:
  1. A log
  2. A commentary
  3. A collection of thoughts
  4. A forum for writing
  5. To build a community
  6. Corporate blogging
These inspired me to consider where I want to position my blog.  Should it be a log of my activities (something to remind me, and possibly share with any readers, the more interesting events of my life), a collection of thoughts or a forum for writing...

What I've taken away from the reading I've done is that a blog should be whatever you want it to be.  I intend mine to be a mixture of all 3 of the categories above; I hope to include events which interest me, thoughts and any formal writing I do.