Monday, March 7, 2011

Crop and Upload Image - Server Side

Previously I showed some HTML and JavaScript for cropping and uploading an image. Here is what I did on the server side to support this.

First I make sure I had rails 3 installed, configured for HAML and jquery. I also download the jqueryui and jcrop javascript and css files and add them to my project.  Then, I use the rails scaffolding functionality to get started:

ruby script\rails generate scaffold image content:binary width:integer height:integer name:string

After running:

rake db:migrate

I have a database table for storing an uploaded image, including the binary content, the height and width of the image and a name. Now that I have a place to store the image, I need to modify the generated web pages, so I can upload it.

Since I prefer HAML, the first thing that I do is rename the two erb files that I need to change: views/layouts/applications.html.erb to views/layouts/applications.html.haml and app/views/images/_form.html.erb to app/views/images/_form.html.haml.  The applications.html file is the template for all the pages, and I need to modify that to allow pages to add their own javascript into the header. _form.html is the partial that is used for both uploading new images and editing images.

Converting the applications.html file from erb to haml is just a straightforward transformation. Here is what it looks like when I am done:
  1 !!! 
  2 %html 
  3   %head 
  4     %title Image Upload 
  5     = stylesheet_link_tag :all 
  6     = javascript_include_tag :defaults 
  7     = csrf_meta_tag 
  8     = yield :head 
  9   %body 
 10     = yield
The only real line of note is the named yield on line 8. This is what will allow me to insert custom stylings and javascript into specific pages.

The only thing left is to rewrite _form.html.haml so that it generates HTML and Javascript like discussed in the client side post. Here is what this looks like.
  1 - content_for :head do 
  2   = stylesheet_link_tag 'jquery.Jcrop' 
  3   = javascript_include_tag  'jquery.Jcrop.min'
  4   %style 
  5     -# insert styling described in client post here 
  6   :javascript 
  7     // insert javascript described in client post here
  8 = form_for(@image) do |f| 
  9   - if @image.errors.any? 
 10     #error_explanation 
 11       %h2 
 12         = pluralize(@image.errors.count, "error") 
 13         prohibited this image from being saved:
 14       %ul 
 15         - @image.errors.full_messages.each do |msg|
 16           %li= msg 
 17   .field 
 18     = f.label :name 
 19     %br 
 20     = f.text_field :name 
 22   Drag and drop image here: 
 23   #cropWidget{:width=>400, :height=>400} 
 24     #surface 
 25     %canvas#canv1{:width=>400, :height=>400} 
 27   = f.hidden_field :content64 
 28   Uploaded image is here: 
 29   %canvas#canv2{:width=>200, :height=>200} 
 30   .field 
 31     = f.label :width 
 32     %br 
 33     = f.text_field :width, :readOnly=>true 
 34   .field 
 35     = f.label :height 
 36     %br 
 37     = f.text_field :height, :readOnly=>true 
 38   .actions 
 39     = f.submit :id=>"imageSubmit", :onClick=>"uploadSelection();", :disabled=>true;
I left out the specific CSS styles and JavaScript as they are described in the previous post. Notice that lines 1-7 are inserted into the head of the template as described by yield :head on line 8 of the application.html.haml file above. Lines 9-16 are the boilerplate error handling that was generated by the scaffold command. The remainder just generates the HTML for displaying the image and for entering the name of the image.

We are now almost, but not quite, done. If you notice the field that we are storing the image content in is content64, while the database column for the binary data is just called content. We need to take the base64 encoding of the image that is sent over the wire as part of the HTML POST and decode it to binary before saving in the database. To do that, we just need to add new methods in our app/models/image.rb Image class.
  1 require "base64" 
  2 class Image < ActiveRecord::Base 
  3   def content64 
  4     return nil unless content 
  5     return "data:image/png;base64," + Base64.encode64(content); 
  6   end 
  8   def content64=(c64) 
  9     index = c64.index(','); 
 10     self.content = Base64.decode64(c64[index..-1]); 
 11   end 
 12 end
This way when the content64 attribute is assigned in our controller class, it'll actually write the binary data to the content field so it can be saved to the database. The only clever part of this is that the content64 data that is passed from the HTML form has the initial string "data:image/png;base64,".  Lines 9-10 calculate where this prefix ends and just decodes the remainder of the string. Line 5 adds this prefix back on to the encoded binary content.

Other Work
So the above will allow you to upload a new image that has been cropped and save it to the database. However, to be able to view that image or edit it, more work is needed. Probably the most important thing to do is to add a new action which will return the image's binary content. The url for this action can be used in the src attribute of <img> tags. The other scaffold generated pages for viewing and listing the uploaded images are then modified to display the image rather than showing the binary content. The last step is to modify the javascript described previously to check if an image already exists (i.e. we are editing an existing uploaded image rather than uploading a new image), and preload that. Since this post has gone on long enough, I will leave all of those details as an exercise for the reader.

No comments: