Generating cropped thumbnails with acts_as_attachment
September 12, 2006 18:55 (Sydney Australia)
Rick Olson’s acts_as_attachment plugin is the shit when it comes to handling file uploads in Rails, but it lacks the :crop option that the file_column plugin does.
If you’re looking for flickr-style square thumbnails you’ll have to do the thumbnail generation yourself.
Luckily for us Rick provides an after_attachment_saved callback which gets called when a photo gets saved. We can use this hook to generate our own thumbnails rather than using acts_as_attachment’s pre-baked :thumbnails option. The only gotcha is that the hook also gets called when the thumbnail itself is saved, so you have to check the parent_id before going ahead.
class Photo < ActiveRecord::Base
THUMBS = { :profile => 150, :medium => 75, :tiny => 25 }
belongs_to :person
acts_as_attachment :storage => :file_system,
:content_type => :image
validates_as_attachment
# We handle our own thumbnail generation
after_attachment_saved do |photo|
if photo.parent_id.nil?
THUMBS.each_pair do |file_name_suffix, size|
thumb = thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, photo.id)
resized_image = photo.crop_resized_image(size)
unless resized_image.nil?
thumb.attributes = {
:content_type => photo.content_type,
:filename => photo.thumbnail_name_for(file_name_suffix.to_s),
:attachment_data => resized_image
}
thumb.save!
end
end
end
end
def crop_resized_image(size)
thumb = nil
with_image do |img|
thumb = img.crop_resized(size, size)
end
thumb
end
end
Maybe some future version of acts_as_attachment could provide a callback for the thumbnail generation itself, which could clean up our class significantly:
class Photo < ActiveRecord::Base
belongs_to :person
acts_as_attachment :storage => :file_system,
:content_type => :image,
:thumbnails => {
:profile => 150,
:medium => 75,
:tiny => 25
}
validates_as_attachment
thumbnail_attachment_data do |photo, file_name_suffix, size|
photo.crop_resized(size, size)
end
end

Comments
Dylan
Thanks, but what’s so wrong with file_column that you would prefer to do all this?
Terry
File_column is proving shaky for me: trying to find a solid alternative
Tim Lucas
Dylan: I need to save the width+height in the DB along with each image version, and, having used much of Rick Olson’s code before, I trust it to do the job
labrat
Thanks a lot for this tip. It came in handy.
You could rewrite the second part:
I’ve definitely added you to my RSS reader. Cheers!
Tim Lucas
But I was under the impression that
with_imagedidn’t return the result of the block, only a true/false.Did that code work for you?
Dylan
Thanks, Tim, I just switched over to acts_as_attachment and this code and it works great.
If however, I wanted non-square thumbs, how might I change the code to provide both dimensions? Thanks again.
Tim Lucas
Dylan, see the line:
resized_image = photo.crop_resized_image(size)This is where I do the photo manipulation. I call the
crop_resized_imagemethod I defined below, but you could just as easily do anything else. Below I just create a single thumbnail, “my_funky_thumb”, and do the custom image manipulation right there inafter_attachment_saved:Dylan
Thanks, Tim, big help. Will give it a shot.
Best.
Labrat
Yes the code does seem to work for me. I found the reference in the comments for a_a_attachment.
How did you get acts_as_attachment to work in a production site? Are you using mongrel_cluster or anything?
Tim Lucas
On the production site we’re using two fastcgi ligttpd dispatchers, with a capistrano after_update command that symlinks the “photos” directory from shared into public
Labrat
Thanks for the info. Finally got it working (embarassed to say it was a permission issue—got Capistrano to take care of that now) under nginx + mongrel cluster (no apache).
rick
tim: my new attachment_fu rewrite should make this easier. check out #resize_image in lib/technoweenie/attachment_fu/processors/rmagick.rb
Tim Lucas
Nice!
So now you can have something more like this, eh?
Nice! I like how it uses a collection of temp files to manage the data for all the intermediate temps. So the last call to
temp_path=wins, eh?Wiktor
Why don’t you just overload thumbnail_for_image ?
acts_as_attachment :thumbnails => { :thumb => { :size => "100x75", :crop => "4:3" }, :reg => { :size => "300x225", :crop => "4:3" } } def thumbnail_for_image(img, size) size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum) if size.is_a?(Hash) dx, dy = size[:crop].split(':').map(&:to_f) w, h = (img.rows * dx / dy), (img.columns * dy / dx) img.crop!(::Magick::CenterGravity, [img.columns, w].min, [img.rows, h].min) end size = size[:size] end if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) size = [size, size] if size.is_a?(Fixnum) img.thumbnail(size.first, size[1]) else img.change_geometry(size.to_s) { |cols, rows, image| image.resize(cols, rows) } end endTim Lucas
can’t remember Wiktor… twas a while now. Your method looks cleaner than mine, so if it works then go with that I say. attachment_fu might make it even nicer…
Martyn Loughran
For those of you using attachment_fu the following should work. I extended Wiktor’s method to resize the image after cropping.
def resize_image(img, size) size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum) if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) size = [size, size] if size.is_a?(Fixnum) img.thumbnail!(*size) # This elsif extends elsif size.is_a?(Hash) dx, dy = size[:crop].split(':').map(&:to_f) w, h = (img.rows * dx / dy), (img.columns * dy / dx) img.crop!(::Magick::CenterGravity, [img.columns, w].min, [img.rows, h].min) size = size[:size] w2, h2 = size.split('x').map(&:to_f) img.resize!(w2,h2) else img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) } end self.temp_path = write_to_temp_file(img.to_blob) endNote that this code assumes Rmagick so you should probably remove the references to other image processors at the top of attachment_fu.rb
Todd
For those who wish to use Wiktor’s style of thumbnails with MiniMagick, this has worked for me so far. I haven’t tested this too extensively, so play with it first. I’m sure it can use some cleaning up.
def resize_image(img, size) size = size.first if size.is_a?(Array) && size.length == 1 if size.is_a?(Hash) dx, dy = size[:crop].split(':').map(&:to_f) ih, iw = img[:height], img[:width] w, h = (ih * dx / dy), (iw * dy / dx) w = [iw, w].min.to_i h = [ih, h].min.to_i img.crop("#{w}x#{h}+0+0\" -gravity \"Center") size = size[:size] end if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) if size.is_a?(Fixnum) size = [size, size] img.resize(size.join('x')) else img.resize(size.join('x') + '!') end else img.resize(size.to_s) end self.temp_path = img.path endAlso, I found that I had to override create_or_update_thumbnail and the corresponding call in after_process_attachment to use size instead of *size. Passing *size flattens the hash into an array.
Todd
Wow, that looks awful. Sorry about that.
Andy Croll
Blogged a way of cropping using ImageScience and attachment_fu with proportional thumbnails, if you’re interested…
http://deepcalm.com/writing/cropped-thumbnails-in-attachment_fu-using-imagescience
Tim Lucas
Thanks Andy! Should really get some preview and autolinkage happening…
labrat
I’ve finally come up with a oneline fix that does it for attachment_fu + rmagick, added bonus is you can hang on to old resizing methods by design.
http://pastie.caboo.se/58467
Sure was a bastard to figure out though!
Tim Lucas
Noice one! Send that patch to rick.
John Burmeister
That Patch worked PERFECT . Thank you.
Bill Brasky
Hey thanks for this perfect one liner patch. You saved me a lot of time. Eloquently.
Thanks again.
labrat
Someone asked me for a similar patch only to minimagick. Unfortunately, I don’t use it myself and would rather not use my own app as a guinea pig but this should work in principle.
Maybe someone can try this out and fix it as necessary.
http://pastie.caboo.se/64069
Just like the oneliner above, as long as you pass it an Array composed of Fixnums of two equal values it will crop it with center gravity. If you’d like to mix it with traditional cropping just pass it a String of equal values. At least that’s the original concept.
It’s based on Todd’s fix above. YMMV .
Rohit
Thanks very much. My thumbnails are much better now.
David Jones
I have written a simple 3 line modification to acts_as_attachment, along with an easy to follow tutorial to get cropping working with acts_as_attachment.
http://d-jones.com/2007/10/11/cropping-support-for-acts_as_attachment
To comment on this article you must have javascript enabled.