Ruby on Rails(6+) Uploading and Cropping images with Shrine, and Cropper.js

I was going around the internet to find a good guide to integrate shrine, and cropper.js to upload and crop the images. All those hours went into vain. So I have decided to give a quick overview on how to upload images with shrine, and crop them using Cropper.js.

FYI, this is a tutorial for Rails 6. You can read more about cropper.js, and shrine from here:

Step # 1 Setup Shrine

Quick note: You must have MiniMagick installed on your system.

We will quickly add shrine to our gem file, and setup the initializer (shrine.rb)

# Gemfile
gem "shrine", "~> 3.0"

Now let’s setup our initializer

# config/initializers/shrine.rbrequire 'shrine'require 'shrine/storage/file_system'require 'image_processing/mini_magick'Shrine.storages = {cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'),store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/store')}Shrine.plugin :activerecordShrine.plugin :cached_attachment_dataShrine.plugin :restore_cached_dataShrine.plugin :validation_helpersShrine.plugin :derivatives, versions_compatibility: true

Step # 2 Add column in the database

For this example, we are taking User model as an example. To add image column we will run a migration.

rails generate migration add_image_data_to_users image_data:text

This will create a new column in our User’s table.

Step # 3 Uploader, and register it with your main Model(User).

We will create a standard uploader which will inherit from Shrine’s class. This will do half of the heavy listing.

Create an uploader class (which you can put in app/uploaders) and register the attachment on your model

# app/uploadersclass ImageUploader < Shrineinclude ImageProcessing::MiniMagickAttacher.validate do# You can add your image validations here ..endAttacher.derivatives do |original|magick = ImageProcessing::MiniMagick.source(original)result = {}unless $logo_crop_x.blank?x = $logo_crop_x.to_fy = $logo_crop_y.to_fw = $logo_crop_w.to_fh = $logo_crop_h.to_fmagick = magick.crop(x,y,w,h)endresult[:thumb] = magick.resize_to_limit!(100, 100)result[:medium] = magick.resize_to_limit!(300, 300)resultendclass Attacherdef promote(*)create_derivativessuperendendend

logo_crop_x, logo_crop_y, logo_crop_h, and logo_crop_w are the coordinates of the images. We will use these to crop the image. Later on we will be providing these coordinates to our uploader.

Now, let’s register the uploader with our User model.

# app/models/user.rb
class User < ApplicationRecord
attr_accessor :logo_crop_x,
:logo_crop_y, :logo_crop_h,:logo_crop_w
include ImageUploader::Attachment(:image)end

So far so good!

Step # 4 Update our controller

Let’s update our controller, and permit the parameters that we are going to use.

class UsersController < ApplicationControllerbefore_action :sanitize_fields_params, only: [:update]def updaterespond_to do |format|current_user.update(user_params)format.html { redirect_to root_path }endendprivatedef user_paramslist_params_allowed = [:image,:logo_crop_x, :logo_crop_y,     :logo_crop_h, :logo_crop_w]params.require(:user).permit(list_params_allowed)enddef sanitize_fields_params$logo_crop_x = params[:user][:logo_crop_x]$logo_crop_y = params[:user][:logo_crop_y]$logo_crop_w = params[:user][:logo_crop_w]$logo_crop_h = params[:user][:logo_crop_h]endend

Phase II (Cropper.js)

Now the fun part starts. We will add cropper in our application and integrate it with our views.

Step # 5 Adding Cropper.js

We will install cropper.js with our package manager, and add it to our application. Create a file called custom_cropper.js in app/javascript/custom. All our configurations related cropper will go in this file.

yarn install cropperjs# app/javascript/packs/application.js

import 'custom/cp'
#app/assets/stylesheets/application.scss
*= require cropperjs/dist/cropper.css

Let’s customize our cropper. Go to custom_cropper.js file, and paste the code there:

Note: We will be opening a new modal to crop the image.

function crop_image_load(data) {data = data.detail;var $crop_x = $("input#logo_crop_x")[0],$crop_y = $("input#logo_crop_y")[0],$crop_w = $("input#logo_crop_w")[0],$crop_h = $("input#logo_crop_h")[0];$crop_x.value = data.x;$crop_y.value = data.y;$crop_h.value = data.height;$crop_w.value = data.width;}function getRoundedCanvas(sourceCanvas) {var canvas = document.createElement('canvas');var context = canvas.getContext('2d');var width = sourceCanvas.width;var height = sourceCanvas.height;canvas.width = width;canvas.height = height;context.imageSmoothingEnabled = true;context.drawImage(sourceCanvas, 0, 0, width, height);context.globalCompositeOperation = 'destination-in';context.beginPath();context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);context.fill();return canvas;}function readURL(input) {if (input.files && input.files[0]) {var reader = new FileReader();reader.onload = function (e) {$('#image').attr('src', e.target.result);}reader.readAsDataURL(input.files[0]);}}var $crop_x = $("input#logo_crop_x"),$crop_y = $("input#logo_crop_y"),$crop_w = $("input#logo_crop_w"),$crop_h = $("input#logo_crop_h");$crop_x.val('');$crop_y.val('');$crop_h.val('');$crop_w.val('');var $image = $('#image')[0];var $button = $('#button');var croppable = false;let x;$('#upload-modal').on('shown.bs.modal', function () {x = new Cropper($image, {aspectRatio: 1,viewMode: 1,ready: function () {croppable = true;},crop: function (event) {crop_image_load(event)}});}).on('hidden.bs.modal', function () {x.destroy();});$button.on('click', function () {var croppedCanvas;var roundedCanvas;croppedCanvas = x.getCroppedCanvas();roundedCanvas = getRoundedCanvas(croppedCanvas);$('#avatar')[0].src = roundedCanvas.toDataURL();$('#avatar')[0].setAttribute("height", "100");$('#avatar')[0].setAttribute("width", "100");});$("#image_to_upload").change(function () {$('#upload-modal').modal('show');readURL(this);});

In a nutshell, this file opens up a new modal whenever user selects a new image.

crop_image_load — — This function will be called whenever we resize our image. It will update the coordinates that we will need to crop our image in the Uploader that we created earlier.

getRoundedCanvas — — To convert the cropped image into nice round shape. Just a fancy touch.

readURL — — Moves the context image to latest selected one.

(FINAL) Step # 6 Update our form html file.

This is the last major step. We will simply add the form fields, modal to view and store our image.

# app/views/users/_form.html.erb<%= form_with model: user, url: user_path do |f| %><div><div class="row align-items-end profile-image" data-controller="thumb"><div class='col-md-6'><div class="form-group"><%= f.hidden_field :image, value: f.object.cached_image_data %><% if !f.object.image.present? %><span class="fa fa-file fa-6x place-holer-image"></span><%= image_tag '', id: :avatar %><%else%><%= image_tag f.object.image_url(:thumb), id: :avatar %><% end %></div></div><div class='col-md-6'><div class="row"><div class="col-md-12"><%= f.file_field :image, id: :image_to_upload %><%= f.label :image, class: 'upload' do %><% end %></div><% %w[x y w h].each do |attribute| %><%= f.hidden_field "logo_crop_#{attribute}", id:"logo_crop_#{attribute}" %><% end %></div></div></div><% end % >
# modal
<div class="modal fade" id="upload-modal" aria-labelledby="modalLabel" role="dialog" tabindex="-1"><div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="modalLabel">Crop Image</h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"><div><img id="image" width="100%" src="" alt="Logo"></div></div><div class="modal-footer"><input type="button" class="btn btn-primary" id="button" value="Crop" data-dismiss="modal"></button><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div>

Final Look

Look out for

  1. If you are getting errors on your browser’s console, it probably means you have some problems related to turbolinks. Make sure they are configured properly.

If you have any questions, feel free to ask in the comments below. Or you can connect with me through my website.

Happy developing!

Hamza Awais

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store