• home
  • forum
  • my
  • kt
  • download
  • Image uploads and resizing for Rails models with mini-magick

    Author: 2007-08-25 16:52:16 From:

    You can download a sample application which uses the method described here at download
    It uses a rails scaffold though.

    We've all heard of RMagick but I hear it uses quite a bit of memory which can in turn be an issue for users of shared hosting. Here's a small tutorial on how to enable image uploads and resizing with mini-magick.

    First of all you will need to install mini-magick. I would normally recommend you type in "gem install mini_magick" into a command prompt but you are automatically assuming your rails hosting provider has support for mini-magick. Worse, the "gem install mini_magick" command doesn't seem to work on Windows for whatever reason. So here is another method of doing this.

    Go to the rubyforge project site for mini-magick at http://rubyforge.org/frs/?group_id=1358

    Download mini_magick-1.2.0.zip from the list of files. Or just download the latest version. There will surely be something new at some point.

    The next thing you have to download is ImageMagick. The website for ImageMagick is found at http://www.imagemagick.org/script/binary-releases.php

    The windows releases seem to be found near the bottom of the page. If you're on windows, get the file named ImageMagick-6.3.0-0-Q16-windows-dll.exe or something similar.

    Next, unzip the mini_magick-1.2.0.zip file you got from RubyForge. Go to the lib folder and copy the file "mini_magick.rb" to the lib folder of your rails application.

    Now, I'm assuming you are uploading an image for a rails model. We'll be working here with a model named Product. The first thing you can do is to create a partial for image uploads. This partial should be rendered in the form for adding or editing a new product. Lets call it _image.rhtml. Here goes an example:


    Code :   - fold - unfold
    1. <div>
    2. <p>Upload an image for your product</p>
    3. <input type="hidden" id="picture_id" value="<%= @product.id %>" name="picture[id]"/><br/>
    4. <input type="file" id="picture_file"  name="picture[file]"/><br />
    5. </p>
    6. </div>
    Next, I'm assuming you have the main form for creating or editing a product. It should have it's multipart value set to true. Here's how to do it.

    For the forms where you will be editing a product, you can use this:


    Code :   - fold - unfold
    1. <%= start_form_tag({:action => 'edit', :id => @product}, :multipart => true) %>
    For the forms where you will be adding a new product, you can use this:


    Code :   - fold - unfold
    1. <%= start_form_tag({:action => 'new'}, :multipart => true) %>
    You can see, that they will both generate very normal form tags except that the value of multipart is set to true. Now, inside this form, you should render the partial for _image. You can do it like so:


    Code :   - fold - unfold
    1. <%= render :partial => 'image', :object => @product %>
    Very good. Next, it is good to create a model to store pictures. Here is how I wrote mine.


    Code :   - fold - unfold
    1.     require 'mini_magick'
    2.     
    3.     class Picture
    4.     
    5.       attr_accessor :id, :file
    6.     
    7.       def initialize(id,file)
    8.         @id = id
    9.         @file = file
    10.        @filename =  base_part_of(file.original_filename)
    11.        @content_type = file.content_type.chomp  
    12.      end
    13.     
    14.      def base_part_of(file_name)
    15.       name = File.basename(file_name)
    16.        name.gsub(/[^\w._-]/, '')
    17.      end
    18.     
    19.      def save
    20.        is_saved = false
    21.        begin
    22.          if @file
    23.           #using @id, find the id of the User, then use the id to create the title
    24.           if @content_type =~ /^image/
    25.   #instead of product = Product.find, you can say dog = Dog.find or whatever corresponds to your model.
    26.              current_product = Product.find(@id.to_i)
    27.              #Make the directory for the id
    28.              Dir.mkdir("#{RAILS_ROOT}/public/images/products/#{@id}") unless File.exist?("#{RAILS_ROOT}/public/images/products/#{@id}")
    29.              #Then create the temp file
    30.              File.open("#{RAILS_ROOT}/public/images/products/#{@id}/#{@filename}", "wb") do |f|
    31.               f.write(@file.read)
    32.             end
    33.       product_image_crop("#{@filename}")
    34.       #update the current product
    35.       image_names = product_image_names("#{@filename}")
    36.       File.open("#{RAILS_ROOT}/public/yo.txt", "wb") do |f|
    37.         f.write(image_names)
    38.       end
    39. current_product.update_attributes("image_square" => image_names[0], "image_small" => image_names[1], "image_medium" => image_names[2], "image_original" => image_names[3])
    40.             is_saved = true
    41.             end
    42.          end
    43.       rescue
    44.        end
    45.        return is_saved
    46.     end
    47.  
    48.   def product_image_crop(product_image_title)
    49.   
    50.     #product_title = product_title.squeeze.gsub(" ","_").downcase
    51.     #find the extension for this file
    52.     image_file_extension = product_image_title[product_image_title.rindex(".") .. product_image_title.length].strip.chomp
    53.     image = MiniMagick::Image.from_file("#{RAILS_ROOT}/public/images/products/#{@id}/#{product_image_title}")
    54.    
    55.     image.resize "400X300"
    56.     image.write("#{RAILS_ROOT}/public/images/products/#{@id}/#{@id}_medium#{image_file_extension}")  
    57.  
    58.     image.resize "240X180"
    59.     image.write("#{RAILS_ROOT}/public/images/products/#{@id}/#{@id}_small#{image_file_extension}") 
    60.    
    61.     image.resize "50X50"
    62.     image.write("#{RAILS_ROOT}/public/images/products/#{@id}/#{@id}_square#{image_file_extension}")
    63.  
    64.     #Finally, rename the originally uploaded image
    65.     File.rename("#{RAILS_ROOT}/public/images/products/#{@id}/#{product_image_title}", "#{RAILS_ROOT}/public/images/products/#{@id}/#{@id}_original#{image_file_extension}")
    66.  
    67.   end
    68.     
    69.   def product_image_names(product_image_title)
    70.      image_file_extension = product_image_title[product_image_title.rindex(".") .. product_image_title.length].strip.chomp
    71.     #Generate an array containing the url of all the images
    72.      ["/images/products/#{@id}/#{@id}_square#{image_file_extension}",  "/images/products/#{@id}/#{@id}_small#{image_file_extension}","/images/products/#{@id}/#{@id}_medium#{image_file_extension}",  "/images/products/#{@id}/#{@id}_original#{image_file_extension}"]
    73.    end
    74.     
    75.    end
    Now, if I could explain what this class does. First of all, it should be named "picture.rb" and should be saved in your models directory. But please note that it does not inherit from ActiveRecord::Base.

    Now what is happening here. First of all, if you look at the partial, _image.rhtml which we created earlier, You will notice that it has two fields, picture_id and picture_file. Now, we can create a new Picture object using Picture.new(id,file), so id corresponds to picture_id and picture_file corresponds to the file being uploaded. More on this later. Now the Picture model has a method called save and this returns true or false. If it returns true, then the image has been saved.

    At this point, you should probably change the migration for your Product model. You add the following:


    Code :   - fold - unfold
    1.     t.column :image_square, :string
    2.     t.column :image_small, :string
    3.     t.column :image_medium, :string
    4.     t.column :image_original, :string
    So you have four columns for different images. I'm assuming you will want 4 different versions  of your image. From a really small thumbnail to the original image.

    The method product_image_names finds the url for each picture. Note that each image is saved under the id for the product. Images for a product with an id of 1, are saved under the folder, /images/products/1 e.t.c

    Anyway, still more work to do. Go to your Product model and add the following.

    First, add this filter to your product model


    Code :   - fold - unfold
    1. before_destroy :delete_image_directory
    Here is the actual method which should also be added to the Product model.


    Code :   - fold - unfold
    1.   #Method to delete a directory
    2.   def rmtree(directory)
    3.   Dir.foreach(directory) do |entry|
    4.     next if entry =~ /^\.\.?$/     # Ignore . and .. as usual
    5.     path = directory + "/" + entry
    6.     if FileTest.directory?(path)
    7.       rmtree(path)
    8.     else
    9.       File.delete(path)
    10.     end
    11.   end
    12.   Dir.delete(directory)
    13. end
    14.  
    15.   def delete_image_directory
    16.   image_dir = "#{RAILS_ROOT}/public/images/products/#{self.id}"
    17.   if File.exist?(image_dir)
    18.     begin
    19.       rmtree(image_dir)
    20.     rescue
    21.     end
    22.   end
    23.   end
    The above methods simply delete the directory of images for your product.

    Next, the controllers.

    Let's say you are adding products via the admin interface. So the controller would be AdminController.

    So let's say you are using the AdminController. Inside this controller, add this method:


    Code :   - fold - unfold
    1.   def save_picture
    2.   @picture = Picture.new(params[:picture][:id], params[:picture][:file])
    3.   resp = ""
    4.   if @picture.save
    5.     resp = 'and it\'s picture was successfully uploaded'
    6.   else
    7.     if Product.find(params[:picture][:id]).image_square != nil
    8.       resp = ''
    9.     else
    10.       if params[:picture][:file].class == StringIO
    11.         resp = 'but you have not yet chosen a picture for it'
    12.       else
    13.       resp = 'but I could not upload the picture because it is not a valid Jpeg, Gif or Png file'
    14.       end
    15.     end
    16.   end
    17.   return resp
    18.   end
    Now, I'm assuming you have save and edit methods for your product model here also. Here's mine.


    Code :   - fold - unfold
    1.   def new
    2.     reps = ""
    3.     if request.post?
    4.       @product = Product.new(params[:product])
    5.       if @product.save
    6.     if params[:picture]
    7.       params[:picture][:id] = @product.id
    8.       reps = save_picture
    9.     end
    10.         flash[:notice] = "A new product was successfully added #{reps}"
    11.         redirect_to :action => 'list'
    12.       end
    13.     else
    14.       @product = Product.new
    15.     end
    16.   end
    17.  
    18.   def edit
    19.   reps = ''
    20.         @product = Product.find(params[:id])
    21.     if request.post?
    22.       if @product.update_attributes(params[:product])
    23.     if params[:picture]
    24.       params[:picture][:id] = @product.id
    25.       reps = save_picture
    26.     end
    27.         flash[:notice] = "The product was successfully edited #{reps}"
    28.         redirect_to :action => 'show', :id => @product
    29.       end
    30.     end
    31.   end
    Note that reps simply tells you if the product image has been saved or not. Now you're basically done. If you want to display an image for your product, you can simply add in your view.


    Code :   - fold - unfold
    1. <%= @product.image_medium %>
    or


    Code :   - fold - unfold
    1. <%= @product.image_small %>
    A small note on mini-magick commands though.

    Let's say I want to make one, and only one change to a file. To load it, I use the new command.


    Code :   - fold - unfold
    1. image = MiniMagick::Image.new("william.jpg")
    2. image.resize "240X180"
    Using MiniMagick::Image.new means that any change to the file is permanent. If william.jpg had dimensions 1028X876, it's dimensions will be permanently changed to 240X180.

    Now if we want to save multiple versions of the file, this is what we need to do.


    Code :   - fold - unfold
    1. image = MiniMagick::Image.from_file("william.jpg")
    2.  
    3. #resize to 400X300
    4. image.resize "400X300"
    5. #now save it with a different name
    6. image.write("william_medium.jpg")
    7.  
    8. image.resize "240X180"
    9. image.write("william_240.jpg")

    discuss this topic to forum

    relation tutorial

    No relevant information

    Category

      Database Related (2)
      Getting Started (7)
      Helpers (4)
      Image Manipulation (2)
      Security (4)

    New

    Hot