For a quick weekend adventure I decided to play around with using Python Imaging Library to take an arbitrary collection of photos and generate a photo gallery. With the help of the PIL documentation this turned out to be quite fun. (Full code available on Github.)

The pictures I'm editing are all my own, and come from my post about Kamioka, the town where I lived a couple years ago.

First, let's just load and resave an image without modifying it.

>>> import Image import Image >>> img = Image . open ( "kamioka.png" ) img = Image.open("kamioka.png") >>> img . save ( "kam2.png" ) img.save("kam2.png")

The saved image is identical to the original, which the kind reader likely hasn't yet seen.

I admit, not too exciting. Next, let's try creating a new image with two copies of that first image.

def mirror ( img , n = 2 ): x , y = img . size mirror_img = Image . new ( "RGB" , ( x * n , y ), "White" ) for i in range ( 0 , n ): mirror_img . paste ( img , ( i * x , 0 )) return mirror_img

Which we can then use to create an image with two copies.

>>> img = Image . open ( "kamioka.png" ) img = Image.open("kamioka.png") >>> mirror ( img ) . save ( "kam3.png" ) mirror(img).save("kam3.png")

We can also use it to make an image with a few more copies.

>>> img = Image . open ( "kamioka.png" ) img = Image.open("kamioka.png") >>> mirror ( img , n = 5 ) . save ( "kam3_2.png" ) mirror(img).save("kam3_2.png")

Here it is with five copies.

Now, let's try creating borders for the pictures to add a bit of sophistication. Let's start with a monochrome border.

def border ( img , width = 10 , color = "White" ): x , y = img . size bordered = Image . new ( "RGB" , ( x + ( 2 * width ), y + ( 2 * width )), color ) bordered . paste ( img , ( width , width )) return bordered

Here is a mirroring of a white bordered image.

>>> img = Image . open ( "kamioka.png" ) img = Image.open("kamioka.png") >>> mirror ( border ( img )) . save ( "kam4.png" ) mirror(border(img)).save("kam4.png")

I have to admit this is a pretty unimpressive border. The border function can be expanded to make it possible to customize borders a bit more. Instead of assuming a border has a width and a color, let's describe a border as a list of width/color tuples.

def border ( img , brdrs = [( 2 , "White" ), ( 8 , "Black" ), ( 1 , "Grey" )]): x , y = img . size width = sum ([ z [ 0 ] for z in borders ]) bordered = Image . new ( "RGB" , ( x + ( 2 * width ), y + ( 2 * width )), "Grey" ) offset = 0 print offset for b_width , b_color in brdrs : bordered . paste ( Image . new ( "RGB" , ( x + 2 * ( width - offset ), y + 2 * ( width - offset )), b_color ), ( offset , offset )) offset = offset + b_width bordered . paste ( img , ( offset , offset )) return bordered

Once again calling border ,

>>> mirror ( border ( img )) . save ( "kam5.png" ) mirror(border(img)).save("kam5.png")

which brings us:

Now that we've improved upon the border a bit, let's make a function which takes a list of images and displays them together neatly. To keep things simple, here are a few compromises we'll make:

we'll scale all images to have the same height,

we'll break pictures into chunks where the largest chunk is the size of the smallest original image,

we'll tile the pictures uniformly.

First we need to normalize all the height of all images such that they are all the height of the shortest image.

def normalize ( imgs ): "Normalize height for all images to shortest image." shortest = min ([ x . size [ 1 ] for x in imgs ]) resized = [] for img in imgs : height_ratio = float ( img . size [ 1 ]) / shortest new_width = img . size [ 0 ] * height_ratio img2 = img . resize (( new_width , shortest ), Image . ANTIALIAS ) resized . append ( img2 ) return resized

Next, we break wider images into chunks where each chunk is as wide as the skinniest image.

def chunk ( imgs ): "Break images into chunks equal to size of smallest image." smallest = min ([ x . size [ 0 ] for x in imgs ]) height = imgs [ 0 ] . size [ 1 ] chunked_imgs = [] for img in imgs : parts = math . ceil (( img . size [ 0 ] * 1.0 ) / smallest ) for i in xrange ( 0 , parts ): box = ( i * smallest , 0 , ( i + 1 ) * smallest , height ) img2 = img . crop ( box ) img2 . load () chunked_imgs . append ( img2 ) return chunked_imgs

Third we need to tile the images into the album page. (Note that merge assumes images have been normalized and chunked.)

def merge ( imgs , per_row = 4 ): "Format equally sized images into rows and columns." width = imgs [ 0 ] . size [ 0 ] height = imgs [ 0 ] . size [ 1 ] page_width = width * per_row page_height = height * math . ceil (( 1.0 * len ( imgs )) / 4 ) page = Image . new ( "RGB" , ( page_width , page_height ), "White" ) column = 0 row = 0 for img in imgs : if column != 0 and column % per_row == 0 : row = row + 1 column = 0 pos = ( width * column , height * row ) page . paste ( img , pos ) column = column + 1 return page

Finally, we wrap up these function calls into the album function which uses them together and also adds a border.

def album ( imgs ): imgs = normalize ( imgs ) imgs = chunk ( imgs ) imgs = [ border ( x ) for x in imgs ] return merge ( imgs )

Now, creating the first album.

>>> album ( imgs ) . save ( "album.png" ) album(imgs).save("album.png")

Here I used all the images from the Kamioka article, which happened to already be of the same sizes.

And here is the output applied against the original images plus the output of the first call to album .

So, that looks totally horrible, but with better choices of images it might actually look decent. I certainly enjoyed getting to work with PIL, and I hope some of the snippets from this meandering project serve as helpful examples.