Update: A few people have emailed me with updates for creating square thumbnails with acts_as_attachment. I’d recommend using the Paperclip plugin for doing your image attachments, you can simply use the # syntax for creating square thumbnails.
An example from the Paperclip readme:
has_attached_file :avatar,
:styles => { :thumb => ["32x32#", :png] }
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
Archived comments
Comments were previously allowed on articles. Though no new comments are being accepted you can see the old comments below.
-
Thanks, but what’s so wrong with file_column that you would prefer to do all this?
-
File_column is proving shaky for me: trying to find a solid alternative
-
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
-
Thanks a lot for this tip. It came in handy.
You could rewrite the second part:
def crop_resized_image(size) self.with_image do |img| img = img.crop_resized(size, size) end endI’ve definitely added you to my RSS reader. Cheers!
-
If that’s the case you can probably get away with:
def crop_resized_image(size) self.with_image do |img| img.crop_resized(size, size) end endBut 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?
-
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.
-
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:# We handle our own thumbnail generation after_attachment_saved do |photo| if photo.parent_id.nil? thumb = thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id("my_funky_thumb", photo.id) thumb_image = nil photo.with_image do |img| thumb_image = img.do_a_resize.do_a_crop end unless thumb_image.nil? thumb.attributes = { :content_type => photo.content_type, :filename => photo.thumbnail_name_for("my_funky_thumb"), :attachment_data => thumb_image } thumb.save! end end end -
Thanks, Tim, big help. Will give it a shot.
Best.
-
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?
-
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
-
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).
-
tim: my new attachment_fu rewrite should make this easier. check out #resize_image in lib/technoweenie/attachment_fu/processors/rmagick.rb
-
Nice!
So now you can have something more like this, eh?
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 # Override the actual thumbnail processing def resize_image(img, size) img.crop_resized(size, size) self.temp_path = write_to_temp_file(img.to_blob) end endNice! 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? -
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 end -
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…
-
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
-
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.
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 enddef resize_image(img, size) size = size.first if size.is_a?(Array) && size.length == 1Also, 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.
-
Wow, that looks awful. Sorry about that.
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 end -
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
-
Thanks Andy! Should really get some preview and autolinkage happening…
-
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!
-
Noice one! Send that patch to rick.
-
That Patch worked PERFECT. Thank you.
-
Hey thanks for this perfect one liner patch. You saved me a lot of time. Eloquently.
Thanks again.
-
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.
-
Thanks very much. My thumbnails are much better now.
-
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