I'm now blogging here, you should go there. Follow me on twitter here.

Cropped Thumbnails in attachment_fu using ImageScience

14 April 2007

I’ve forked the git repository of attachment_fu to include this technique but these days I’m mainly using Paperclip for my image uploading needs.

Upon discovering attachment_fu by the awesome Rick Olsen and the (not as scary as RMagick) ImageScience I believed all my image uploading and resizing problems were solved! Mike Clark’s excellent tutorial led me down the garden path toward geting exactly what I needed.

However the precise functionality for the thumbnailing I required was missing. I needed to create proportionally (but non-square) cropped thumbnails of my images. For example you pass in a ‘portrait’ image but the thumbnail has to fit in a ‘landscape’ space on the page but without any horrid squashed pixels.

Like so.

Proportional Cropping

First thing I found when digging through the source to attachment_fu was that “!”, aspect ratio flag, wasn’t enabled. So I reenabled it. (changes marked with +)

FLAGS = ['', '%', '<', '>', '!']#, '@']

Then the to_s method of the geometry class wasn’t passing out the flags correctly.

def to_s
  str = ''
  str << "%g" % @width if @width > 0
  str << 'x' if (@width > 0 || @height > 0)
  str << "%g" % @height if @height > 0
  str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
+ str << RFLAGS.index(@flag)
end

And we needed to add a case to fix the heights when passed an !-suffixed geometry string in the new_dimensions_for method.

when  :aspect
  new_width = @width unless @width.nil?
  new_height = @height unless @height.nil?

With this fixed all it remained to do was to modify the resize_image function in the image_science_processor.rb.

def resize_image(img, size)
  # create a dummy temp file to write to
  self.temp_path = write_to_temp_file(filename)
  grab_dimensions = lambda do |img|
    self.width  = img.width  if respond_to?(:width)
    self.height = img.height if respond_to?(:height)
    img.save temp_path
    self.size = File.size(self.temp_path)
    callback_with_args :after_resize, img
  end
  size = size.first if size.is_a?(Array) && size.length == 1
  if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
    if size.is_a?(Fixnum)
      img.thumbnail(size, &grab_dimensions)
    else
      img.resize(size[0], size[1], &grab_dimensions)
    end
  else
    n_size = [img.width, img.height] / size.to_s
+   if size.ends_with? "!"
+     aspect = n_size[0].to_f / n_size[1].to_f
+     ih, iw = img.height, img.width
+     w, h = (ih * aspect), (iw / aspect)
+     w = [iw, w].min.to_i
+     h = [ih, h].min.to_i
+     img.with_crop( (iw-w)/2, (ih-h)/2, (iw+w)/2, (ih+h)/2) {
+       |crop| crop.resize(n_size[0], n_size[1], &grab_dimensions )
+       }
+   else
       img.resize(n_size[0], n_size[1], &grab_dimensions)
     end
  end
end

I got stuck for hours until I received some kind help from Ramon who helped me with the final piece of the puzzle and the guys from the comments on toolmantim.com on whose metaphorical shoulders I stood.

This method could easily be extended to the RMagick and miniMagick processors (with less fiddling as they have the proportional resize and crop built in) and I may do that very thing over the next couple of days as I humbly lay this patch before the original author.

Minimagick

Ian Drysdale has done a version of this for minimagick, why not check it out if ImageScience is not your bag.