← Back to all posts

Crop photos with Rails, Paperclip, and cropperjs

David Anderson | May 07, 2019

In a previous post, I shared how I integrated the Jcrop jQuery library to offer a cropping interface on the frontend which updated hidden fields in a form that was submitted to our controller to crop the photo using the paperclip gem. The below tutorial is essentially the same, but in this case we’ll use the cropperjs library’s jQuery plugin rather than JCrop.

In this case, we’ll try to do something like this: 1. Add cropperjs to our app and add crop_images routes 2. Get cropperjs working on our page 3. Submit cropperjs coordinates to our server 4. Use coordinates submitted to crop photo using paperclip

1. Basic Setup

I’m going to assume you have a User model and that you’ve already successfully setup paperclip. It should look something like this:

# app/models/user.rb

class User < ActiveRecord::Base
  has_attached_file :photo,
    styles: {full: '512x512>', thumb: '256x256#'},
    default_url: "/:class/:attachment/default.png"
end

Download (or copy and paste the contents of the files into your files) the cropperjs library from here, the cropperjs CSS file, and the cropperjs jquery-plugin from here. Then add the cropper.js, jquery-cropper.js, and cropper.css files to your app’s vendor directory. The js files will go in vendor/assets/javascripts/cropper.js and vendor/assets/javascripts/jquery-cropper.js. The CSS file should go here vendor/assets/stylesheets/cropper.css. Now we need to add those assets to our asset manifest files.

// app/assets/javascripts/application.js

//= require cropper.js
//= require jquery-cropper.js
/* app/assets/stylesheets/application.css */

*= require cropper

Let’s add routes for the pages where a user can crop an image.

# config/routes.rb

resources :users do
  resources :crop_images, only: [:new, :create]
end

2. Get Jcrop working

First we need to create our CropImagesController and the crop_images_controller#new view. We’re using Bootstrap so our view will have some familiar layout classes.

# app/controllers/crop_images_controller.rb

class CropImagesController < ApplicationController
  def new
    @user = User.find(params[:user_id])
  end
end
// app/views/crop_images/new.html.haml

.container
  .row
    .col-md-12
      %h3.m-b-md
        = t('.crop_image')
      - if @user.photo.exists?
        .row
          .col-md-9
            #spinner
            #crop-stage.m-b-sm{style: 'max-height: 500px;'}
              = image_tag @user.photo.url(:auto_orient), id: 'crop-image', style: 'max-width:none;max-height:none;'
            .crop-buttons
              .btn-group
                %button.btn.btn-default{data: {method: 'rotate', option: '-90'}, title: 'Rotate Left'}
                  %i.fa.fa-rotate-left
                %button.btn.btn-default{data: {method: 'rotate', option: '90'}, title: 'Rotate Right'}
                  %i.fa.fa-rotate-right
            = form_tag crop_images_url do
              = hidden_field_tag :redirect_path, params[:redirect_path]
              = hidden_field_tag :object_id, @user.id
              = hidden_field_tag :coords_x, nil, id: 'coords-x'
              = hidden_field_tag :coords_y, nil, id: 'coords-y'
              = hidden_field_tag :coords_h, nil, id: 'coords-h'
              = hidden_field_tag :coords_w, nil, id: 'coords-w'
              = hidden_field_tag :coords_rotate, nil, id: 'coords-rotate'

              = submit_tag t('aaa.rest.save'), class: 'btn btn-primary m-h-md', data: {disable_with: "..."}
      - else 
        %p.text-danger.m-b-lg
          = t('.user_does_not_have_image_to_crop')

In our view we check to make sure the user has a photo and display a message if he/she doesn’t. If the user does have a photo, then we show the original image using image_tag and give the image an ID of #crop-image. I’ve added some inline styles to set the max-height.

We’ve also added a form to the page which will submit to the crop_images_controller#create action when the user hits save. The form has hidden fields for the cropping coordinates and the rotation, initially, and those are the fields we’ll set in our javascript.

Add a new file to app/assets/javascripts (you’ll have to add this file to your application.js manifest file if you don’t use //= require_tree .).

// app/assets/javascripts/image_cropper.js

ImageCropper = function(elementId) {
  var self = this;

  var $image = $(elementId);

  var updateCoords = function(event) {
    $('#coords-x').val(event.detail.x);
    $('#coords-y').val(event.detail.y);
    $('#coords-w').val(event.detail.width);
    $('#coords-h').val(event.detail.height);
    $('#coords-rotate').val(event.detail.rotate);
  }

  $image.cropper({
    viewMode: 1,
    aspectRatio: 1,
    movable: false,
    scaleable: false,
    zoomable: false,
    zoomOnTouch: false,
    zoomOnWheel: false,
    crop: updateCoords,
  })

  var clickCropButtonsListener = function() {
    $('.crop-buttons').on('click', '[data-method]', function() {
      $image.cropper('rotate', $(this).data('option')); 
    });
  }

  self.initListeners = function() {
    clickCropButtonsListener();
  }
}

$(document).ready(function() {
  if ( $('#crop-image').length > 0 ) {
    cropper = new ImageCropper('#crop-image');
    cropper.initListeners();
  }
});

Our ImageCropper class initializes cropper with a few options. We’re setting the aspectRatio to 1 and disabling the ability move, scale, and zoom in on the image. We also set the crop callback to our updateCoords function. The updateCoords method takes the coordinates that cropper passes in and sets our hidden fields to the appropriate values. We’ve also added a listener for our rotate buttons, clickCropButtonsListener. When a rotate button is clicked, we call the cropper’s rotate method which rotates the photo and will call the crop() method (which subsequently calls our updatedCoords callback. At this point, cropperjs should be working! If you navigate to your /users/:user_id/crop_images/new page, you should see the user photo and be able to crop it. Now what happens when we hit save?

Submit cropper coordinates to the server

When you hit save on the /users/:user_id/crop_images/new, it will submit a post request to the crop_images_controller#create action with the coordinate params. The create action needs submit the coordinate information to our user model and to paperclip.

# app/controllers/crop_images_controller

  def new
    @user = User.find(params[:user_id])
  end

  def create
    @user = User.find(params[:user_id])
    if @user.photo.exists? &&
        params[:coords_x].present? && 
        params[:coords_y].present? && 
        params[:coords_w].present? && 
        params[:coords_h].present?

      @user.photo_crop_x = params[:coords_x]
      @user.photo_crop_y = params[:coords_y]
      @user.photo_crop_w = params[:coords_w]
      @user.photo_crop_h = params[:coords_h]
      @user.photo_crop_rotate = params[:coords_rotate].present? ? params[:coords_rotate] : 0
      
      @user.photo = File.open(@user.photo.path(:original))
      @user.save
      flash[:success] = "Successfully Saved"
      redirect_to user_url(@user)
    else
      flash[:notice] = "You must submit cropping dimensions"
      redirect_to new_user_crop_image_url(user_id: @user.id)
    end
  end

In the above create action we first check to make sure that the user has a photo and that all the coordinate params have been submitted. If they haven’t we redirect to the new page with an error message. If the user does have a photo and the coordinate params are submitted we assign the user’s virtual cropping attributes with the given data. The user’s “virtual cropping attributes” are available to us by using Rails’ attr_accessor in our user model like so:

# app/models/user.rb

class User < ActiveRecord::Base
  attr_accessor :photo_crop_x, :photo_crop_y, :photo_crop_w, :photo_crop_h, :photo_crop_rotate
end

After setting those attributes in our controller with the provided data, we call save on the user. During the save process, paperclip will now have access to the cropping coordinates. We’ll write a custom processor to perform the cropping.

Use coordinates submitted to crop photo using paperclip

The final piece to the puzzle is to write a custom processor for paperclip. Custom processor’s go in your lib/paperclip_processors directory and allow you to transform the picture/attachment in any way you see fit. In this case, our processor will inherit from Paperclip’s Thumbnail processor and will look like this:

# lib/paperclip_processors/cropper.rb
module Paperclip
  class Cropper < Thumbnail
    def cropping?
      target.photo_crop_w.present? && 
        target.photo_crop_h.present? &&
        target.photo_crop_x.present? &&
        target.photo_crop_y.present? 
    end

    def target
      @attachment.instance
    end

    def transformation_command
      return super unless cropping?
      crop_command = [
        "-rotate #{target.photo_crop_rotate.to_f} -crop"
        "#{target.photo_crop_w.to_f}x" \
        "#{target.photo_crop_h.to_f}+" \
        "#{target.photo_crop_x.to_f}+" \
        "#{target.photo_crop_y.to_f}",
        "+repage"
      ]
      crop_command + super
    end
  end
end

The transformation_command first checks to make sure that we’re cropping the photo by checking if our target (in this case or user object) has all the virtual cropping attributes present. If those aren’t present, then we defer to super. If the cropping attributes are present we create an ImageMagick rotate and crop commands which will rotate and crop the photo to our desired dimensions and then pass the cropped photo onto subsequent paperclip processing.

Conclusion

And that’s it! With these pieces in place, you can now crop and rotate photos using Rails, paperclip, and cropperjs. You should be able to navigate to the cropping page, select the cropping area on the photo, hit save, and finally have a cropped photo saved to your user model. Building a feature like the above can be a bit overwhelming when you first start, but breaking it up into individual steps before you set out to implement can keep you on the right track before trying to piece it all together.

SUBSCRIBE

Drop your email in the box below to subscribe to my newsletter. Once per week you'll get Ruby/Rails tips, guides, job postings, and general thoughts from the web developer trenches.