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

Step # 1 Setup Shrine

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

# Gemfile
gem "shrine", "~> 3.0"
# 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

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.

# 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
# 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

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
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);});

(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.

--

--

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