19 Mar 2012

Using one carrierwave image uploader with different sizes on several models

First of all, a typical use case:

  • 2 or more models with some image to upload, each model needs different image sizes. For example, you might need images for user’s avatar, photo galleries and/or screenshots.
  • using the awesome carrierwave gem

The common solution is to have several image uploaders, often with fancy names to distinguish them. I didn’t like this approach, so why don’t use some metaprogramming? ;-)

I’ve used this solution on a commercial project with enough satisfaction, the mayor advantages are:

  • DRY code: it doesn’t make sense to have several image uploaders just because you need different sizes on each model
  • embrace conventions: you pick some decent version names (eg. thumb, mini, main, and so on) and reuse them contextually to the model.

To get an instant picture of what we’re going to achieve, here’s the custom ImageUploader, I’ve removed some autogenerated code, you should already know how to use it. Check the comments inside it:

class ImageUploader < CarrierWave::Uploader::Base
  # use mini_magic gem for image processing
  include CarrierWave::MiniMagick

  # call setup_available_size method before cache image
  before :cache, :setup_available_sizes

  def store_dir
    # ...
  end

  def default_url
    # ...
  end

  # we process images with a custom method (read above)
  process :dynamic_resize_to_fit => :default

  # default processing, we assume that each model has a "mini" version
  version :mini do
    process :dynamic_resize_to_fit => :mini
  end

  # conditional processing: we process "thumb" version only if it was defined in model
  version :thumb, :if => :has_thumb_size? do
    process :dynamic_resize_to_fit => :thumb
  end

  def extension_white_list
    # ...
  end

  def sanitize_regexp
    # ...
  end

  # a lame wrapper to resize_to_fit method
  def dynamic_resize_to_fit(size)
    resize_to_fit *(model.class::IMAGE_SIZES[size])
  end

  # here's the metaprogramming magic!
  # we check if the called method matches "has_VERSION_size?"
  # VERSION is a version name for image size
  def method_missing(method, *args)
    # we've already defined "has_VERSION_size?", so if a method with
    # similar name is missed, it should return false
    return false if method.to_s.match(/has_(.*)_size\?/)
    super
  end

  protected
  # the method called at the start
  # it checks for <model>::IMAGE_SIZES hash and define a custom method "has_VERSION_size?"
  # (more on this later in the article)
  def setup_available_sizes(file)
    model.class::IMAGE_SIZES.keys.each do |key|
      self.class_eval do
        define_method("has_#{key}_size?".to_sym) { true }
      end
    end
  end

end

And now, some models, each with the same ImageUploader and a IMAGE_SIZES Hash containing same keys, but different image sizes:

# app/models/photo.rb
class Photo < ActiveRecord::Base
  # custom image sizes: each key is a version name
  IMAGE_SIZES = {
    :default => [1280, 1280],
    :mini => [300,900],
    :thumb => [100, 300]
  }

  mount_uploader :image, ImageUploader
  # ...
end

# app/models/product.rb
class Product < ActiveRecord::Base
  # other images sizes: same keys, different sizes
  IMAGE_SIZES = {
    :default => [700, 700],
    :mini => [300,300],
    :thumb => [100, 100]
  }

  mount_uploader :image, ImageUploader
  # ...
end

As you can see, the key part relies on three methods:

setup_available_sizes: it defines some helper methods, according to the versions that where specified in models. That’s why it gets called before processing and storage of the uploaded file. Did you notice that this method accepts a file argument? It’s not a typo, but it’s because Carrierwave always passes that object to its callbacks (check the code here and here, it’s not documented). If you try to omit it, you’ll get a ArgumentError.

method_missing: it doesn’t need too much explanation (or go to read this book, now!), it should be enough to know that in this case, we use it to check if a given model, has defined a particular version (through the setup_available_sizes method we’ve seen above). In fact, method_missing is called if and only if there isn’t a has_VERSION_size? defined. That’s why it returns false.

dynamic_resize_to_fit: this is a simple wrapper to the carrierwave’s resize_to_fit method. Instead of passing width and height values, we pass a version name, so it can lookup the relative sizes from the model. To be honest, this approach is quite lame, because you can use some more motaprogramming fu to dynamically wrap carrierwave’s processor methods. Now you have a decent excuse to play with something after you’ve finished to read ;-)

That’s all, folks!

Enjoyed this article? Share it!