#182 Cropping Images (revised)
- Download:
- source codeProject Files in Zip (533 KB)
- mp4Full Size H.264 Video (25.2 MB)
- m4vSmaller H.264 Video (13.1 MB)
- webmFull Size VP8 Video (17.2 MB)
- ogvFull Size Theora Video (26.7 MB)
Below is a form from a Rails application for creating a new User
record with a name and an avatar.
The image we’ve chosen for this user is 1200 pixels wide but when we upload it it’s automatically resized and cropped to make it 100 pixels square.
We’d like to give the user more control over how their avatar image is cropped by allowing them to pick the portion of the uploaded image they want to use as their avatar and that’s what we’ll do in this episode.
Creating The Crop Page
Our application currently has a User model and uses CarrierWave to handle image uploads. (If you’re unfamiliar with CarrierWave we covered it in episode 253.)
class User < ActiveRecord::Base mount_uploader :avatar, AvatarUploader end
In User
we call mount_uploader
with an avatar
field and an AvatarUploader
class. Here’s what that class looks like:
# encoding: utf-8 class AvatarUploader < CarrierWave::Uploader::Base include CarrierWave::RMagick storage :file # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end version :thumb do resize_to_fill(100, 100) end end
This class inherits from RMagick
so that it can handle resizing and cropping images and it creates a thumbnail for each image called thumb resized to fit inside a 100x100 square.
Once a user has entered their name, chosen an image and submitted the form we want them to be taken to a second page where they can choose the portion of the image they want to use as their avatar. Users can upload images at any size so we’ll create another variation of each image that’s resized to a known size and present that version to the user to crop their avatar from.
version :large do resize_to_limit(600, 600) end
We’ve called this new version large and used resize_to_limit
instead of resize_to_fill
so that the image isn’t cropped at all, just resized to fit within a 600 pixel square if the original is bigger than 600 pixels high or wide.
In the UsersController
we’ll edit the edit
and update
actions so that the secondary page is shown when an avatar has been uploaded. If no image has been supplied we’ll redirect as before.
class UsersController < ApplicationController # index, show, new, edit and destroy actions omitted. def create @user = User.new(params[:user]) if @user.save if params[:user][:avatar].present? render :crop else redirect_to @user, notice: "Successfully created user." end else render :new end end def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) if params[:user][:avatar].present? render :crop else redirect_to @user, notice: "Successfully updated user." end else render :new end end end
We could make a dedicated crop action so that the user can crop their image at any time but this will work for us here.
Next we’ll write the new crop template. All we do on this page for now is show the large version of the uploaded image.
<h1>Crop Avatar</h1> <%= image_tag @user.avatar_url(:large) %>
We’ll need to add some functionality to enable users to crop this large image and we’ll use a jQuery plugin called jCrop to help with this. We need to download this plugin then copy Jcrop.gif
into /vendor/assets/images
, jquery.Jcrop.js
into /vendor/assets/javascripts
and jquery.Jcrop.css
into /vendor/assets/stylesheets
.
The image file is referenced by the stylesheet and while this works in development mode in production Rails will add a hash to the end of the image name which may cause problems. If it does we can either move the image into the public assets directory, change the stylesheet so that it dynamically references the image correctly or turn off hashing in production.
We’re in development mode so we don’t need to worry about this for now but we do have to include the files we’ve added in the appropriate manifest files. First we’ll add Jcrop’s JavaScript file after jQuery in the JavaScript manifest.
//= require jquery //= require jquery_ujs //= require jquery.Jcrop //= require_tree .
Similarly we’ll need to reference Jcrop’s stylesheet in the CSS manifest.
/* *= require jquery.Jcrop *= require_self *= require_tree . */
We’ll need to enable Jcrop on our large avatar image and we’ll do that in the users
CoffeeScript file. Before we do so we’ll need to give the large image an id
so that we can reference it from JavaScript.
<h1>Crop Avatar</h1> <%= image_tag @user.avatar_url(:large), id: "cropbox" %>
To add Jcrop to this image we just need to call Jcrop()
on it.
jQuery -> $('#cropbox').Jcrop()
We may have to restart our Rails server for these changes to be picked up but once we have we’ll be able to select an area of the image.
Sending The Cropped Area Back to The Server
We’ve got JCrop working now but we still need a way to submit the co-ordinates of the cropped area back to the server. Before we do that we’ll add some options to JCrop to restrict the area that can be selected. While we do this we’ll also move the code into a class as we’re going to have to write a lot more JavaScript code before we’re done.
jQuery -> new AvatarCropper() class AvatarCropper constructor: -> $('#cropbox').Jcrop aspectRatio: 1 setSelect: [0, 0, 600, 600]
Our JCrop code is now in an AvatarCropper
class and we’ve set two options. The first restricts the aspect ratio of the selected area so that the selected part of the image is square; the second sets the initial selected area and here we’ve chosen to select the largest possible square from the image.
We need to pass the selected cropping area back to the server so that we can crop the image. We can do that in the crop
template by adding a form with four fields that will store the coordinates of the cropped area. These fields will be hidden in the final form but for now we’ll make them text fields so that we can see what’s being passed in.
<h1>Crop Avatar</h1> <%= image_tag @user.avatar_url(:large), id: "cropbox" %> <%= form_for @user do |f| %> <div class="actions"> <% %w[x y w h].each do |attribute| %> <%= f.text_field "crop_#{attribute}"%> <% end %> <%= f.submit "Crop" %> </div> <% end %>
Our User
model doesn’t have these crop
attributes so we’ll add them as virtual attributes.
class User < ActiveRecord::Base mount_uploader :avatar, AvatarUploader attr_accessor :crop_x, :crop_y, :crop_w, :crop_h end
We’ll need to modify our JavaScript code so that the form fields are populated when the selected area changes. JCrop has a couple of event callbacks that will help with this: onSelect
and onChange
. These take a function so we’ll create an update
function that will be called each time the selected area changes.
jQuery -> new AvatarCropper() class AvatarCropper constructor: -> $('#cropbox').Jcrop aspectRatio: 1 setSelect: [0, 0, 600, 600] onSelect: @update onChange: @update update: (coords) => $('#user_crop_x').val(coords.x) $('#user_crop_y').val(coords.y) $('#user_crop_w').val(coords.w) $('#user_crop_h').val(coords.h)
The update
function has the coordinates passed to it in an object and we use this to set the values of the form fields. Note that we use the ‘fat arrow’ in this function to preserve the context. When we reload the cropping page now we’ll see the coordinates for the currently selected area and these will update automatically as we move the crop area around.
When the form is submitted the User
model is updated with the crop
attributes from the form. We can use an after_update
callback to trigger the cropping.
class User < ActiveRecord::Base mount_uploader :avatar, AvatarUploader attr_accessor :crop_x, :crop_y, :crop_w, :crop_h after_update :crop_avatar def crop_avatar avatar.recreate_versions! if crop_x.present? end end
The callback calls a new crop_avatar
method which tells CarrierWave to recreate the images to crop them, though only if the crop
attributes have been passed in.
In the AvatarUploader
class the thumbnail version is recreated when crop_avatar
is called. We’ll add some code to this method so that the image is cropped based on the coordinates in the User
model.
version :thumb do process :crop resize_to_fill(100, 100) end def crop if model.crop_x.present? manipulate! do |img| x = model.crop_x.to_i y = model.crop_y.to_i w = model.crop_w.to_i h = model.crop_h.to_i img.crop!(x, y, w, h) end end end
We can call model
at any time in the AvatarUploader
class to reference the User
model so after checking that the crop_x
value is present we crop the image. In CarrierWave we can call manipulate!
to get the current image from ImageMagick and then call crop!
on it to change the cropping value based off the cropping coordinates from the User
model.
When we try this it doesn’t work. An area of the image is cropped but it’s not the one we selected. The problem is that the image we crop from is the original full resolution image, but the coordinates come from the large version of the image. A quick fix for this is to resize the original image to the size of the large image before we drop it.
def crop if model.crop_x.present? resize_to_limit(600, 600) manipulate! do |img| x = model.crop_x.to_i y = model.crop_y.to_i w = model.crop_w.to_i h = model.crop_h.to_i img.crop!(x, y, w, h) end end end
When we try this now the thumbnail image is cropped correctly based on the area we selected.
Adding a Preview
Now that we know this is working, we’ll replace the text fields on the cropping page with hidden field and add some code to add a live preview to the page.
<h1>Crop Avatar</h1> <%= image_tag @user.avatar_url(:large), id: "cropbox" %> <h4>Preview</h4> <div style="width:100px; height:100px; overflow:hidden;"> <%= image_tag @user.avatar.url(:large), :id => "preview" %> </div> <%= form_for @user do |f| %> <div class="actions"> <% %w[x y w h].each do |attribute| %> <%= f.hidden_field "crop_#{attribute}"%> <% end %> <%= f.submit "Crop" %> </div> <% end %>
We’ll also need some JavaScript to update the preview image as the selected image changes so we’ll call a new updatePreview
function that will do this when the selected area changes.
update: (coords) => $('#user_crop_x').val(coords.x) $('#user_crop_y').val(coords.y) $('#user_crop_w').val(coords.w) $('#user_crop_h').val(coords.h) @updatePreview(coords) updatePreview: (coords) => $('#preview').css width: Math.round(100/coords.w * $('#cropbox').width()) + 'px' height: Math.round(100/coords.h * $('#cropbox').height()) + 'px' marginLeft: '-' + Math.round(100/coords.w * coords.x) + 'px' marginTop: '-' + Math.round(100/coords.h * coords.y) + 'px'
When we move or resize the selected area now the preview updates live.
Now users can know exactly what their avatar will look like before they click ‘Crop’.