Flickr Photo Collage with Ruby

Yesterday I played a bit with Ruby, RMagick and FlickrAPI. I have to admit that I'm very impressed by the ImageMagick library and the RMagick Ruby bindings seem to work very smooth. It would be interesting to see this combination in a project, maybe with some image caching mechanism in order to reduce the overload of image processing.

Initially I wanted to make a little ruby script that fetches some images from Flickr and creates a bigger image, with the Flickr images arranged in order. But as I was browsing the RMagick documentation, I discovered `ImageList#montage` function which seemed to do exactly what I wanted. Here's the result, with some comments in place. This script can be extended in order to provide some better looking results - for example it would be interesting to see the names of the image posters, or also some better image effects (shadows, slight rotations in order for the images to appear more natural - eg. polaroid effect, etc.)

For accessing the FlickrAPI, I used the flickraw library - one of the best FlickrAPI Ruby binding in my opinion. I tried other libraries in the past, but they were either obsolete, or just too buggy. Flickraw provides a low level access to the FlickrAPI, but with a Ruby style syntax. The library is just a wrapper for the FlickrAPI methods - which means that even if the API will change, the library will most likely continue to work. Many claps to Flickraw's author for choosing this great design.

#!/usr/bin/ruby
require 'flickraw'
require 'open-uri'
require 'RMagick'
 
#
# Obtain the the photos from Flickr.
#
def get_photos( username, count = 9 )
  begin
    user = flickr.people.findByUsername( :username => username )
    photo_list = flickr.photos.getContactsPublicPhotos( :user_id => user.nsid, :count => 9 )
  rescue Exception => ex
    puts ex
    return []
  end
 
  return photo_list.slice(0, 9)
end
 
#
# Construct the URL of the photo, given a photo object as returned by flickraw.
#
def photo_url ( photo )
  return "http://farm#{photo.farm}.static.flickr.com/#{photo.server}/#{photo.id}_#{photo.secret}.jpg"
end
 
#
# Download the specified photos. It will use a caching mechanism in order to save 
# bandwidth.
#
def download_photos( photo_list )
  files = []
 
  Dir.mkdir 'images' unless File.exists?( 'images' )
 
  photo_list.each do |p|
    url = photo_url p 
 
    fileName = 'images/' + File.basename( url )
 
    if !File.exists?( fileName )
      puts url.to_s + " => " + fileName
      open url do |remote|
        open(fileName, 'wb') { |local| local << remote.read }
      end
      else
      puts "Skipping #{url} because it already exists."
    end
    files << fileName
  end
  return files
end
 
#
# Create the collage, using Magick::ImageList#montage
# method.
#
def create_image( args = { } )
  # Set some default values
  props = { 
    :width => 1024,
    :height => 768,
    :files => nil,
    :username => 'unknown-user',
    :output => '',
    :tile_h => 3,
    :tile_v => 3
  }
 
  args.each { |k,v|
    k = k.to_sym
    raise ArgumentError, "unknown property #{k}" unless props.key?(k)
    props[k] = v
  }
 
  props[:output] = props[:username] + '.jpg' unless props[:output].length > 0
 
  # Construct the image list
  image_list = Magick::ImageList.new
  props[:files].each do |img|
    image = Magick::Image.read( img ).first
 
    # Add a nice, white border around each image
    image.border!(30, 30, "#ffffff")
    image.background_color = "none"
    image_list << image
  end
 
  # Compute the placement of the images. Not a very interesting part...
  padding, image_dim = Hash.new, Hash.new
  padding[:x], padding[:y] = props[:width] * 0.05, props[:height] * 0.05
  image_dim[:width], image_dim[:height] = (props[:width] - 6*padding[:x]) / props[:tile_h], (props[:height] - 6*padding[:y]) / props[:tile_v]
 
  # Construct the montage.
  image_list = image_list.montage do
    self.tile = "#{props[:tile_h]}x#{props[:tile_v]}"
    self.background_color = '#000000'
    self.geometry = "#{image_dim[:width]}x#{image_dim[:height]}+#{padding[:x]}+#{padding[:y]}"
    self.shadow = true
  end
 
  # Flatten everything and write it to a file.
  image = image_list.flatten_images
  image.resize!( props[:width], props[:height] )
  image.write( props[:output] )
end
 
# Check the number of parameters
if ARGV.length <= 0
  puts "Not enough arguments."
  exit
end
 
# Main body
username = ARGV.first
photos = get_photos( username )
image_files = download_photos( photos )
create_image( :files => image_files, :username => username, :width => 400, :height => 300 )



Hello Cristi,
I was reading you used flickraw and i am trying to use it too but I am having errors.
May you help me trying to understand what I am doing wrong?
Thank you in advance.
Best,
Jorge

Hi Jorge,

Can you provide more details about those errors? I've just tried to the flickraw part from the posted script and it appears to be still working (albeit it's a bit old). Not sure about the RMagick part, since I didn't have enough time to install it.

Be sure to install the flickraw gem first:

gem install flickraw

Let me know if you're still having troubles.

Regards,
Cristi