-
Notifications
You must be signed in to change notification settings - Fork 275
Image Cropping
This walkthrough shows how to add image cropping functionality to a Rails app. It assumes you have direct uploads set up as shown in Adding Direct App Uploads.
We'll be generating thumbnails, so let's add the ImageProcessing gem to the Gemfile:
# Gemfile
# ...
gem "image_processing", "~> 1.9"
We'll be using libvips for image processing, so make sure to have it installed:
$ brew install vips
Let's now generate image thumbnails and set them up as derivatives:
# config/initializers/shrine.rb
# ...
Shrine.plugin :derivatives
# app/uploaders/image_uploader.rb
require "image_processing/vips"
class ImageUploader < Shrine
THUMBNAILS = {
large: [800, 800],
medium: [600, 600],
small: [300, 300],
}
Attacher.derivatives do |original|
vips = ImageProcessing::Vips.source(original)
THUMBNAILS.transform_values do |(width, height)|
vips.resize_to_limit!(width, height)
end
end
end
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
# ...
def create
@photo = Photo.new(photo_params)
if @photo.valid?
@photo.image_derivatives! if @photo.image_changed? # create derivatives
@photo.save
# ...
else
# ...
end
end
# ...
end
Now we'll give users the ability to crop uploaded images. We will show a crop box using Cropper.js, then on form submit crop points will be sent to the backend, which will then apply cropping during processing.
We'll start by installing Cropper.js:
$ yarn add cropperjs
We'll create a cropbox()
function:
// app/javascript/cropbox.js
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
function cropbox(image, url, { onCrop }) {
image.src = url
new Cropper(image, {
aspectRatio: 1,
viewMode: 1,
guides: false,
autoCropArea: 1.0,
background: false,
zoomable: false,
crop: event => onCrop(event.detail)
})
}
export default cropbox
Then we'll call it after the upload finishes, and update the uploaded file data in the hidden field when crop points change:
// app/javascript/fileUpload.js
// ...
import cropbox from 'cropbox'
// ...
uppy.on('upload-success', (file, response) => {
// retrieve uploaded file data
const uploadedFileData = response.body['data']
// set hidden field value to the uploaded file data so that it's submitted
// with the form as the attachment
hiddenInput.value = JSON.stringify(uploadedFileData)
cropbox(imagePreview, response.uploadURL, {
onCrop(detail) {
let fileData = JSON.parse(hiddenInput.value)
fileData['metadata']['crop'] = detail
hiddenInput.value = JSON.stringify(fileData)
}
})
})
// ...
In our uploader we can now read the crop points from the uploaded file and apply cropping before resizing:
# app/uploaders/image_uploader.rb
class ImageUploader < Shrine
THUMBNAILS = {
large: [800, 800],
medium: [600, 600],
small: [300, 300],
}
Attacher.derivatives do |original|
vips = ImageProcessing::Vips.source(original)
vips = vips.crop(*file.crop_points) # apply cropping
THUMBNAILS.transform_values do |(width, height)|
vips.resize_to_limit!(width, height)
end
end
class UploadedFile
# convenience method for fetching crop points from metadata
def crop_points
metadata.fetch("crop").fetch_values("x", "y", "width", "height")
end
end
end
Now you should have working image cropping.
Let's now move image processing into a background job:
# config/initializers/shrine.rb
# ...
Shrine.plugin :backgrounding
Shrine::Attacher.promote_block { PromoteJob.perform_later(record, name, file_data) }
Shrine::Attacher.destroy_block { DestroyJob.perform_later(data) }
# app/jobs/promote_job.rb
class PromoteJob < ApplicationJob
def perform(record, name, file_data)
attacher = Shrine::Attacher.retrieve(model: record, name: name, file: file_data)
attacher.create_derivatives
attacher.atomic_promote
end
end
# app/jobs/destroy_job.rb
class DestroyJob < ApplicationJob
def perform(data)
attacher = Shrine::Attacher.from_data(data)
attacher.destroy
end
end
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
# ...
def create
@photo = Photo.new(photo_params)
if @photo.valid?
# we removed `photo.image_derivatives!` call here
@photo.save
# ...
else
# ...
end
end
# ...
end
We'll add a fallback URL for missing derivatives which uses on-the-fly processing, so that the user sees the cropped image while the background job is still processing:
# config/initializers/shrine.rb
# ...
Shrine.plugin :derivation_endpoint, secret_key: Rails.application.secret_key_base
Shrine.plugin :default_url
# config/routes.rb
Rails.application.routes.draw do
# ...
mount ImageUploader.derivation_endpoint => "/derivations/image"
end
# app/uploaders/image_uploader.rb
# ...
class ImageUploader < Shrine
# ...
plugin :derivation_endpoint, prefix: "derivations/image"
# Default URLs of missing derivatives to on-the-fly processing.
Attacher.default_url do |derivative: nil, **|
next unless derivative && file
file.derivation_url :transform, shrine_class.urlsafe_serialize(
crop: file.crop_points,
resize_to_limit: THUMBNAILS.fetch(derivative),
)
end
# Generic derivation that applies a given sequence of transformations.
derivation :transform do |file, transformations|
transformations = shrine_class.urlsafe_deserialize(transformations)
vips = ImageProcessing::Vips.source(file)
vips.apply!(transformations)
end
# ...
end