Skip to navigation

Generating cropped thumbnails with acts_as_attachment

Published September 12, 2006 - Updated June 14, 2009

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.

  1. Dylan

    Thanks, but what’s so wrong with file_column that you would prefer to do all this?

  2. Terry

    File_column is proving shaky for me: trying to find a solid alternative

  3. 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

  4. labrat

    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 end

    I’ve definitely added you to my RSS reader. Cheers!

  5. Tim Lucas

    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
    end
    

    But I was under the impression that with_image didn’t return the result of the block, only a true/false.

    Did that code work for you?

  6. 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.

  7. 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_image method 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 in after_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
    
  8. Dylan

    Thanks, Tim, big help. Will give it a shot.

    Best.

  9. 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?

  10. 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

  11. 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).

  12. rick

    tim: my new attachment_fu rewrite should make this easier. check out #resize_image in lib/technoweenie/attachment_fu/processors/rmagick.rb

  13. Tim Lucas

    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
    end
    

    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?

  14. 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
      end 
  15. Tim 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…

  16. 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)
      end

    Note that this code assumes Rmagick so you should probably remove the references to other image processors at the top of attachment_fu.rb

  17. 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 end

    Also, 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.

  18. Todd

    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
    
  19. 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

  20. Tim Lucas

    Thanks Andy! Should really get some preview and autolinkage happening…

  21. 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!

  22. Tim Lucas

    Noice one! Send that patch to rick.

  23. John Burmeister

    That Patch worked PERFECT. Thank you.

  24. Bill Brasky

    Hey thanks for this perfect one liner patch. You saved me a lot of time. Eloquently.

    Thanks again.

  25. 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.

  26. Rohit

    Thanks very much. My thumbnails are much better now.

  27. 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

Thoughts

toolmantim

I’m Tim Lucas, a user experience developer currently in Sydney Australia.

I occasionally write, snap photos, present on various technical topics, tweet my going-ons, share teh codes and post tidbits to the scrapbook.

Most recently I published Simplifying ticket sales on sydneyoperahouse.com (February 16, 2010)

Work with me via Agency Rainford, or shoot an email to and say hello.

Powered by social networks