My, it’s been a while since I wrote a Rails tutorial. I’m going to come back from the land of esoteric JavaScript techniques for a minute and talk about something you might actually want to use in real life.
Many people, when writing an app that uses image uploads in some way, like to generate thumbnails for said images. They put together some list of sizes they need to generate and make sure the app creates these sizes when users upload files. Fine. But what if you want to change your site design and need some new sizes? You need to regenerate thumbnails for all those millions of images your users sent you.
To get around this problem, you can use on-demand thumbnail generation. This means that the thumbnails are generated when an HTTP request comes in for them, rather than when the original files are uploaded. If we put the required dimensions in the request URL, we can get Rails to generate thumbnails at any size whenever we want.
First, you’re going to need to decide on a URL scheme for your thumbnails. I’m going to keep it simple here and serve all my site images with URLS that look like so:
/images/thumbs/34/600x450.jpg
That points to image id 34, at 600px wide by 450px tall. Simple. You might want to set up a more fragmented directory structure if you’re going to be generating lots of images, something like
/images/thumbs/037/825/004/600x450.jpg
which would load image #37825004. We’re going to need a route in
config/routes.rb
to handle our simple URL, so we add the following:
map.thumbnail "/images/thumbs/:id/:dims.jpg",
:controller => "images", :action => "serve_image"
The idea is that we generate the thumbnail and save it at the requested URL, so that future requests for the image will be handled by the webserver without invoking Rails.
So we’ve got a public interface for requesting images, now we need to set up the
model to deal with the thumbnail process. We need a method that returns the URL
for an image thumbnail, and a method for generating the thumbnail. It helps if
your model includes an aspect_ratio
field, which is generated from the
original image on upload. Our model also has a full_path
field, which says
where the original image is stored in the filesystem.
class UploadedImage < ActiveRecord::Base
# You can leave +height+ blank if you like.
def thumb_path(w, h = nil)
h ||= width / aspect_ratio
"/images/thumbs/#{id}/#{w.to_i}x#{h.to_i}.jpg"
end
# Where to store images in the filesystem when they
# are created.
def image_path(w, h = nil)
"#{RAILS_ROOT}/public#{thumb_path(w, h)}"
end
# Generate thumbnail from the original image
def thumbnail!(w, h)
ImageTools.thumbnail(full_path, image_path(w,h), w.to_i, h.to_i)
end
end
For generating the thumbnails, we call out to a module that handles all our
image-related needs, stored in lib/image_tools.rb
. There are faster ways of
generating thumbnails but I’ve had trouble with some of RMagick’s features on
some servers. Do check out its documentation for more info.
require 'RMagick'
require 'fileutils'
module ImageTools
class < self
include Magick
def thumbnail(source, target, width, height = nil)
return nil unless File.file?(source)
height ||= width
img = Image.read(source).first
rows, cols = img.rows, img.columns
source_aspect = cols.to_f / rows
target_aspect = width.to_f / height
thumbnail_wider = target_aspect > source_aspect
factor = thumbnail_wider ? width.to_f / cols : height.to_f / rows
img.thumbnail!(factor)
img.crop!(CenterGravity, width, height)
FileUtils.mkdir_p(File.dirname(target))
img.write(target) { self.quality = 75 }
end
end
end
So now we’ve got the model sorted out so it can provide URLs for thumbnails and
generate the images, and we have a route set up for requesting images, but we
need the bit in the middle: the controller. Remember, if our controller action
is called, it means the image does not yet exist in the public
directory and
must be generated and saved there.
class ImagesController < ActionController::Base
def serve_image
@image = UploadedImage.find(params[:id])
width, height = params[:dims].scan(/\d+/).map(&:to_i)
@image.thumbnail!(width, height)
redirect_to(@image.thumb_path(width, height))
end
end
We just generate the thumbnail and then redirect to the thumbnail’s URL. This
second request will be handled by Apache as the image file will now be sitting
in the public
directory. Now, all you have to do is put
<%= image_tag @image.thumb_path(600,450) %>
in your views and all your images will be generated as and when they are needed.