We all have been using the same code for uploading images for years, but didn't you always feel that there is something wrong with it? For every other task like writing texts, picking a date, selecting from lot of choices we have a good tool that can help in implementing such feature and improve the user experience, but file uploads almost always feel a little broken. There are some Flash tools that might help, but they are still not good enough.

Solution

Well, welcome to the world invaded by Filepicker and Aviary. Speaking short, Filepicker is a tool that let the user upload images not only from the computer itself but also from web services such as Facebook or Dropbox. Aviary provides you with a powerful HTML5 editor for manipulating photos. Both of them process the images on their servers and provide you an url for downloading a file. If you only need more powerful uploading you can stick with Filepicker widgets otherwise we need to get our hands dirty with their Javascript APIs (or CoffeeScript as you will see) but is not hard at all.

Working with code

Let's start with the view:

<%= link_to _("Set avatar"), "#", :'data-avatar' => "set" %%>
<a href="#" data-avatar="set">Set avatar</a>

Nothing fancy here. Classic Rails link_to method, using _('') method for translating with FastGettext. We don't care about URL because we are going to handle clicks in Javascript so I used "#" as URL. Instead of using css classes or id for such link I prefer to use custom data-* attribute

First, we need to display Filepicker popup for choosing image when our link is clicked.

filepicker = window.filepicker
filepicker.setKey "filepicker api key"
$(document).ready ->
  $('body').delegate '[data-avatar="set"]', 'click', ->
    images = filepicker.MIMETYPES.IMAGES
    filepicker.getFile images, (url, metadata) ->
      console.log("Choosen image url: #{url}")

I use jQuery delegate because if it was a Single Page Application (SPA Todo app example) or the link is dynamically added via AJAX, it can still be properly handled.

After clicking the user needs to give permission for using data from a service or simply upload file from computer, or even take a photo using computer built-in camera.

It's time now to run the photo editor when the file is picked instead of just using console.log.

Create instance of the editor:

featherEditor = new Aviary.Feather(
  apiKey: "key"
  apiVersion: 2
  onSave: (imageID, newURL) ->
    featherEditor.close()
    return false
)

Use it when file picked:

images = filepicker.MIMETYPES.IMAGES
filepicker.getFile images, (url, metadata) ->
  preview = $('[data-avatar="preview"]')[0]
  preview.src = url
  featherEditor.launch
    image: preview
    url: url

When user finishes editing the photo and presses “Save” button, onSave callback is executed. You can save the url value in JS variable or use it to fill some hidden field in a form or send it to the server. However the documentation states that “this image may not yet be ready so you will have to poll this link, or alternatively handle the hi-res image server-side”. This is my biggest disappointment when using those two products. For that reason we are going to use postUrl option so that Aviary will send us a request to this given URL when the image is ready. Obviously you will have to use different value of the setting for development, staging and production environment. In development you can either forward some port from your router (I assume it is publicly available) to your computer or alternatively, if have a server you can use ssh to forward traffic from the server to your local machine.

Forwarding ports with ssh:

ssh user@YOUR_SERVER_IP -R YOUR_SERVER_IP:SOME_SERVER_PORT:127.0.0.1:3000

(Update) Alternatively you can use a solution that does it for you and does not require having custom server. Avdi did a really nice research of http forwarding tools

Launching the editor with postUrl:

featherEditor.launch
  image: preview
  url: url
  postUrl: "http://YOUR_SERVER_IP:SOME_SERVER_PORT/aviary"

Let's see the controller that is used when Aviary notifies us of the ready image:

class AviaryController < ApplicationController
  skip_before_filter :verify_authenticity_token, only: [:create]

  def create
    @user = User.last
    @user.remote_avatar_url = params[:url]
    @user.save!
    head :created
  end
end

Find user, set avatar url and save. As simple as that. Where does remote_avatar_url setter comes from ? It is a feature of carrierwave library that I use to store and resize avatars. It can download the remote avatar itself so I do not need to bother myself with that. You can use it with RMagick, mini_magick or vips.

class User < ActiveRecord::Base
  mount_uploader :avatar, AvatarUploader
end
class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::RMagick
  # include CarrierWave::MiniMagick
  # include CarrierWave::Vips

  include Sprockets::Helpers::RailsHelper
  include Sprockets::Helpers::IsolatedHelper

  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def default_url
    asset_path "avatar/default.png"
  end

  version :large do
    process :resize_to_fit => [256, 256]
  end
  version :medium do
    process :resize_to_fit => [128, 128]
  end
  version :small do
    process :resize_to_fit => [64, 64]
  end
end

The default_url is used when avatar is not set. That way User#avatar method can nicely behave as Null Object. Avdi blogpost about Null Objects is definitely worth reading.

But we don't want anyone to be capable to send requests to our application and change avatars, do we ? We need to add some protection. And we need to know which user avatar should be changed. Every user will have its own token for updating the avatar. Again, we use custom data-* (exactly data-avatar-token) attribute to store the token in HTML.

<%= link_to _("Set avatar"), "#", :'data-avatar' => "set", :'data-avatar-token' => AvatarToken.new(current_user).token, :'data-avatar-id' => current_user.id %%>
<a href="#" 
   data-avatar="set"
   data-avatar-token="akzlEoaW9WhV8djhtWJmCLd9vjQ="
   data-avatar-id="1">Set avatar
</a>

We use postData to store additional metadata that should come with the request from Aviary to our App.

$(document).ready ->
  $('body').delegate '[data-avatar="set"]', 'click', ->
    self  = $(this)
    token = self.attr('data-avatar-token')
    id    = self.attr('data-avatar-id')
    images = filepicker.MIMETYPES.IMAGES
    filepicker.getFile images, (url, metadata) ->
      preview = $('[data-avatar="preview"]')[0]
      preview.src = url
      featherEditor.launch
        image: preview
        url: url
        postUrl: "http://IP:PORT/users/#{id}/avatar"
        postData:
          token: token

Now we can use this data in our controller to verify the request:

class Users::AvatarsController < ApplicationController
  skip_before_filter :verify_authenticity_token, only: [:create]

  def create
    @user    = User.find(params[:user_id])
    postdata = JSON.parse(params[:postdata]) rescue {}
    token    = postdata['token']
    AvatarToken.new(@user).verify! token
    @user.remote_avatar_url = params[:url]
    @user.save!
    head :created
  end
end

And this leaves us with the implementation of AvatarToken class.

require 'openssl'
require 'base64'

class AvatarToken
  class Invalid < StandardError; end

  attr_reader :user
  delegate :login, to: :user

  def initialize(user)
    @user      = user
    @algorithm = OpenSSL::Digest::Digest.new('sha1')
  end

  def verify!(another_token)
    unless token == another_token
      msg = "invalid '#{another_token}' token for user '#{login}'"
      raise Invalid, msg
    end
  end

  def token
    digest = OpenSSL::HMAC.digest(@algorithm, key, id)
    Base64.strict_encode64(digest)
  end
end

What do you compute digest of ? Well, that depends on your application.

One more thing that I would like to do is to always get square images from Aviary. I could not find 100% reliable way of doing that. My trick is to allow users to use only one type of crop ratio and show the crop tool as initial one. However, the user can still press “Cancel” unfortunately.

featherEditor.launch
  cropPresets: ['1:1']
  initTool: 'crop'

So the whole JS part looks like this:

filepicker = window.filepicker
filepicker.setKey "Filepicker API Key"
featherEditor = new Aviary.Feather(
  apiKey: "Aviary API Key"
  apiVersion: 2
  onSave: (imageID, newURL) ->
    featherEditor.close()
    return false
)

$(document).ready ->
  $('body').delegate '[data-avatar="set"]', 'click', ->
    self  = $(this)
    token = self.attr('data-avatar-token')
    id    = self.attr('data-avatar-id')
    images = filepicker.MIMETYPES.IMAGES
    filepicker.getFile images, (url, metadata) ->
      preview = $('[data-avatar="preview"]')[0]
      preview.src = url
      featherEditor.launch
        image: preview
        url: url
        postUrl: "http://IP:PORT/users/#{id}/avatar"
        postData:
          token: token
        fileFormat: 'png'
        cropPresets: ['1:1']
        initTool: 'crop'
        onError: (errorObj) ->
          alert(errorObj.message + errorObj.code)

Few more notes about good and bad parts of this solution:

Pro:

  • The concepts behind Filepicker and Aviary are amazing and I believe they will change the web. It's like 'Editor as a Service', 'Picker as a Service'. What else could be a service ? I would love to use Gliffy editor in my app the same way I use Aviary.

  • Filepicker can store files directly in S3 so you do not have to keep them. I just prefer to have them on my machine.

  • Javascripts are available via HTTPS links.

Cons:

  • When using filepicker the user accepts filepicker.io application when connecting to Facebook or Dropbox, not our own application. This might be also considered a good thing if you did not connected your App with Facebook, but I would prefer if the widgets asks for permissions for my app. However I am not sure if that would be possible at all.

  • You cannot force Aviary to provide image in one ratio.

  • You cannot download from Aviary the image in different resolutions. The workaround is to upload it again to Filepicker and download converted. Too much hassle for me. It was just easier to this step on our server.

  • Both services ask you to link directly to their Javascript files instead of downloading them and using in your asset pipeline solution. So there are going to be additional HTTP requests when loading the page. But the good side is that if they fix some bug or improve the editor, the changes will be automatically available to your users with you deploying your app again.

  • After save, the photo URL from Aviary is not available immediately. This presents a huge UI problem. What should I show to my user after setting new avatar when I might not yet have a new avatar image to display ? Even after refresh of the page the new avatar might not yet be ready if the server is still waiting for a request from Aviary.

  • Aviary is not doing exponential backoff. It sends the request to your server only once. The game is over if you failed to handle it. (Sidenote: if you ever need to implement exponential backoff strategy in Ruby or Rails, check exponential-backoff gem.

  • The full list of Aviary translations is not bad but it is still missing few important ones for me like Greek or Turkish (forgive me my Eurocentrism).

  • You cannot change the language after initial Aviary configuration. Single Page Applications that are capable of changing language without reloading the page probably need to create a new instance every time they want to use Aviary editor instead of calling launch method multiple times on one object.

  • Filepicker does not allow you to choose any translation.

comments powered by Disqus