RESIZING TRANSPARENT IMAGES WITH DJANGO + PIL

I can’t seem to get sorl thumbnail to play nice with transparent PNGs, it keeps adding weird black and grey backgrounds because it’s doesn’t maintain the alpha channel in it’s output.


Python’s PIL Module uses different versions of the Image.save() method depending on the format of the image being saved. sorl thumbnail uses the following call when it creates the final image output:

im.save(self.dest, quality=self.quality)

This call utilizes a parameter call quality which, according to the documentation is only available on JPEGs. Even changing the THUMBNAIL_EXTENSION in my settings.py file to “png” still didn’t make it maintain the alpha channel the way I had expected it to. Bug 56 is currently open for this issue at the sorl thumbnail project home.

The solution

I wanted to setup a way to pull the images off of the transparency resize them and then drop them on an opaque background of it’s own independent size.

So the first thing I did was to load in an image via PIL:


im = Image.open(filename)

So now that we’ve got our image we need to create the opaque background for it… right now we’ll just use a solid black one. Use the following PIL method call to create a background at an identical size to the original image:


bg = Image.new('RGBA', im.size, (0, 0, 0))

At this point I’ve got two images, im the logo file and bg the opaque black background. Using the PIL paste method I can stick there two together.


bg.paste(im)

which gives us the following image:

If you’re like me, you’re like – “WHAT THE HELL?!?! I just pasted a transparent PNG on a black background, why is it all white???” The answer is because you didn’t tell Python to maintain the alpha channel when it pasted.

The way that PIL deals with transparency when saving or pasting PNGs (that have a mode of “RGBA” or “P”) is to allow for a “transparency” parameter which is an integer from 0 – 255 which is sort of like a transparency tolerance for that image, and represents the “opaqueness” of the pixels to maintain transparency for. Since this is different for every image what we need to do is just maintain the value of the PNG we’re working with.

Note: that if you paste an “RGBA” image (WE ARE!), the alpha band is ignored in a paste. You can work around this by using the same image as both source image and mask – like we have below. More information at the PIL Handbook

Change the paste call to look like this:

bg.paste(im, (0, 0), im)

which produces this:

This call has two additional parameters. The first is is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as (0, 0)) for image we’re pasting in – for this we’re pasting it in at (0,0) – and the second is transparency value – which we’re just telling them use the the value from im.

Although close this still isn’t quite right. There are a bunch of extra transparent pixels which create the strips of blank vertical space on the top and bottom of the image… lets get rid of them.


Add the following code right below the first image.open call like this:

im = Image.open(filename)
 
#convert to black and white
invert = ImageChops.invert(im)
bw = im.convert("1")
bw = bw.filter(ImageFilter.MedianFilter)
 
# add a white bg and do a difference
diffbg = Image.new("1", im.size, 255)
diff = ImageChops.difference(bw, diffbg)
 
#use the difference to create a bounding box
bbox = diff.getbbox()
 
#crop to that bounding box
if bbox:
      im = im.crop(bbox)

and we get just the non-transparent parts of the original:

The next step is to be able to dynamically control the color of the opaque background. In order to do this we need to accept a new parameter which should be a hex color value encased in quotes. Change the line where you create the bg image to look like this (where bgcolor is the quoted hex color string):

bg = Image.new('RGBA', im.size, ImageColor.getcolor(bgcolor[1:-1], 'RGB'))

So if we passed the string “#FF0000″ as bgcolor we get this:

Which is rad. Now we want to pass in two different size values. The first one is a maximum width / maximum height tuple for the logo. Thsi value will be used to re-size the logo according to it’s aspect ratio to one of these maximum values. The second value is also a tuple and is the absolute width and height of the background image. Change your function to accept these 2 new parameters – I’m writing a custom Django Template tag which I will share at the end.


Now change the line where you create the bg image, with these two lines of code where logo_width and logo_height are the integer values of your logo size tuple, and the same for bg_width and bg_height:

im.thumbnail([logo_width, logo_height], Image.ANTIALIAS)
bg = Image.new('RGBA', (bg_width,bg_height,), ImageColor.getcolor(bgcolor[1:-1], 'RGB'))

So now we’ve got independent sizes, lets center the logo in the background. Replace the paste call from before with this code

image_width, image_height = im.size
xpos = (bg_width/2) - (image_width/2)
ypos = (bg_height/2) - (image_height/2)
 
bg.paste(im, (xpos, ypos), im)

so if we call the function like with the following parameters:

background-color = “#FF0000″
logo_size = [200x200]
bg_size = [200x200]

produces this image:

and calling it with these parameters:

background-color = “#FF0000″
logo_size = [200x200]
bg_size = [400x300]

produces this image:

and finally(!) calling the function with these parameters:

background-color = “#000000″
logo_size = [100x100]
bg_size = [200x200]

produces this image:

  1. Face of the Cookie || Dane 2.0:

    [...] friend Veronique made this movie using Dane’s latest blog post about whatever it is he does while he’s typity typing away on the internets. It is pure [...]

  2. Reid:

    One more key bit is that if you are pasting together several images, you need to make sure that the image you’re adding is RGBA and not just RGB or PIL will complain that there’s a bad mask or something. I have a loop in my program that passes the image or None depending on whether the image is RGBA or not.

  3. Dane:

    Nice tip!

    Thanks Reid! I’ve only messes around with pasting 2-3 images and they were all RGBA (by coincidence) – so this is good to know!

  4. Andre LeBlanc:

    I’m current using sorl.thumbnail for a clients website and i have it generating perfect png thumbnails with great transparency on one server, but the exact some codebase on a different machine will not generate a good thumbnail, its either got a totally white background, or wierd artifacts where there should be transparency.

    This pretty much confirms that given the right circumstances, sorl and PIL can do proper transparency, the only difference I can see is that the broken thumbnails come from ubuntu 8.10, and the good ones come from 8.04

  5. Andre LeBlanc:

    I’ve submitted a patch for http://code.google.com/p/sorl-thumbnail/issues/detail?id=56 that works for me everywhere now.

  6. Dane:

    Awesome, thanks Andre!

  7. Marissa Tellinghuisen:

    When one deliberates the issue at hand, i have to agree with your endings. You understandably show knowledge about this topic and i have much to discover after reading your article.Plenty salutations and i will come back for any further updates.

  8. Burton Haynes:

    Thanks for your article. I am new at development and this got me straight.

  9. viagra:

    This is very useful pinfo. This will absolutely going to help me in my projects, that I’m working on. Can I take a fragment of your post to my site?

  10. Denver Franchette:

    That was one explicit from most posts I’ve noticed in a chronic prolonged time. A complete good deal appreciated, I’m doubtless have to hang around here far extra.

Leave a Reply