This article aims to provide a method of cropping images to the desired scale and uploading it via gem in Ruby on Rails

Cropping Image in Rails via Carrierwave and Cropbox

Specifications

As of this writing, the following versions and code are used:

Scope and Limitations

This post will provide steps on using Cropbox with Carrierwave only. Which means, that by following the steps, your carrierwave is expected to be properly working already.

Client Side Preparation

  1. Download the cropbox JS files needed. Then add it to your working project. Click here to download the files.

  2. Provide a UI for uploading and cropping of the image.

    In my case, I used a modal box triggered by clicking the image to be replaced. My form inside the modal looks like this:

    <div class="modal fade bs-example-modal-img" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
     <div class="modal-dialog modal-lg">
      <div class="modal-content">
       <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
         <span aria-hidden="true">×</span>
        </button>
        <h4 class="modal-title" id="exampleModalLabel">
         User Avatar
        </h4>
       </div>
    
        <div class="modal-body">
         <div class="form-group">
          <%= file_field_tag "user_avatar", :class => 'form-control',  :accept => 'image/png,image/gif,image/jpeg' %>
         </div>
         <div class="row form-set">
          <div class="col-md-6">
    
              <div class="imageBox" id="avatarBox">
                  <div class="thumbBox"></div>
                  <div class="spinner" style="display: none">
                   Loading...
                  </div>
              </div>
    
              <div class="action">
                  <input type="button" id="btnZoomIn" class="btnZoomIn avatar btn btn-default" value="+">
                  <input type="button" id="btnZoomOut" class="btnZoomOut avatar btn btn-default" value="-">
                  <input type="button" id="btnCrop" class="btnCrop avatar btn btn-default" value="Crop">
              </div>
          </div>
          <div class="col-md-6">
              <div class="cropped croppedAvatar">
                <!--PLACE DEFAULT IMAGE-->
                <image src="<%= @user.avatar.url %>" style="width:100%;" id="avatar-c" class="thumbnail" />
              </div>
              <div class="action">
                  <button type="button" class="btn btn-default pull-right" data-dismiss="modal" aria-label="Close">
                   Close
                  </button>
              </div>
          </div>
         </div>
        </div>
    
      </div>
     </div>
    </div>
    
    My image that triggers the modal goes like this:

    <image src="<%= @user.avatar.url %>" style="width:100%;" id="avatar" class="thumbnail" data-toggle="modal" data-target=".bs-example-modal-img" />
    

    NOTE: I've used Twitter Bootstrap for the modal.

  3. Provide a hidden field for storing the datafile.
    <%= hidden_field_tag :avatar_datafile %>
    

  4. I've added the following CSS styling just to make it appealing (optional)

    <style type="text/css">
    .imageBox{
        position: relative;
        height: 400px;
        width: 400px;
        border:1px solid #aaa;
        background: #fff;
        overflow: hidden;
        background-repeat: no-repeat;
        cursor:move;
    }
    .imageBox .thumbBox{
        position: absolute;
        top: 50%;
        left: 50%;
        width: 200px;
        height: 200px;
        margin-top: -100px;
        margin-left: -100px;
        border: 1px solid rgb(102, 102, 102);
        box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.5);
        background: none repeat scroll 0% 0% transparent;
    }
    .imageBox .spinner{
        position: absolute;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        text-align: center;
        line-height: 400px;
        background: rgba(0,0,0,0.7);
    }
    .cropped{
     border: 1px solid #d3d3d3;
     height: 400px;
     width: 400px;
     vertical-align:  middle;
     text-align: center;
    }
    .action{
     text-align: center;padding-top: 5px;
    }
    </style>
    

  5. Add the following scripts to your code. These should handle all events for you including calling the Cropbox JS.

    <script type='text/javascript'>
     $(window).load(function() {
    
         var options =
         {
             thumbBox: '.thumbBox',
             spinner: '.spinner',
             imgSrc: '/assets/default.png'
         }
    
         var cropper;
    
      $("#user_avatar").on('change', function(){
             var reader = new FileReader();
             reader.onload = function(e) {
                 options.imgSrc = e.target.result;
                 cropper = $('#avatarBox').cropbox(options);
             }
             reader.readAsDataURL(this.files[0]);
             this.files = [];
         });
    
    
         // Crop handler.
         $('.btnCrop').on('click', function(){
          var img = cropper.getDataURL();
    
          // Place the cropped image's datafile.
             $('.croppedAvatar').html('<img src="'+img+'" width="100%">');
    
             // Place it to the default image. The one that triggers the modal.
             $('#avatar').attr('src', img);
    
             // Place the datafile value in the hidden field
      $('#avatar_datafile').val(img);       
         });
    
         $('.btnZoomIn').on('click', function(){        
          cropper.zoomIn();         
         });
    
         $('.btnZoomOut').on('click', function(){         
          cropper.zoomOut();         
         });
    
     });
    
    </script>
    


Server Side Preparation

  1. Now it's time to handle it on the server side. Start with adding the data conversion from data-image to ActionDispatch::Http::UploadedFile

    NOTE: This is based on Dave Hullihan's code which I revised a little allowing to convert a data-file string as it is other than passing it as an object.

    On the application_controller.rb, add the following:

    # Split up a data uri
    def split_base64(uri_str)
      if uri_str.match(%r{^data:(.*?);(.*?),(.*)$})
        uri = Hash.new
        uri[:type] = $1 # "image/gif"
        uri[:encoder] = $2 # "base64"
        uri[:data] = $3 # data string
        uri[:extension] = $1.split('/')[1] # "gif"
        return uri
      else
        return nil
      end
    end
    
    # Convert data uri to uploaded file. Expects object hash, eg: params[:post]
    def convert_data_uri_to_upload(data_uri)
      if !data_uri.blank?
        image_data = split_base64(data_uri)
        image_data_string = image_data[:data]
        image_data_binary = Base64.decode64(image_data_string)
    
        temp_img_file = Tempfile.new("data_uri-upload")
        temp_img_file.binmode
        temp_img_file << image_data_binary
        temp_img_file.rewind
    
        img_params = {:filename => "data-uri-img.#{image_data[:extension]}", :type => image_data[:type], :tempfile => temp_img_file}
        uploaded_file = ActionDispatch::Http::UploadedFile.new(img_params)
    
        return uploaded_file
      end
    
      return nil
    end
      

    The methods above will accept data URI and convert them to ActionDispatch objects which the carrierwave accepts.

  2. Remember the hidden fields we placed on the client-side? This will be used and processed.

    Just convert this data URI (see the previous step) and pass this to your uploader attribute.

    @user.avatar = convert_data_uri_to_upload(params[:user_avatar])
    

    Then proceed with your saving.


A brief explanation


The Cropbox generates data-image which is a string. Unfortunately, carrierwave accepts the usual ActionDispatch::Http::UploadedFile only.

In that case, the data-image needs to be converted into such object so that carrierwave can accept it. Basically, it means decoding the data uri which is encoded on base64 and wrapping it into an object handled by carrierwave.

That's it. Save everything and then proceed with testing!

No comments :

Post a Comment