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!
Top comments (2)
shouldn't
resize_before_save
be in the model instead of the controller?It could be if you wanted it to be 🤷
For me, it feels weird having the user model knowing about image resizing, though it's possible this might work in the
before_save
andbefore_update
hooks. However, then you have to do something clever to make sure that you aren't resizing the image on every save.My app is pretty simple and the user controller is the only place that a user profile picture can be set, so I'm comfortable with this on the controller.