Image handling is a common task in an application and for Ruby on Rails we have quite a few plugins like rmagick, minimagick, paperclip, attachment-fu etc available to effectively do the same. Paperclip is by far the most loved and popular plugin used to attach images to a model in a rails application. It makes life easy with good documentation and screencasts available to guide through. But at an advanced level, the challenge comes in when an api interface to upload and download images needs to be provided for the model. As paperclip does not support this by iteself, and there is not much help or guide available to help solve this problem, this post makes life easier for ROR api developers, who have to provide a way to attach images sent through xml to the model.
Lets start with a model person that has an image attachment photo. The model will require the following fields in the migration.
add_column :persons, :photo_content_type, :string
add_column :persons, :photo_file_size, :integer
add_column :persons, :photo_updated_at, :datetime
/app/models/user.rb
has_attached_file :photo
validates_attachment_presence :photo
validates_attachment_content_type :photo, :content_type => ['image/jpeg', 'image/png']
end
We won’t be going through the HTML views here, as this is already well documented and available. We are more concerned with the API point of view, so here’s how the controller’s create method would have been if xml responds to is not yet introduced…
@person = Person.create(params[:person])
respond_to do |format|
if @person.save
flash[:notice] = 'Person was successfully created.'
format.html { redirect_to(@person) }
format.xml { render :xml => @person, :status => :created, :location => @person }
else
format.html { render :action => "new" }
format.xml { render :xml => @person.errors, :status => :unprocessable_entity }
end
end
end
Though, we can observe the xml render statements present there, this isn’t quite going to work for us. Well to make it work first we need to decide how do we specify the images in the xml document ?
one way, is offer a separate URL endpoint that allowed direct uploading for just the one image. But this solution requires more than one HTTP call to create or update the model attributes and also will require as many as the number of images in the model, which is not very efficient.
Another way is to embed the image in the xml, where the image is encoded as the base64 encoded string. So the xml will be of this format
<root>
<person>
<name>Nisha Singhvi</name>
</person>
<!-- specify photo element outside the person element, because photo is string type, while the person model -->
&<!--expects Paperclip::Attachment type; hence need to do manipulations before the photo can be assigned to the model's photo attribute -->
<photo_file_name>my_photo.jpg</photo_file_name>
<photo type = "String">
<!--base-64 encoded string-->
</photo>
</root>
when this xml is sent to the create method we need to decode the encoded string in a temporary image file and then assign this file object to the model’s paperclip::Attachment object. The modified create method will now be
respond_to do |format|
if request.format.xml?
#build other attributes of the person model
@person = Person.new(params[:root][:person])
#xml file contains photo as base64 encoded string
unless params[:root][:photo].blank?
temp = params[:root][:photo]
#decode the base64 encoded string and store it in the temp file name
#provided in params[:root][:photo_file_name]
File.open(params[:root][:photo_file_name],"wb") do |file|
file.write(ActiveSupport::Base64.decode64(temp))
end
#open the temp file created and assign it to the paperclip::Attachment object - photo - of Person
f = File.open(params[:root][:photo_file_name])
@person.photo = f
#delete the temp file created
File.delete(params[:root][:photo_file_name])
end
end
@person = Person.new(params[:person]) if request.format.html?
if @person.save
flash[:notice] = 'Person was successfully created.'
format.html { redirect_to person_path(@person) }
format.xml { render :xml => @person, :status => :created, :location => @person }
else
format.html { render :action => 'new' }
format.xml { render :xml => @person.errors, :status => :unprocessable_entity }
end
end
end
Having done the create method you might be wondering how to render these photos back to the client application. Now to get the image for the api request, again just rendering the model object will just give the database columns i.e photo_file_name, photo_content_type, photo_file_size, photo_updated_at but the actual image will be missing.
@person = Person.find(params[:id])
respond_to do |format|
format.html
format.xml(render :xml => @person, :status => :ok)
end
end
Here again we have two options…
One is to render the path and url of the variable by building the xml using builder.
/app/views/person/show.xml.builder
xml.person{
xml.id(@person.id)
xml.name(@person.name)
xml.path(@person.photo.path)
xml.url(@person.photo.url)
xml.file_name(@person.photo_file_name)
}
But this again is not very efficient, as the url and path are relative to the server address, which in turn raises security issues.
The second option is to embed the image as the base-64 string and send it in the xml file by building the xml
/app/controllers/persons_controller.rb
@person = Person.find(params[:id])
if request.format.xml?
@photo = ActiveSupport::Base64.encode64(open(@person.photo.path) { |io| io.read }) unless @person.photo.url == "/photos/original/missing.png"
end
respond_to do |format|
format.html
format.xml
end
end
/app/views/person/show.xml.builder
xml.person{
xml.id(@person.id, :type => "integer")
xml.name(@person.name, :type => "string")
xml.photo_file_name(@person.photo_file_name, :type => "string")
xml.photo_content_type(@person.photo_content_type, :type => "string")
xml.photo(@photo, :type => "string")
}
So here we have images downloaded and uploaded to a model via xml for the purpose of API. This should also apply the same way to api using json too. Please feel free to leave your suggestions as comments.
Tags: attachment, paperclip, rails, rails api, ror, ruby, upload images, xml


Thanks! I was looking for something like this. Working with base64 encoded images is a great idea! Since this is something that will be needed more and more in the future I’ll look into paperclip and see if there are options to make this work better out of the box. Thanks again, matthias
Thanks for the post. I was dealing with this same situation, but I was using Base64 instead of ActiveSupport::Base64 — Not sure the difference, but I’m no longer getting nil#unpack errors.