Enhancing screenshots with PIL

When I wrote a post last week about using Flickr as a CDN, there was a paragraph that bothered me:

One thing did change when I started using Flickr to host screenshots: I had to change my screenshot format from PNG to JPEG. Flickr accepts PNGs, but it saves the resized images as JPEGs, and any transparent areas in the original PNG become black in the resized JPEGs. Since many of my screenshots are of windows with the semi-transparent shadow, the resized versions on Flickr look like shit. With JPEG screenshots, the window shadow backgrounds are white, which is just what I want.

While a white background to the window shadow is generally OK when I include a screenshot here, it isn’t always “just what I want.” For example, when I want to put a window screenshot in an update to a post, the white background of the screenshot looks stupid against the light green of the update <div>. And when I need to mix screenshots of multiple windows with those of single windows, there’s a clash of styles because my multiple window screenshots usually have a bit of my Desktop color (Solid Aqua Dark Blue) showing.1

Unfortunately, the screencapture command, which my snapflickr program calls, doesn’t have an option to layer in a background color. So I needed to do the layering myself. Since snapflickr is written in Python, the natural way to add the background is through the Python Imaging Library (PIL).

I was a little apprehensive. I’ve used PIL several times in the past and never had any real trouble installing it, but I hadn’t installed it under Lion, and in particular I hadn’t installed it under Lion after Xcode moved from /Developer to /Applications. Gabe Weatherhead (Macdrifter) has tweeted recently about the problems he had getting PIL installed, and I was not inclined to follow in his footsteps (he eventually got it installed via Macports, a system I never want to see again). The comments on this post suggest that Apple’s movement of Xcode, combined with the fact that it no longer installs the command line tools with Xcode by default, has turned the installation of PIL into an almost superhuman effort.

But I really wanted that background color in my screenshots, and I didn’t want to do it through AppleScripting Acorn if I could help it. So I started by installing the Command Line Tools through Xcode’s Preferences:

Xcode Download Preferences

Then I crossed my fingers and typed

sudo easy_install pil

It worked on the first try! Oh, there were compilation warnings aplenty, most of which had to do with casting 32-bit values as 64-bit (or something like that, I didn’t save them), but when I was done, I had a PIL that was usable. Apparently, PIL just likes me better than it likes Gabe.

The right library makes all the difference in the world. Adding a background to my screenshots was very easy with PIL. Here are the lines I had to add to snapflickr:

 95: # Add a desktop background if it's a shadowed window screenshot.
 96: snap = Image.open(fn)
 97: if snap.mode == 'RGBA':
 98:   # Crop it to a more reasonable size, 
 99:   # and paste it over a solid-colored background.
100:   cropbox = (margin[0],
101:              margin[1],
102:              snap.size[0]-margin[2],
103:              snap.size[1]-margin[3])
104:   snap = snap.crop(cropbox)
105:   bg = Image.new('RGB', snap.size, bgcolor)
106:   bg.paste(snap, None, snap)
107:   bg.save(fn)

We start on Line 96 by opening the screenshot, which has already been saved to the Desktop as a PNG file and has been given the name stored in the fn variable. Since I want to add the background color only to window screenshots, and window screenshots in PNG format have an alpha channel to handle the shadow transparency, the test on Line 97 will return true only for window screenshots. The remaining lines will be skipped for rectangular area screenshots.

I like the shadow Apple adds to windows, but when you take a screenshot with a shadow, there’s a lot of extra margin beyond the shadow itself. Lines 100-104 crop out the extra using a margin tuple defined earlier in the program as

margin = (25, 5, 25, 35)

These are how many pixels get cropped off the left, top, right, and bottom sides. I chose them after a bit of experimentation because I thought they gave a decent look to the result.

Line 105 creates a solid background image of the same size as the newly-cropped screenshot and filled with the color defined near the top of the program as

bgcolor = (61, 101, 156)

which are the decimal RGB values for Solid Aqua Dark Blue.

Line 106 pastes the screenshot on top of the background. The third argument to paste is the mask. According to the PIL documentation, calling paste with three arguments

updates only the regions indicated by the mask. You can use either “1”, “L” or “RGBA” images (in the latter case, the alpha band is used as mask).

So using snap as both the source and the mask is like using two layers in a program like Photoshop or Acorn. The screenshot is set in a layer in front of the background layer, which is visible through the transparent parts. The image is then flattened into a single layer.

The new image with the background color is then saved, overwriting the original screenshot. Because the bg image is in RGB mode (no alpha channel), so is the saved image. When the image is uploaded to Flickr, the original size will be a PNG and all the other sizes will be JPEGs, but there’ll be no funny black background areas in the resized images because Flickr won’t have to deal with transparency in the original.

It’s entirely possible that I’ll fiddle around some more with the margins, trying different crop amounts to see if I like something better. But I think the basic structure of the code is set.

  1. Actually, what’s usually showing in a multi-window screenshot is Backdrop. Although I like to keep a tidy Desktop, there are usually at least a few icons on it. Backdrop is an app that covers up the Desktop—and any app windows it’s stacked in front of—with a nice solid color. Great for screenshots and screencasts.