The subject of cropping and resizing images comes up fairly often in the Rails community, with a number of tools out there to automate these manipulations.
When manipulating images, I like to use MiniMagick to keep the memory footprint of the application server itself small. Since MiniMagick just wraps the command-line convert and mogrify tools, special-casing your image resizer is as simple as modifying the flags passed to the underlying system command.
There are a number of resizing algorithms in use. Various approaches include maintaining the original image’s aspect ratio, cropping over-sized images, and padding resized images.
In order to achieve good fit an image may need to be both scaled and cropped. A well-fit image will maintain as much of the original image as possible while still conforming to the final target dimensions and aspect.
To illustrate the desired results, we’ll be manipulating the speakersitter image.

Check out some examples of the various approaches to fixed-size output.
-
With over-sized images, by far the simplest approach is the naive crop. Unfortunately, this fails us when the image is too small, and looks pretty terrible in most cases anyway.
-
Next up is the naive scale. This approach is nice in that it delivers consistent results no matter what the image’s original dimensions were. On the down side, this consistent behavior typically sucks.
-
Padding can be a nice way of getting generally good-looking output. A padding algorithm will scale an image maintaining its aspect ratio so that none of its dimensions is greater than the corresponding output dimension, and then will composite that scaled image onto the center of a blank image of the target dimensions. In this example, the checkerboard pattern represents the background image onto which the original image is composited. In real world usage, this image would be pure white or transparent.
-
The good fit approach is similar to the padding approach, in that the source image is immediately scaled with a fixed aspect ratio. In the padding approach the image’s dimensions such that none is greater than its corresponding output dimension, both are constrained such that none is less than the output dimension. This guarantees that part of the source image will be cropped unless it is exactly the same aspect as the output dimensions.
The scaled source image can be cropped according to one of nine different gravities. If your application deals primarily with portraits and face images, North gravity is typically the best.

For most other applications, including avatar and thumbnail generation, Center Gravity can be preferable.
Sadly, MiniMagick doesn’t support doing this all from the command line without a little pre-computation. We must sadly scale and then crop according to offsets we compute ourselves. Fortunately, I have already taken care of this for you!
I monkey-patched these changes directly into attachment_fu’s mini_magick_processor, but the calculations and generated commandline flags should serve you wherever you use MiniMagick.
module Technoweenie::AttachmentFu::Processors::MiniMagickProcessor GRAVITY_TYPES = [ :north_west, :north, :north_east, :east, :south_east, :south, :south_west, :west, :center ] def resize_and_crop_image(img, size, options = {}) gravity = options[:gravity] || :center g = Geometry.from_s(size.to_s) img.opaque img_width, img_height = *(img[:dimensions].map { |d| d.to_f } ) resize_string = '' # Resize image to minimize difference between actual dimension and target dimension if img_width / g.width < img_height / g.height resize_string = "#{g.width.to_i}x" resultant_width = g.width resultant_height = (img_height * (g.width / img_width)) else resize_string = "x#{g.height.to_i}" resultant_width = (img_width * (g.height / img_height)) resultant_height = g.height end width_offset, height_offset = crop_offsets_by_gravity( gravity, [ resultant_width, resultant_height ], [ g.width, g.height ] ) img.combine_options do |i| i.args << '+matte' i.resize(resize_string) i.gravity('NorthWest') i.crop "#{g.width.to_i}x#{g.height.to_i}+#{width_offset}+#{height_offset}!" end end def crop_offsets_by_gravity gravity, original_dimensions, cropped_dimensions raise(ArgumentError, "Gravity must be one of #{GRAVITY_TYPES.inspect}") unless GRAVITY_TYPES.include?(gravity.to_sym) raise(ArgumentError, "Original dimensions must be supplied as a [ width, height ] array") unless original_dimensions.kind_of?(Enumerable) && original_dimensions.size == 2 raise(ArgumentError, "Cropped dimensions must be supplied as a [ width, height ] array") unless cropped_dimensions.kind_of?(Enumerable) && cropped_dimensions.size == 2 original_width, original_height = original_dimensions cropped_width, cropped_height = cropped_dimensions # No vertical offset for northern gravity vertical_offset = case gravity when :north_west, :north, :north_east then 0 when :center, :east, :west then [ ((original_height - cropped_height) / 2.0).to_i, 0 ].max when :south_west, :south, :south_east then (original_height - cropped_height).to_i end horizontal_offset = case gravity when :north_west, :west, :south_west then 0 when :center, :north, :south then [ ((original_width - cropped_width) / 2.0).to_i, 0 ].max when :north_east, :east, :south_east then (original_width - cropped_width).to_i end return [ horizontal_offset, vertical_offset ] end end
Leave a Comment