Abhijat M

Abhijat M

Tuesday, 6 August 2013

:: Rails Cropping Demystified ::

Design choice:
Solution1
Carrierwave, S3, Fog, Jquery File Upload, Jcrop + RMagick / MiniMagick

Advantage Server Side:

  1. Support for all browser versions
  2. Single stream file upload and can be coupled with resque for heavy loading.


OR

Solution2
S3,  Jquery File Upload, HTML5  cropping + CORS

Advantage client side and CORS Upload

  1. More compute optimized
  2. Server band-with as files are directly sent to S3
  3. Best suited to multiple file uploads.


Solution 1: Server side cropping
Setup:
Carrierwave has an excellent readme guide on how to install and setup these are the important bits:

GemFile:
gem 'carrierwave'
gem "fog"
gem "rmagick"

Initializer carrierwave.rb (Constants as defined in config.yml)
CarrierWave.configure do |config|
  config.fog_credentials = {
    :provider               => 'AWS',                        # required
    :aws_access_key_id      => APP_CONFIG['s3']['aws_access_key_id'],                        # required
    :aws_secret_access_key  => APP_CONFIG['s3']['aws_secret_key'],                        # required
    :region                 => 'us-east-1',                  # optional, defaults to 'us-east-1'
    # :host                   => 's3.domain.com',             # optional, defaults to nil
    # :endpoint               => 'https://s3.domain.com:8080' # optional, defaults to nil
  }
  config.fog_directory  = APP_CONFIG['s3']['bucket_name']                      # required
  config.fog_public     = false                                   # optional, defaults to true
  config.fog_attributes = {'Cache-Control'=>'max-age=315576000'}  # optional, defaults to {}
end
Uploader Model: 
  storage :fog setup for s3 done!
class UserAvatar < CarrierWave::Uploader::Base
  storage :fog
  include CarrierWave::RMagick

  version :thumb do
  process :manualcrop
  process :resize_to_fill => [200,200]

  end
  version :large do
    process :manualcrop
    process :resize_to_fit => [800, 800]
  end

  def extension_white_list
    %w(jpg jpeg gif png)
  end

 def manualcrop
 return unless model.cropping?
 manipulate! do |img|
   img = img.crop(model.crop_x.to_i,model.crop_y.to_i,model.crop_w.to_i,model.crop_h.to_i)
 end
 end

  def default_url
    ActionController::Base.helpers.asset_path([version_name, "default.png"].compact.join('_'))
  end
  
    def cache_dir
    "#{Rails.root}/tmp/uploads"
  end
end

Important Bits in the User Model
class User < ActiveRecord::Base
  
attr_accessor  :crop_x, :crop_y, :crop_h, :crop_w

   mount_uploader :avatar,UserAvatar

  after_update :reprocess_profile, :if => :cropping?

  def cropping?
    !crop_x.blank? && !crop_y.blank? && !crop_w.blank? && !crop_h.blank?
  end

  def profile_geometry
    img = Magick::Image::read(self.avatar.url).first
    {:width => img.columns, :height => img.rows }
  end

private

  def reprocess_profile
    self.avatar.recreate_versions!
  end

Controller:
  def crop_photo
    user = current_user
    user.crop_x = params[:settings]["crop_x"]
    user.crop_y = params[:settings]["crop_y"]
    user.crop_h = params[:settings]["crop_h"]
    user.crop_w = params[:settings]["crop_w"]
    user.save
    current_user.save
    redirect_to my_fav_path
  end
View Page:

Crop / Adjust your new picture

<%= image_tag(current_user.photo_url(:large), :id => "target", :alt => "Avatar") %>

Preview

<%= image_tag(current_user.photo_url(:large), :class => "jcrop-preview", :alt => "Preview") %>
<%= form_for :settings, :url => crop_photo_settings_path do |f| %> <% for attribute in [:crop_x, :crop_y, :crop_h, :crop_w] %> <%= f.hidden_field attribute, :id => attribute %> <% end %>
<%= button_tag "Crop", :class => 'submit' %>
<% end %>
Alternative Approach

As an alternate to using RMagick gem you can use MiniMagick gem and in that case the Uploader Model method to crop can change to
 def manualcrop
 return unless model.cropping?
    manipulate! do |img|
      size = model.crop_w << 'x' << model.crop_h
      offset = '+' << model.crop_x << '+' << model.crop_y
      img.crop("#{size}#{offset}") # Doesn't return an image...
      img = yield(img) if block_given?
      img #
    end
  end
And the image geometry can be fetched using
  def profile_geometry
    MiniMagick::Image.open(self.avatar.url)[:width]
  end



Solution 2: 
Client Side Cropping using HTML5 Canvas.

This is the alternative solution for re-sizing images and uploading them using Jquery File Uploader















File Uploader:
$('#fileupload').fileupload({
    url: '//jquery-file-upload.appspot.com/',
    dataType: 'json',
    // Enable image resizing, except for Android and Opera,
    // which actually support image resizing, but fail to
    // send Blob objects via XHR requests:
    disableImageResize: /Android(?!.*Chrome)|Opera/
        .test(window.navigator && navigator.userAgent),
    imageMaxWidth: 800,
    imageMaxHeight: 800,
    imageCrop: true // Force cropped images
})
https://github.com/blueimp/jQuery-File-Upload/wiki/Client-side-Image-Resizing


In order to upload directly to S3 you can use CORS - Cross Origin Resource Sharing Cross domain uploads A live Example and usage of this solution can be found here: Direct Upload S3 Jquery File Upload




Now that I have listed both approaches here are few design decisions worth considering..

Enjoy!!



References:
http://railscasts.com/episodes/182-cropping-images-revised
http://stackoverflow.com/carrierwave-crop-specific-version