Using one carrierwave image uploader with different sizes on several models
2012-03-19
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!