By default, Ruby on Rails Active Storage saves images as-is, then allows them to be resized at a later time using the attachment's variant
method.
Sometimes you may want to resize an image before you save it. Rails doesn't provide a built-in way of accomplishing this, but it can be achieved by intercepting the image from params
, then editing it in place before you call your model's save
or update
methods.
Setup
This post assumes that you have already run the Active Storage database migrations and have a model that uses a has_one_attached
field.
Ruby on Rails 7 uses the image_processing
gem to resize images, so either uncomment image_processing
from you Gemfile, or add it if it is not already present.
# Gemfile
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"
After that, install your Gemfile dependencies:
bundle install
Next, add the following private
method into a controller where you want to resize an image before persisting it to Active Storage.
def resize_before_save(image_param, width, height)
return unless image_param
begin
ImageProcessing::MiniMagick
.source(image_param)
.resize_to_fit(width, height)
.call(destination: image_param.tempfile.path)
rescue StandardError => _e
# Do nothing. If this is catching, it probably means the
# file type is incorrect, which can be caught later by
# model validations.
end
end
This method accepts a param
that contains an image and modifies its temporary file that is created on disk before it is uploaded to the selected Active Storage service.
Finally, add a before_action
that runs the resize before the actions where you'd like to resize the image:
before_action lambda {
resize_before_save(user_params[:profile_picture], 100, 100)
}, only: [:update]
A complete example
You can now resize images before they are persisted with Active Storage by calling the resize_before_save
method prior to calling the update
action.
class UsersController < ApplicationController
before_action :set_defaults
before_action :authenticate_user!
before_action lambda {
resize_before_save(user_params[:profile_picture], 100, 100)
}, only: [:update]
def edit
end
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to edit_user_url(@user) }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
private
def set_defaults
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(
:display_name,
:profile_picture,
)
end
def resize_before_save(image_param, width, height)
return unless image_param
begin
ImageProcessing::MiniMagick
.source(image_param)
.resize_to_fit(width, height)
.call(destination: image_param.tempfile.path)
rescue StandardError => _e
# Do nothing. If this is catching, it probably means the
# file type is incorrect, which can be caught later by
# model validations.
end
end
end
Troubleshooting
Occasionally when uploading files in development, I've received an ActiveSupport::MessageVerifier::InvalidSignature
error. I don't know what causes this, but restarting my development server has typically fixed the issue.
Parting thoughts
I recommend pairing this approach with the active_storage_validations
gem and adding a validation to the attachment field to ensure that the attachment that you'd like to resize is an image. Ideally, an application should gracefully handle invalid input with useful error messages.
I also want to give credit to posts by Elaine Osbourn and Donapieppo that were instrumental in helping me figure this out.
If you liked this post, please leave it a like, or comment if you know a better way of doing this!