For a project I’m currently working on, I needed a way to upload several photos to attach to a “dog” model, and allow easy cropping of the photos. In Ruby on Rails, this turned out to be harder than I expected.
The main problems I encountered were:
After several weekends of experimenting, reading lots of blogs and tutorials, and playing with various plug-ins, I finally have a working solution I’m pretty happy with. Here’s what I ended up doing:
I have two models: “Dog” and “Upload”. I started by calling my second model “Attachment”, but after running into problems, I learned that Paperclip uses that name internally and will not work correctly if you name your model “Attachment”.
$ rails dogs $ cd dogs $ script/plugin install git://github.com/giraffesoft/attribute_fu.git $ script/generate model dog name:string $ script/generate model upload description:string dog_id:integer \ photo_file_name:string photo_file_size:integer $ script/generate controller dogs $ script/generate controller uploads |
The Dog model just has a name for now. The Upload model will represent each uploaded photo. It has a description which the user can enter, a dog_id foreign key, and two attributes for Paperclip: a photo_file_name string and a photo_file_size integer. See the Paperclip documentation for more details on how these (and other) special attributes are used.
# models/dog.rb class Dog < ActiveRecord::Base has_many :uploads, :attributes => true, :discard_if => proc { |upload| upload.photo_file_size.nil? } end |
I updated the Dog model to have many uploads. The “:attributes” parameter is part of attribute_fu, and lets the Dog controller create associated models at the same time a new Dog is created. The “:discard_if” tells attribute_fu not to create an associated model if certain conditions are met — in this case, if there’s no file that was uploaded. This is needed because we may put many file upload fields in the “new Dog” form, but a user may only select one or two photos. In that case, all the blank form elements would be submitted too, and created as empty Upload instances.
#models/upload.rb class Upload < ActiveRecord::Base belongs_to :dog has_attached_file :photo, :styles => { :thumb => ["100x100", :jpg], :pagesize => ["500x400", :jpg], }, :default_style => :pagesize end |
This Upload model belongs_to a dog, to match the has_many association above. “has_attached_file” is part of Paperclip, and the “photo” attribute must match the prefix of the database fields above (e.g. photo_file_name). Paperclip can automatically re-size photos and keep multiple versions, based on the “styles” hash. Here we create a small thumbnail, and a version good for embedding in web pages. In both cases, the photo is converted to a JPEG if needed. The original photo is stored with a style of “original”.
The main view we’re concerned about is the “new dog” view. This will give the user a form to add a new dog, and upload photos to associate with the dog.
# views/dogs/new.html.erb <h1>New dogh1> < % form_for @dog, :html => { :multipart => true } do |f| %> < %= f.error_messages %> <p> < %= f.label :name %><br /> < %= f.text_field :name %> p> <div id='uploads' style='border: 1px solid silver'> < %= f.render_associated_form(@dog.uploads, :new => 5) %> div> < %= f.add_associated_link('Add another photo', @dog.uploads.build) %> <p> < %= f.submit "Create" %> p> < % end %> |
< %= f.label :name %>
< %= f.text_field :name %>
< %= f.submit "Create" %>
< % end %>The first part of the above view gives us a form for a new dog instance. It also calls attribute_fu’s “render_associated_form” to create a nested form for upload instances. Here we create five blank instance forms. The instance forms are generated by a partial named after the associated model… in this case the partial is _upload.html.erb (below). We also add a link to create additional file upload forms on the fly via attribute_fu JavaScript with the “add_associated_link” call.
# views/dogs/_upload.html.erb <p class='upload'> <label for="upload_description">Description:label> < %= f.text_field :description %> <label for="upload_photo">Photo:label> < %= f.file_field :photo %> < %= f.remove_link "remove" %> p> |
< %= f.text_field :description %> < %= f.file_field :photo %> < %= f.remove_link "remove" %>
The partial is pretty standard, just a field for our photo description, and a file upload field. The only unusual part is attribute_fu’s “remove_link” JavaScript call, which lets the user remove one of the photo upload forms.
Here’s what the completed form looks like:
The nice thing about attribute_fu is that no special controller logic is needed! The dog controller is just a standard RESTful controller! When the form is submitted, the dog instance is created, and all the associated upload instances are created simultaneously:
# controllers/dogs_controller.rb class DogsController < ApplicationController def index @dogs = Dog.find :all end def show @dog = Dog.find params[:id] end def new @dog = Dog.new end def create @dog = Dog.new params[:dog] if @dog.save flash[:notice] = 'Dog was successfully created.' redirect_to @dog else render :action => "new" end end end |
The last feature to add is the image cropping functionality. We’re going to do this in the “edit upload” action. The edit form will show the image, and let the user drag a rectangle around the region to crop. The form will return the rectangle coordinates, and we’ll override the standard update_attributes method of the Upload model, to perform the crop.
In the main view template, I’m including the jsCropperUI JavaScript code and its dependencies. The scripts go in the public/javascripts directory when you install the jsCropperUI code.
# views/layouts/application.html.erb < %= javascript_include_tag 'cropper/lib/prototype.js' %> < %= javascript_include_tag 'cropper/lib/scriptaculous.js?load=builder,dragdrop' %> < %= javascript_include_tag 'cropper/cropper.js' %> |
The “edit upload” view is a little complex. Here’s how it’s put together:
# view/uploads/edit.html.erb <h1>Editing uploadh1> < % form_for(@upload) do |f| %> < %= f.error_messages %> <p> < %= f.label :description %><br /> < %= f.text_field :description %> p> <!-- CROP FORM --> <div id='cropwrap'> < %= image_tag @upload.photo.url, :id => 'cropimage' %> div> <div id='cropresults'> < %= f.label 'x1' %> < %= f.text_field 'x1', :size => 6 %> <br /> < %= f.label 'y1' %> < %= f.text_field 'y1', :size => 6 %> <br /> < %= f.label 'width' %> < %= f.text_field 'width', :size => 6 %> <br /> < %= f.label 'height' %> < %= f.text_field 'height', :size => 6 %> <br /> div> <!-- cropresults --> <!-- END CROP FORM --> <p> < %= f.submit "Update" %> p> < % end %> |
< %= f.label :description %>
< %= f.text_field :description %>
< %= f.submit "Update" %>
< % end %>In addition to the edit field for our model’s normal attributes (“description”, in this case), we also have fields for the geometry of our crop rectangle. These fields will be filled in automatically by the jsCropperUI. We also need to display the image we’re cropping, and identify it with a CSS ID so the jsCropperUI can attach to it. To attach the jsCropperUI, we need to add some JavaScript to the page:
# view/uploads/edit.html.erb <script type="text/javascript" language="JavaScript"> function onEndCrop( coords, dimensions ) { $( 'upload_x1' ).value = coords.x1; $( 'upload_y1' ).value = coords.y1; $( 'upload_width' ).value = dimensions.width; $( 'upload_height' ).value = dimensions.height; } Event.observe( window, 'load', function() { new Cropper.Img('cropimage', { minWidth: 50, minHeight: 50, displayOnInit: true, onEndCrop: onEndCrop } ); } ); script> |
This attaches the jsCropperUI to the photo we want to edit, identified by the “cropimage” CSS ID. It also sets up a callback function to set our form fields with values based on the crop coordinates. Check the jsCropperUI documentation for more details.
Here’s what our page looks like:
There are two other things to be aware of here:
First, the “image_tag” is displaying the “:default” style of the uploaded image. If you look back to the Upload model code above, that means it’s an image that’s been scaled down to at most 500×400 pixels. This is important, since we don’t want to display a full-sized image if someone’s uploaded a giant photo from a 10 megapixel digital camera!
Second, we’re telling the form helpers to create form fields for attributes like “x1” and “width”, which aren’t attributes of our Upload model. To make this work, we need to add some virtual attributes to the model (virtual, because they’re not associated with any fields in our database table):
# models/upload/upload.rb attr_accessor :x1, :y1, :width, :height |
Now, when the form is submitted, the standard “update” action in the uploads controller will call “update_attributes”, just like any other form and controller:
# controllers/uploads_controller.rb def update @upload = Upload.find params[:id] if @upload.update_attributes params[:upload] flash[:notice] = 'Upload was successfully updated.' redirect_to @upload else render :action => "edit" end end |
To make this actually crop the image, we need to override the update_attributes method in the upload model:
# models/upload.rb require 'RMagick' def update_attributes(att) # Should we crop? scaled_img = Magick::ImageList.new(self.photo.path) orig_img = Magick::ImageList.new(self.photo.path(:original)) scale = orig_img.columns.to_f / scaled_img.columns args = [ att[:x1], att[:y1], att[:width], att[:height] ] args = args.collect { |a| a.to_i * scale } orig_img.crop!(*args) orig_img.write(self.photo.path(:original)) self.photo.reprocess! self.save super(att) end |
What’s going on here? We want to crop our original, high-resolution image, but we used a scaled down version in the jsCropperUI to select our crop rectangle. To adjust for that, we need to check the pixel width (“columns”) for the original and scaled images, and compute a scaling factor. Next we prepare the four arguments for the RMagick crop function, which expects x1, y2, width, and height. We create an array with the arguments, convert to integer (they’re still strings, since they came from a text field in the form), and scale them. We can pass multiple arguments to a method by prefacing an array with an asterisk. That’s something new I came across in this project, and I suspect may come in handy again some time. The image is cropped, and then written back to disk, replacing the original.
“reprocess!” is a Paperclip method to re-generate thumbnails and other scaled versions of an image from the original. And finally, we call the true “update_attributes” method to handle things like the “description” or other actual attributes of our model.
So there you have it… a framework to deal with multiple image uploads to an associated model, with user-selected cropping.
I’ve packaged up the example code into a GitHub repository. It’s a fully functional Rails 2.2.2 app, assuming you have the Paperclip gem and RMagick already installed.
Comments and improvements are welcomed.
Georges
February 18, 2009Many thanks to you! May your shoes fly over a golden pavement :)
Georges
February 19, 2009Tip: I added a “:dependent => :destroy” to the has_many declaration to have the pictures deleted (record and files) when editing a “container” and removing an image using the JS “remove” link.
James West
March 1, 2009Thank you so much. I’m having a play with your info right now!
James
James West
March 1, 2009Any ideas why I might be getting Unknown key(s): discard_if, attributes errors?
I assume that this is something to do with not installing attributes_fu properly but may be something else.
I REALLY want to get this working
:-)
James
James West
March 2, 2009Well, I sorted the above problem by downloading your bundled source code.
Still uncertain what caused the problem in the first place though.
Any chance of showing what happens to images if upload records are deleted?
Marcus Lopes
March 9, 2009Nice tutorial….
the method below is missing in the dog_controller but aside from that everything is perfect.
def edit
@dog = Dog.find params[:id]
end
thanks
Marcus
shree
March 12, 2009Hi,
I was not able to upload image using paperclip and so I uploaded images using attachment_fu plugin. Can anyone tell me how to implement above type of cropping using attachment_fu.The application allows the user to upload image and the user can crop the uploaded image as he wishes and then save.
Zoran
March 20, 2009Thanks for sharing. This is exactly what I was looking for!
Eric
March 20, 2009Great tutorial. Note to onlookers — if you have trouble with any of the functionality, download the app from GitHub (see link at bottom of article) to compare it’s code against yours to see if anything is missing or different. Note also that this tutorial assumes you’ve already installed Paperclip, ImageMagick, and RMagick. If you haven’t, you’ll need to do so before this will work. Even the app in GitHub doesn’t have the paperclip plugin included.
Eric
March 21, 2009I had a problem with the remove link resulting in a JavaScript error saying $(this).up is not a function. To fix this I had to change the application.html.erb in the sample app replacing the with just . Apparently the version of prototype in the cropper library was older and not compatible with the attribute_fu plugin.
I hope this helps somebody — I spent a couple hours figuring that one out.
paul
March 24, 2009nice writeup!
i’d use has_image as no rmagick is needed (http://github.com/norman/has_image/tree/master) and code all the “magick” issues on a command line basis.
Assume you’ll upload a few 10 meg images. No way to free this space using rmagick without server instance restart…
mike
April 14, 2009Thanks for all the comments. Now that Rails 2.3 supports nested model attributes, some of the above (e.g. attribute_fu) needs to change. I also, as Paul points out, need to move away from RMagick.
This was a proof-of-concept test, and for my real code I’ve integrated Uploadify to do uploads using Flash, giving real-time feedback via a progress bar. Once I’m done with everything I’ll post a new article with all the updates (Uploadify, no RMagick, no attribute_fu, etc.)
mike
April 14, 2009For those who are interested, I found the following similar solution for uploading with Paperclip and then cropping: http://github.com/jschwindt/rjcrop/tree/master
Pat
May 2, 2009Hi,
I have a question:
How do you paginate the images from attribute_fu or the @dogs_uploads?
Thanks
Jaume
May 27, 2009Anybody knows how can I show the dog image near the description in the _uploads partial form?
I don’t know which are the parameters for the image_tag helper. How i can get the url?
pedro
May 27, 2009Hi. First I’d like to thank you for this great tutorial. Im having trouble cropping my pictures through Imagemagick… this is the error that comes up:
Invalid JPEG file structure: missing SOS marker `C:/myfiles/InstantRails/rails_apps/dog/public/photos/3/original/termo1.jpg’ @ jpeg.c/EmitMessage/232
Do you have any idea of what this may indicate? How can I fix it?
Thanks
Rodigo
May 30, 2009Great Work!!! Simple and effective, perfect!
Nave
June 9, 2009Brilliant work…!! :)
J.
June 24, 2009Hi Mike,
are you still planning on write a how to using paperclip with uploadify? I was wondering how that is coming along, since that is the exact thing that I would need right now. Cant find anything else on the web…
Cheers J.
mike
June 27, 2009Unfortunately I don’t have the time right now to post another tutorial. What I’m doing in my project is using Paperclip, the Flash- and JQuery-based Uploadify (http://www.uploadify.com/), and jQuery’s Jcrop (http://deepliquid.com/content/Jcrop.html). It’s still a bit of a mess, but it works.
mike
February 22, 2010I’ve finished and launched my project using the image uploader/cropper, so I’m hoping to do another write-up some time soon. If I do, it will be on my new software development blog at http://thelastpixel.net/ instead of here on my personal blog. Please check there for updates.
Thanks for all the feedback!
Mike
kunalan
September 12, 2010Unknown key(s): discard_if, attributes errors? fixed
put the above line inside the Rails::Initializer.run do |config| scope in environment.rb
config.gem “paperclip”
Rodrigo
October 12, 2010Hey! thanks for this tutorial.
Im doing almost the same, but Im having an error: uninitialized constant FileColumn::Magick::ImageList
I have paperclip – not filecolumn – and i have exact code than you. But cant get this work.
What should this error be?
thanks
kubilan
October 15, 2010Hi, excellent post, but I’m having the hardest time trying to get it to work
C:/Rails-2.5-win/ruby/bin/mongrel_rails: No such file or directory – identify “C:/DOCUME~1/Owner/LOCALS~1/Temp/stream.3452.0”
Is it because I’m adding this code to the code from this tutorial: http://ezror.com/blog/index.shtml
Saran
September 10, 2013Hi friend,
I have tried your application. i got an bug.when i try to run user/new it shows the error message “undefined method `uploads’ “. i am a beginner in ruby.plz help me
Advance thanks