soledad penadés
repeat 4[fd 100 rt 90]

Superminigallery: a gallery with ruby, rmagick and builder

Imagine you have been in a very nice place for holidays. You took a lot of pictures and want to show them to your family and friends, but you don't feel like using services like flickr or programs like iPhoto. You just want to put them in your own server and give the url to your friends.

What can you do? Well, you could do like me and create a little script to generate an HTML file, with thumbnails and even watermarked images (just in case some creepy individual decides to use your stuff without asking first).

Superminigallery thumbnail

Requirements

This script requires a couple of gems to be installed: RMagick and builder (but if you've done some stuff with Rails you might already have them). RMagick is used for dealing with the images and builder is used for generating the XHTML. This is because I didn't want to write any html by hand, with their less than and greater than signs, attributes, etc.

Using it

  1. Create a folder in your computer. For example: holidays.
  2. Then you copy there the pictures you want to show to the world.
  3. Open a terminal and cd to that directory. E.g.
    cd ~/Desktop/holidays
  4. Execute the script! E.g.
    ruby ~/code/superminigallery.rb
  5. Wait until it finishes

When it finishes you'll find there's an output folder in the holidays folder. That's where the index.html file, as well as all the thumbnails and watermarked images are. Simply upload the contents of this folder to your host and let everybody know about it!

Ok, but show me the code

The first lines act like a configuration area. You can change the output folder name, so that it is called superoutput, gallery, whatever you like (as long as it is a valid path name).

You may change the sizes of the generated pictures; these sizes are defined in the versions variable. Each pair means [width, height]. For example, the thumbnails are 300 pixels wide and 150 pixels high.

output_path = 'output'

versions = {
  'thumbnail' =>  [300,150],
  'big'       =>  [1024,768]
}

You can also configure which EXIF tags need to be retrieved. Since their names are too obscure for non-technical savvy people I decided to create this hash for storing the key (Exif tag) and the nice name to show with the value. So instead of showing DateTimeOriginal, it will simply output Taken.

exif_fields = {
  'Taken'     =>  'DateTimeOriginal',
  'Camera'    =>  'Model',
  'Exposure'  =>  'ExposureTime', 
  'Shutter Speed' =>  'ShutterSpeedValue'
}

There are way more tags you could show, but they can be confusing for normal people and only entertain geeks, so it's better to keep them down to a minimum.

Declare the builder object, and initialize it with the XHTML header.

x = Builder::XmlMarkup.new(:target=>xhtml, :indent=>1)

x.instruct!
x.declare! :DOCTYPE, :html, :PUBLIC, "-//W3C//DTD XHTML 1.0 Strict//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
x.html( "xmlns" => "http://www.w3.org/1999/xhtml" ) {

Now it would be amazing to have some styling in the page so that it doesn't look so ugly. We can put an style tag inside the head, and use the text! method for adding literal text to the builder object:

x.style( "type"=>"text/css" ) { x.text! "
      body{
        font-family:georgia,serif
      }
     
      h1,h2 {
        margin-top: 0;
      }
      …
      "
}

(the means there's more code but I have reduced it for clarity purposes)

Now, we need to create the output directory. I haven't bothered with outputting error messages if the directory already exists or anything. It will always try to create it:

begin
    FileUtils.mkdir output_path
  rescue
  end

We need to create a Magick::Draw object for watermarking the images, and define its parameters:

draw = Magick::Draw.new
draw.gravity = Magick::CenterGravity
draw.pointsize = 64
draw.font_family = "Helvetica"
draw.font_weight = Magick::BoldWeight
draw.stroke = 'none'
draw.fill = "#ffffff99"

Basically we are saying: use Helvetica bold 64pt, painting it with white (ffffff) and some transparency (99 for alpha channel). If you don't have Helvetica installed in your system, replace it with your favourite font.

(But since 2007 is Helvetica's 50th anniversary, you should do everything possible to use Helvetica!)

Now we open the current directory (where the script was executed) and find all files with jpg and JPG extensions, and sort them. That's because sometimes the images don't get listed in alphabetical order, and us humans like to see things in sequential order. Specially because they usually are numbered incrementally, and older numbers mean older images, so IMG001 should appear before IMG100.

Dir['*.jpg','*.JPG'].sort.each do |f|

Read each file into a Magick::Image object:

img = Magick::Image.read(f).first

And for each version…

versions.each do |k,v|

… create the version filename by appending the version name to the filename, like big_IMG_1234.jpg, and the output filename, by prepending the output path to the version filename:

version_file =  k + '_' + f
output_img_path = File.join(output_path, version_file)

If the version is 'thumbnail', we'll add the image metadata to the builder object. Note how you don't need to open or close tags, but just include things in blocks or parenthesis to get the mark up done.

if(k=='thumbnail')
        x.div('class'=>'picture') {
                x.h2(f)
                x.a('href'=> version_file.sub('thumbnail_', 'big_')) {
                        x.img('src'=>version_file)
                }
                x.dl {
                        x.dt('Dimensions')
                        x.dd(img.columns.to_s + ' x ' + img.rows.to_s)

                        exif_fields.each do |title, field|
                                key = "Exif:#{field}"
                                if img[key]!=nil
                                        x.dt(title)
                                        x.dd(img[key])
                                end
                        end
                }
        }
end

Resizing the image is as simple as

version = img.crop_resized(v[0], v[1])

crop_resized returns another Image object which we store in the version variable.

Now, if we are dealing with the 'big' version, we'll add the watermark that we prepared at the beginning. That is done with

if(k=='big')
        draw.annotate(version, 0, 0, 0, 0, "(c) soledadpenades.com")
end

You can replace my (c) soledadpenades.com with your text, of course!

And for writing the resulting image to disk:

version.write output_img_path

Very very important: do not forget to call the Garbage Collector. For some reason which I still haven't been able to elucidate, the RMagick gem leaks memory furiously. So if you forget to do a

GC.start

as I did with the first version of the script, your computer will mostly hung if you make it generate a lot of thumbnails. If you look at the current processes with top or a similar tool, you'll find a ruby process eating more and more memory with each picture it processes.

And finally, we just need to output the generated XHTML to index.html:

File.open(File.join(output_path, 'index.html'), 'w+') do |file|
  file.puts xhtml
end

Here's the result and here's the source code. With only 120 lines of code (excluding the license text :D), it's way easy to modify to suit your tastes.

Don't tell anyone but…

I must confess I got the inspiration for this from herotyc's jGallery. But he used a bash script and I thought there should be a way of doing the same with ruby :-)

// 12 responses to Superminigallery: a gallery with ruby, rmagick and builder

herotyc
herotyc
20070627

Hey, great stuff! Good job.
I'll take away your watermark idea and put into my jGallery, hehe. I did my script for the same way you did yours(learning of course), I was looking for a very easy way to have photos in my webpage. I tried some php galleries like Coppermine, but they were so slow, and uploading en entire gallery was painful.
Just two questions:
How do you deal with rotated photos? Photos taken with the camera rotated 90º
Were you forced to use the MIT licence? If not, why did you choose it?
The photos are beautiful, 'Menorca' I guess.
Cheers!

sole
sole
20070627

Yep, adding pictures to those galleries, one by one, is horribly slow!

About the questions:
1) My camera has already rotated the pictures. Since I'm using crop_resized, RMagick will take care of adjusting the width or height as necessary before cropping. I think some older versions of RMagick do not have that method, so you have to manually add a piece of code with something for detecting if you need to resize vertically or horizontally.
2) I chose the MIT because
- it's short and easy to read
- you can learn with the code and use it if you wish
- if you use it in more products, more people can learn from your products because it "propagates".

And yes the pics are from Menorca :-) ("Minorca" as they say here hehe)

David Bock
David Bock
20070628

I didn't know RMagick could read EXIF info so easily! Nice! Thanks for that snippet.

sole
sole
20070628

You're welcome! Hope it's useful…

walter
walter
20071101

Thank you for a nice script. I'm trying it out but it keeps duplicating the same image twice on one row, then moving on to the next image. The output shows each image being processed four times instead of twice. Any thoughts?

I'm also looking up RMagick on how to move the watermark around.

Cheers

sole
sole
20071101

Hi Walter!

I don't really know why could it fail. What system are you using?

I am thinking that maybe in Windows the *.jpg and *.JPG files could return the same results twice but I haven't tested it in Windows.

Other than that once it enters the loop it just tries to get the first image of each file (it seems that JPG files can store more than one image). So I can't think of any other error source…

Have you tried changing the gravity parameter for moving the text to a different place?

Let me know if the windows thing works :-)

walter
walter
20071102

Yes after fiddling around with it for a while it was indeed the Dir['*JPG','*.jpg'] line causing the error in Windows 2000 but you beat me to it ;)

So then just a note here for any other newb. What was happening was that the Dir was reading the jpg file then doing it again for JPG for the same file name.

in the script rewritting the line to just one Dir['*.jpg'].sort.each do |f| did the job.

Thanks for the gravity parameter note! I'll take a look at that.

Cheers
Walter

walter
walter
20071102

i'm going over rmagick now, what's happening is that the thumbnail is okay but the larger image get's cropped and expanded to the version hash parameter. Thus working correctly but I'll need another method. Makes sense for the thumbnail, but any way to leave the big image as-is rather than specifying a size in version hash?

thanks
walter

walter
walter
20071102

If it helps, I should point out that my 'big' images are already a uniform 600×400, but even when I specify 1024×768 in the version hash, the image still gets cropped. Oddly the thumbnail is more representative of the total image.

I'll try moving around the line version = img.crop_resized(v[0], v[1]) and I'll let you know if it works

walter

sole
sole
20071102

Maybe that's happening because of the aspect ratio which is different. 1024×768 is roughly 4:3 while 600×400 is 3:2. Maybe that's why rmagick is trying to adjust things and being too intelligent.

Take a look at the crop_resized docs and see if you can play a bit with Gravity again.

Other solution is to work out your own method, combining resize and crop. I did it once for a host which had an old rmagick gem installation and hadn't the crop_resized method in it. It's not too complicated, you just need to know very well what you want to do.

Good luck :-)

walter
walter
20071102

So…I went over the Gravity and image sizes again. It wasn't the aspect ratio but that was the key to fixing it. After playing around with it, I started over, going over each variable setting. It turns out for the big images to be viewed properly I had to set it to 400×600 not 600×400. Yeesh.

Yep, I fried a good brain cell, but I learned a lot. Ah well, life as a nuby. Thanks for your help! Great script.

Cheers
Walter

sole
sole
20071102

Glad to hear you finally found your way around it :-)

I guess frying cells is the only way for learning for real. Once you do that you never forget what made you fry the cell.

Thanks for using it too!

Feel free to leave a reply

Comments are moderated: Rude and offtopic ones are out!