On-the-fly image handling with Dragonfly

This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.


There are some fantastic gems and plugins out there for handling images in Rails apps, such as Paperclip, Attachment-fu, etc., that on the whole are well-written and do a pretty good job.

However, something has always narked me when using them.

In general, to add a preview_image to, say, your Album model, you do something like this:

class Album
  some_macro_method :preview_image,
    :thumbnails => {
      :listing => "300x200",
      :thumb => "100x100!",
      :small => "50x50#"
    }
end

Then in the view that shows the image, you can use something like this:

<%= album.preview_image.url(:listing) %>
...
<%= album.preview_image.url(:thumb) %>
...etc.

This is all very well, provided you know the exact sizes of the thumbnails that you will need at the very beginning.

Unfortunately, more often than not, this is not the case, and we find ourselves running some ‘reprocess’ task every time a new thumbnail size is added, or one is changed.

Furthermore, we might not always want to use all of those thumbnails. If we use these models in a script which makes no use of the images, or in our tests, then having to process thumbnails every time we create a model is expensive and unnecessary!

Taking these things into account, it seems clear to me that the fundamental problem is that the thumbnail specifications are in the wrong place!

Images, and what size they should be, are visual concerns, so surely the correct place for specifying thumbnails is in the view.

The Solution

What we really want is to have our model looking like this:

class Album
  some_macro_method :preview_image
end

and our view looking like this:

<%= album.preview_image.url("300x200") %>
...
<%= album.preview_image.url("100x100!") %>
...etc.

The way to achieve this will be to generate thumbnails on-the-fly - this will both remove the need to reprocess thumbnails when there are changes, and minimize the creation of unused thumbnails.

NB If you’re concerned about performance at this point, this is easy to optimise using HTTP caching - we’ll mention that later - what’s important for now is that the thumbnails are specified in the correct place.

Dragonfly

Although I’d been thinking about this problem for a long time, it’s taken a while to get round to actually creating a ruby gem for doing it!

So at last, I’m very pleased to introduce Dragonfly, a Rack-based ruby gem for processing/encoding on the fly.

There are already one or two gems which deal with on-the-fly processing (see below), and while these are good for their specific tasks, I wanted one which was generic enough as to allow the use of different processing libraries, different data stores, etc.

From the start it has been designed to be as modular and extendable as possible, making it highly configurable, but falling back to sensible defaults for those who just want to do easy thumbnailing without delving into configuration details.

In particular, it was designed with the following in mind:

  • useable outside of Rails as a Rack application
  • useable without ActiveRecord
  • useable with any media type (not just images)
  • useable with any kind of data store (filesystem, S3, SQL, CouchDB, etc.)
  • ability to create custom processors (more than just resize, rotate, etc.)
  • ability to create custom encoders (i.e. any format - png, jpg, svg, txt, etc.)
  • should make good use of HTTP caching for performance
  • ability to have multiple dragonfly apps in the same process, each with their own separate configuration

Given the above points regarding not being tied to Rails/ActiveRecord, it’s been designed primarily as a Rack application, but with an optional extension module for using with ActiveRecord.

Using with Rails is simply a matter of using it as a Rails Middleware, and configuring appropriately (which is done for you if you use the supplied generator or helper Rails configuration file).

Basic usage as a Rack application

Basic usage of a dragonfly app involves storing data (e.g. images), then serving that data, either in its original form, processed, encoded or both.

We set up the dragonfly app and run it (e.g. in our rackup file config.ru):

require 'rubygems'
require 'dragonfly'

app = Dragonfly::App[:my_app_name]

app.configure_with(Dragonfly::RMagickConfiguration)
app.configure do |c|
  # any other configuration here
end

run app

(If you are unfamiliar with how to run Rack apps with config.ru, then see e.g. this tutorial).

Elsewhere in our code, we store some content:

# Store
uid = app.store(File.new('path/to/image.png'))
      # ===> returns a unique uid for that image, "2009/11/29/145804_file"

We can get the url for a processed and encoded version of this content:

url = app.url_for(uid, '30x30', :gif)
      # ===> "/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x30&s=aa78e877113f6bc9"

Now when we visit the url /2009/11/29/145804_file.gif?m=resize&o[geometry]=30x30&s=aa78e877113f6bc9 in the browser (given the app is running of course), we get the resized image!

NOTE: the GET parameter s=aa78e877113f6bc9 is for protecting from DOS attacks. You can turn this off if you wish - see the documentation.

Using in Rails

As mentioned earlier, the ActiveRecord/Rails part of the code is a layer which sits on top of the main app.

In fact, the main task of the ActiveRecord extension is storing the content uid mentioned above.

Let’s say we want to add a ‘cover image’ attribute to our Album model.

Then in environment.rb:

config.gem 'rmagick',    :lib => 'RMagick'
config.gem 'rack-cache', :lib => 'rack/cache'
config.gem 'dragonfly',  :lib => 'dragonfly/rails/images', :source => 'http://gemcutter.org'

The file ‘dragonfly/rails/images’ creates a Dragonfly App and configures it for use with Rails, then inserts it into the Rails middleware stack (see the docs for more info). Note that we’re using RMagick for image processing/encoding, and Rack::Cache to cache requests.

Migration: ruby add_column :albums, :cover_image_uid, :string

Model:

class Album < ActiveRecord::Base
  image_accessor :cover_image            # Defines reader/writer for cover_image
end

View (for uploading via a file field):

<% form_for @album, :html => {:multipart => true} do |f| %>
  ...
  <%= f.file_field :cover_image %>
  ...
<% end %>

View (to display):

<%= image_tag @album.cover_image.url(:gif) %>
<%= image_tag @album.cover_image.url('400x200') %>
<%= image_tag @album.cover_image.url('100x100!', :png) %>
<%= image_tag @album.cover_image.url('100x100#') %>
<%= image_tag @album.cover_image.url('50x50+30+30sw', :tiff) %>
<%= image_tag @album.cover_image.url(:rotate, 15) %>
...etc.

As you can see from the migration, the model’s main concern is storing the uid for the content. The method album.cover_image.url(...) is a wrapper around the Dragonfly app’s url_for(uid, ...) method mentioned earlier, using the stored cover image uid.

Extra niceties

The ActiveRecord extension module actually has a number of useful helper methods, which should make it extremely easy and intuitive to use, as if the image is like any other model attribute.

See the docs for using with ActiveRecord for more details, but for now, here are some examples of some of the things you can do.

Assignment: “`ruby album = Album.new

album.coverimage = ”\377???JFIF\000…“ # can assign as a string… album.coverimage = File.new(‘path/to/myimage.png’) # … or as a file… album.coverimage = some_tempfile # … or as a tempfile

album.cover_image # => #<Dragonfly::ActiveRecordExtensions::Attachment:0x103ef6128…

album.coverimage = nil album.coverimage # => nil ”`

Inspection and processing: “`ruby album.coverimage.width # => 280 album.coverimage.mime_type # => ‘image/png’

album.cover_image.transform!(‘30x30#nw’, :gif) # (operates on self)

album.coverimage.width # => 30 album.coverimage.mime_type # => ‘image/gif’ ”`

Write to a file: ruby album.cover_image.to_file('out.png')

URLs: ruby album.cover_image.url(:png) # => '/media/2009/12/05/170406_file.png' album.cover_image.url('300x200#nw', :gif) # => '/media/2009/12/05/170406_file.gif?m=resize_and_crop&o[height]=...'

Generating (for tests/populate tasks): ruby album.cover_image = Dragonfly::App[:images].generate(300, 200) (the above line of code assumes the Dragonfly app is called :images).

There are a number of validations you can use too:

class Album
  validates_presence_of  :cover_image
  validates_size_of      :cover_image, :maximum => 500.kilobytes
  validates_mime_type_of :cover_image, :in => %w(image/jpeg image/png image/gif)
  validates_property :width, :of => :cover_image, :in => (0..400)

  image_accessor :cover_image
end

Per-blog-post thumbnails

Now that we’re generating thumbnails on the fly, we can wave ‘bye-bye’ to reprocess tasks every time we change the sizes of our thumbnails, and smugly shout ‘so long suckers’ to tests bogged down by unnecessary image resizing.

However, it becomes apparent that now that our thumbnail definitions are in the correct place, other things become easier.

One example is this blog!!

We have a number of users who want to create blog posts with images sized on a per-blog-post basis.

Previously this would have been difficult, because we’d have had to specify the sizes up front, e.g.

class BlogPost
  some_macro_method :image,
    :thumbnails => {
      :small => "100x100!",
      # ...etc.
    }
end

Alternatively the user could resize the image themselves before uploading, and then use the original, but this is tedious.

Now we are generating thumbnails on the fly, this is trivial. There are a number of ways we could achieve this, but this blog uses something along the lines of the following (actually it has multiple images, but for simplicity, let’s assume there is one image per blog post):

First, we add an image attribute to the blog post model

Migration:

add_column :blog_posts, :image_uid, :string

Model:

class BlogPost &lt; ActiveRecord::Base
  image_accessor :image
end

Then we allow the user to add tags like img[300x200!] to the blog post.

Before rendering, substitute all instances of e.g. img[300x200!] with <img src="#{blog_post.image.url('300x200!')}" />

Here are some examples for this post!!

150x150
150x150!
150x150#
100x100+250+150nw

(image taken from here)

Flexible Configuration

Dragonfly can be configured in various ways to suit various use cases.

To see some examples, such as using with Heroku and S3, and text image replacement, see the docs.

Other approaches

Other on-the-fly processing libraries of note include Fleximage and Rack::Thumb (sorry if I’ve overlooked any other obvious ones!).

These are very good at what they do, but don’t quite encompass all the aspects I listed above (among other things, Fleximage is tied to Rails and ActiveRecord, while Rack::Thumb is a lightweight Rack middleware that deals only with image resizing/cropping), which is why I felt the need to build Dragonfly.

Getting it

The gem is hosted on gemcutter, installed in the usual way

sudo gem install dragonfly

The code is here on github.

Documentation is here.

Feel free to fork away, contribute, suggest improvements etc.

Enjoy!