toolmantim

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:

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!

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?

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_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

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?


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?

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 

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…

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

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.

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

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.