Saturday, 11 October 2014

Crop, zoom and rotate image using carrierwave

Crop, zoom and rotate image using carrierwave


We already have a good reading about cropping in rails using jcrop from ryanbates railscast (http://railscasts.com/episodes/182-cropping-images?view=asciicast). But in my recent rails project I need functionality to rotate and crop zoomed image. So I made following changes that might help others.

## My original image :-



First of all we need carrierwave to upload images and mini_magick to manipulate images. So add following gems in your Gemfile.

##/Gemfile

gem 'carrierwave'
gem 'mini_magick'

To crop, zoom and rotate image we need to add 'cropzoom' file and 'custom.min' file in assets. Find here https://github.com/cropzoom/cropzoom

Following assetes changes required :

##/app/assets/javascripts/application.js

//= require jquery.cropzoom
//= require jquery-ui-1.10.3

##/app/assets/stylesheets/application.css

*= require jquery-ui-1.10.3.custom.min
*= require jquery.cropzoom

Now, to get coordinates of crop, zoom or rotated image we need to set attr_accessor for :
Cropped image coordinates : crop_x, crop_y, crop_w, crop_h
Zoomed image coordinates : zoom_x, zoom_y, zoom_w, zoom_h
Dragged image coordinates : drag_x, drag_y
Rotation angle : rotation_angle

So, my model looks like this :- 

/app/models/user.rb

class User < ActiveRecord::Base

mount_uploader :image, ImageUploader
attr_accessor :crop_x, :crop_y, :crop_w, :crop_h, :rotation_angle, :zoom_w, :zoom_h, :zoom_x, :zoom_y, :drag_x, :drag_y

Changes required in image crop view.


##/app/views/users/crop.html.erb

# Hide the original image.

<%= image_tag @user.image.url, class: 'img-responsive hide_img' %>

# Container where we will crop, zoom and rotate image
<div id='crop_container'></div>

<% form_for @user do |f| %>
  <% for attribute in [:zoom_w, :zoom_h, :zoom_x, :zoom_y, :drag_x, :drag_y, :rotation_angle, :crop_x, :crop_y, :crop_w, :crop_h] %>
    <%= f.hidden_field attribute, :id => attribute %>
  <% end %>
  <p><%= f.submit "Crop" %></p>
<% end %>

# Initialize cropzoom container

<script>
  $(document).ready(function() {
    var cropzoom = $('#crop_container').cropzoom({ 
      width:300,
      height:300,
      bgColor: '#CCC',
      enableRotation:true,
      enableZoom:true,
      zoomSteps:10,
      rotationSteps:10,
      selector:{
        centered:false,
        startWithOverlay: true,
        borderColor:'blue',
        borderColorHover:'yellow'
      },
      image:{
        source:$('.img-responsive').attr('src'),
        width:768,
        height:768,
        minZoom: 20,
        onRotate: function(imageObject, rotate_angle){
          // Get rotatation angle
          $("#rotation_angle").val(rotate_angle);
        },
        onZoom: function(imageObject, dimensions){
          // Get zoom coordinates
          $('#zoom_w').val(dimensions.w);
          $('#zoom_h').val(dimensions.h);
          $('#zoom_x').val(dimensions.posX);
          $('#zoom_y').val(dimensions.posY);
          // Set drag coordinates to zero when image is zoomed
          $('#drag_x').val(0);
          $('#drag_y').val(0);
        },
        onImageDrag: function(imageObject, position){
          // Get dragged image coordinates
          $('#drag_x').val(position.posX);
          $('#drag_y').val(position.posY);
          // Set zoom x-y coordinates to zero when image is dragged
          $('#zoom_x').val(0);
          $('#zoom_y').val(0);
        }
      }
    });
  });
  
  // Get cropped image coordinates

  $(document).on('click', 'input[type="submit"]', function() {
    var get_html = $('#infoSelector').html()
    var get_array = get_html.split('|')
    var get_x_y_coords_array = get_array[0].split('-')
    var get_w_h_coords_array = get_array[1].split('-')
    var get_x_coord = get_x_y_coords_array[0].split(':')[1]
    var get_y_coord = get_x_y_coords_array[1].split(':')[1]
    var get_w_coord = get_w_h_coords_array[0].split(':')[1]
    var get_h_coord = get_w_h_coords_array[1].split(':')[1]
    $('#crop_x').val(get_x_coord);
    $('#crop_y').val(get_y_coord);
    $('#crop_w').val(get_w_coord);
    $('#crop_h').val(get_h_coord);
  });
</script>

Crop view looks like this :-




Note :- In documentation of cropzoom plugin 'onImageDrag()' shuold have two values, but it have only one. Line no. 182 in my case

So, We need to change in jquery.cropzoom.js.

##/app/assets/javascripts/jquery.cropzoom.js

Change in $($image).draggable() function.

Change :- 
  if ($options.image.onImageDrag != null)
    $options.image.onImageDrag($image);
To :-
  if ($options.image.onImageDrag != null)
  $options.image.onImageDrag($image, getData('image'));

Required controller changes.

##/app/controllers/users_controller.rb

@user = User.find(params[:id])

#If we get image from remotely
@user.remote_image_url = User.first.image

if @user.update_attributes(params[:user])
  if params[:user][:crop_x].present?
    @user.image = @user.image.resize_and_crop
    @user.save!
    @user.image.recreate_versions!
  end
end

##/app/assets/stylesheets/custom.css

// To hide image 

.hide_img {
  display: none;
}

Now, finally changes are required in carrierwave uploader.

##/app/uploaders/image_uploader.rb

include CarrierWave::MiniMagick

version :custom_crop do
  process :resize_and_crop
end

def resize_and_crop
  if model.class.to_s == "User"
    if model.crop_x.present?
      manipulate! do |img| 
        w = model.crop_w.to_i
        h = model.crop_h.to_i
        
        // Set x-y coordinates of cropped image.
        x = model.zoom_x.to_i >= 0 ? (model.crop_x.to_i - model.zoom_x.to_i) : (model.zoom_x.to_i.abs + model.crop_x.to_i)
        y = model.zoom_y.to_i >= 0 ? (model.crop_y.to_i - model.zoom_y.to_i) : (model.zoom_y.to_i.abs + model.crop_y.to_i)
        x = model.drag_x.to_i >= 0 ? (x - model.drag_x.to_i) : (model.drag_x.to_i.to_i.abs + x) 
        y = model.drag_y.to_i >= 0 ? (y - model.drag_y.to_i) : (model.drag_y.to_i.to_i.abs + y) 

        img.combine_options do |i|
          // First we need to resize image with zoomed image. For more details you can find here "https://github.com/minimagick/minimagick"
          i.resize "#{model.zoom_w.to_i}x#{model.zoom_h.to_i}+#{model.zoom_x.to_i}+#{model.zoom_y.to_i}^\!"
          // Rotate zoomed image
          i.rotate(model.rotation_angle.to_i)
          // Crop zoomed and rotated image
          i.crop "#{w}x#{h}+#{x}+#{y}"
        end
        img
      end
    end
  end
end


## Final output is :-


2 comments:

  1. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete
  2. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete