← Back to all posts

Crop photos with Rails, Paperclip, and Jcrop

David Anderson | May 03, 2019

Are you having trouble putting all the pieces together to allow users to crop a photo and save the cropped photo to your model using paperclip? You have a user model with a paperclip attachment for the user’s profile photo and using the Jcrop jQuery library seems straightforward enough, but how do you get the cropped dimensions from the front end submitted to your app and then to paperclip so that the cropping actually occurs? It’s easier than it seems!

With a feature that has a lot of moving pieces, I like to break it up into more manageable pieces before trying to get it all to work. In this case, we’ll try to do something like this:

  1. Add Jcrop to our app and add crop_images routes
  2. Get Jcrop working on our page
  3. Submit Jcrop 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 the Jcrop library and then add the jquery.Jcrop.js and jquery.Jcrop.css files to your app’s vendor directory. The js file will go here vendor/assets/javascripts/jquery.Jcrop.js and the css file should go here vendor/assets/stylesheets/jquery.Jcrop.css. Now we need to add those assets to our asset manifest files.

// app/assets/javascripts/application.js

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

*= require jquery.Jcrop

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 Crop Image
      - if @user.photo.exists?
        #crop-stage.m-b-lg
          = image_tag @user.photo.url(:original), id: 'crop-image', style: 'max-width:none;max-height:none;'
          = form_tag user_crop_images_url(user_id: @user.id) do
            = 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'
            = submit_tag t('aaa.rest.save'), class: 'btn btn-primary', data: {disable_with: "..."}
      - else
        %p.text-danger
          This user doesn't have a photo 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-width and max-height as none. This allows our image to initially show up at its original size, and we’ll use Jcrop’s Box Sizing method to scale the image to a manageable width/height in the browser. More on that to come!

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, 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;

  self.initJcrop = function() {
    $(elementId).Jcrop({
      aspectRatio: 1,
      boxWidth: 600,
      onChange: self.updateCoords,
      onSelect: self.updateCoords
    })
  }

  self.updateCoords = function(coords) {
    $('#coords-x').val(coords.x);
    $('#coords-y').val(coords.y);
    $('#coords-w').val(coords.w);
    $('#coords-h').val(coords.h);
  }
}

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

Our ImageCropper class initializes Jcrop with a few options. We’re setting the aspectRatio to 1 and boxWidth to 600. The boxWidth option is what scales down the image to a maximum width of 600 (or whatever you set it to). We also set the onChange and onSelect callback options to our updateCoords function. The updateCoords method takes the coordinates that Jcrop passes in and sets our hidden fields to the appropriate values. At this point, Jcrop 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 Jcrop 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

class CropImagesController < ApplicationController
  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 = 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
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
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 = [
        "-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 our 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 crop command which will 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 photos using Rails, paperclip, and Jcrop. 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.