Creating Resized Thumbnails with RMagick

Problem

You want to create resized thumbnails as you upload images to your application.

Solution

Use the RMagick image library to process thumbnails as each image is uploaded and saved to your application. This solution extends ," by adding a "thumb" field to the photo table for storing image thumbnails:

db/migrate/001_build_db.rb:

class BuildDb < ActiveRecord::Migration
 def self.up
 create_table :items do |t|
 t.column :name, :string
 t.column :description, :text
 end
 create_table :photos do |t|
 t.column :item_id, :integer
 t.column :name, :string
 t.column :content_type, :string
 t.column :data, :binary
 t.column :thumb, :binary
 end
 end
 def self.down
 drop_table :photos
 drop_table :items
 end end

It also adds image-processing code to the photo method of the Photo model definition:

app/models/photo.rb:

require 'RMagick' # or, this line can go in environment.rb include Magick class Photo < ActiveRecord::Base
 belongs_to :item
 def photo=(image_field)
 self.name = base_part_of(image_field.original_filename)
 self.content_type = image_field.content_type.chomp
 img = Magick::Image::read_inline(Base64.b64encode(image_field.read)).first
 img_tn = img
 img.change_geometry!('600x600') do |cols, rows, image|
 if cols < img.columns or rows < img.rows then
 image.resize!(cols, rows)
 end 
 end 
 self.data = img.to_blob
 img_tn.change_geometry!('100x100') do |cols, rows, image|
 if cols < img.columns or rows < img.rows then
 image.resize!(cols, rows)
 end 
 end 
 self.thumb = img_tn.to_blob
 # Envoke RMagick Garbage Collection:
 GC.start
 end
 def base_part_of(file_name)
 name = File.basename(file_name)
 name.gsub(/[^\w._-]/, '')
 end end

The Photos Controller gets an additional method, show_thumb, to fetch and display thumbnail images:

app/controllers/photos_controller.rb:

class PhotosController < ApplicationController
 def show
 @photo = Photo.find(params[:id])
 send_data(@photo.data,
 :filename => @photo.name,
 :type => @photo.content_type,
 :disposition => "inline")
 end
 def show_thumb
 @photo = Photo.find(params[:id])
 send_data(@photo.thumb,
 :filename => @photo.name,
 :type => @photo.content_type,
 :disposition => "inline")
 end end

Discussion

To get a better feel of what's going on behind the scenes when you upload a file you can set a breakpoint in the photo method and inspect the properties of the incoming image_field parameter using the breakpointer.

To learn more about using the Rails breakpoint facility, see .


The class method tells us that we are dealing with a object of the StringIO class:

irb(#<Photo:0x40a7dd10>):001:0> image_field.class
=> StringIO

The first thing we extract from this object is the name of the uploaded file. The solution uses the base_part_of method to perform some cleanup on the filename by removing spaces and any unusual characters. The result is saved in the "name" attribute of the Photo object:

irb(#<Photo:0x40a7dd10>):002:0> image_field.original_filename
=> "logo.gif" 

Next, we can examine the content_type of the image. The content type method of the StringIO class returns the file type with a carriage return appended to the end. The solution removes this character with chomp and saves the result.

irb(#<Photo:0x40a7dd10>):003:0> image_field.content_type
=> "image/gif\r" 

The solution attempts two resize operations for each uploaded image. This is usually what you want to avoid storing arbitrarily large image files in your database. Each call to RMagick's change_geometry! method attempts to resize its own copy of the Magick::Image object if the size of that object is larger than the dimensions passed to change_geometry!. If the uploaded image is smaller than the minimum requirements for your primary or thumbnail images fields, then skip resizing it.

RMagick's change_geometry! is passed a geometry string (e.g., '600x600'), which specifies the height and width constraints of the resize operation. Note that the aspect ratio of the image remains the same. The method then yields to a block that we define based on our specific requirements. In the body of our blocks, we check that the image's height and width are both smaller than the corresponding values we're constraining to. If so, the call does nothing, and the image data is save to the database, otherwise the resizing is performed.

After a resize attempt, each image object is converted to a blob type and saved in either the data or thumb fields of the photos table.

As in ," we display these images with methods that use send_data in our Photos controller.

See Also