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.

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.