ActiveStorage - get image dimensions after upload - ruby-on-rails

I am using Rails + ActiveStorage to upload image files, and would like to save the width and height in the database after upload. However, I'm having trouble finding any examples of this anywhere.
This is what I've cobbled together from various API docs, but just end up with this error: private method 'open' called for #<String:0x00007f9480610118>. Replacing blob with image.file causes rails to log "Skipping image analysis because ImageMagick doesn't support the file" (https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/analyzer/image_analyzer.rb#L39).
Code:
class Image < ApplicationRecord
after_commit { |image| set_dimensions image }
has_one_attached :file
def set_dimensions(image)
if (image.file.attached?)
blob = image.file.download
# error: private method `open' called for #<String:0x00007f9480610118>
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
end
end
end
This approach is also problematic because after_commit is also called on destroy.
TLDR: Is there a "proper" way of getting image metadata immediately after upload?

Rails Built in Solution
According to ActiveStorage Overview Guild there is already existing solution image.file.analyze and image.file.analyze_later (docs ) which uses ActiveStorage::Analyzer::ImageAnalyzer
According to #analyze docs :
New blobs are automatically and asynchronously analyzed via analyze_later when they're attached for the first time.
That means you can access your image dimensions with
image.file.metadata
#=> {"identified"=>true, "width"=>2448, "height"=>3264, "analyzed"=>true}
image.file.metadata['width']
image.file.metadata['height']
So your model can look like:
class Image < ApplicationRecord
has_one_attached :file
def height
file.metadata['height']
end
def width
file.metadata['width']
end
end
For 90% of regular cases you are good with this
BUT: the problem is this is "asynchronously analyzed" (#analyze_later) meaning you will not have the metadata stored right after upload
image.save!
image.file.metadata
#=> {"identified"=>true}
image.file.analyzed?
# => nil
# .... after ActiveJob for analyze_later finish
image.reload
image.file.analyzed?
# => true
#=> {"identified"=>true, "width"=>2448, "height"=>3264, "analyzed"=>true}
That means if you need to access width/height in real time (e.g. API response of dimensions of freshly uploaded file) you may need to do
class Image < ApplicationRecord
has_one_attached :file
after_commit :save_dimensions_now
def height
file.metadata['height']
end
def width
file.metadata['width']
end
private
def save_dimensions_now
file.analyze if file.attached?
end
end
Note: there is a good reason why this is done async in a Job. Responses of your request will be slightly slower due to this extra code execution needs to happen. So you need to have a good reason to "save dimensions now"
Mirror of this solution can be found at How to store Image Width Height in Rails ActiveStorage
DIY solution
recommendation: don't do it, rely on existing Vanilla Rails solution
Models that need to update attachment
Bogdan Balan's solution will work. Here is a rewrite of same solution without the skip_set_dimensions attr_accessor
class Image < ApplicationRecord
after_commit :set_dimensions
has_one_attached :file
private
def set_dimensions
if (file.attached?)
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
height = meta[:height]
width = meta[:width]
else
height = 0
width = 0
end
update_columns(width: width, height: height) # this will save to DB without Rails callbacks
end
end
update_columns docs
Models that don't need to update attachment
Chances are that you may be creating model in which you want to store the file attachment and never update it again. (So if you ever need to update the attachment you just create new model record and delete the old one)
In that case the code is even slicker:
class Image < ApplicationRecord
after_commit :set_dimensions, on: :create
has_one_attached :file
private
def set_dimensions
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
self.height = meta[:height] || 0
self.width = meta[:width] || 0
save!
end
end
Chances are you want to validate if the attachment is present before saving. You can use active_storage_validations gem
class Image < ApplicationRecord
after_commit :set_dimensions, on: :create
has_one_attached :file
# validations by active_storage_validations
validates :file, attached: true,
size: { less_than: 12.megabytes , message: 'image too large' },
content_type: { in: ['image/png', 'image/jpg', 'image/jpeg'], message: 'needs to be an PNG or JPEG image' }
private
def set_dimensions
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
self.height = meta[:height] || 0
self.width = meta[:width] || 0
save!
end
end
test
require 'rails_helper'
RSpec.describe Image, type: :model do
let(:image) { build :image, file: image_file }
context 'when trying to upload jpg' do
let(:image_file) { FilesTestHelper.jpg } # https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html
it do
expect { image.save }.to change { image.height }.from(nil).to(35)
end
it do
expect { image.save }.to change { image.width }.from(nil).to(37)
end
it 'on update it should not cause infinitte loop' do
image.save! # creates
image.rotation = 90 # whatever change, some random property on Image model
image.save! # updates
# no stack ofverflow happens => good
end
end
context 'when trying to upload pdf' do
let(:image_file) { FilesTestHelper.pdf } # https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html
it do
expect { image.save }.not_to change { image.height }
end
end
end
How FilesTestHelper.jpg work is explained in article attaching Active Storange to Factory Bot

Answering own question: my original solution was close, but required ImageMagick to be installed (it wasn't, and the error messages did not point that out). This was my final code:
class Image < ApplicationRecord
attr_accessor :skip_set_dimensions
after_commit ({unless: :skip_set_dimensions}) { |image| set_dimensions image }
has_one_attached :file
def set_dimensions(image)
if (Image.exists?(image.id))
if (image.file.attached?)
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(image.file).metadata
image.width = meta[:width]
image.height = meta[:height]
else
image.width = 0
image.height = 0
end
image.skip_set_dimensions = true
image.save!
end
end
end
I also used this technique to skip the callback on save!, preventing an infinite loop.

I think you can get dimension from javascript before update and then post those data into controller.
you can check it out:
Check image width and height before upload with Javascript

Related

How do you test a specific image file extension in Rails?

I have a custom validation to prevent uploads of image file types other than JPG or PNG for an Account logo.
Is it possible to stub a file with a specific file extension to test the patch action of the controller handling the upload ?
This is what the custom validation looks like:
has_one_attached :logo
validate :correct_logo_file_type
def correct_logo_file_type
if logo.attached? && !logo.content_type.in?(%w(image/jpg image/png))
errors.add(:logo, :incorrect_file_type)
end
end
ACCEPTABLE_TYPES = ['image/png', 'image/gif'].freeze
has_one_attached :screenshot, dependent: :destroy
validate :acceptable_image
private
def acceptable_image
return true unless screenshot.attached?
errors.add(:screenshot, 'must be a PNG or GIF') unless ACCEPTABLE_TYPES.include?(screenshot.content_type)
end

How do you pass additional variables to a CarrierWave uploader in Rails?

I see this has been asked a few times over the years (eg Upload path based on a record value for Carrier wave Direct, Passing a parameter to the uploader / accessing a model's attribute from within the uploader / letting the user pick the thumbnail size), but I'm convinced I must be overcomplicating this, as it seems like a very simple problem...
I have a very straightforward Video model that mounts an uploader:
class Video < ApplicationRecord
mount_uploader :file, VideoUploader
end
In the controller, I allow two parameters:
def video_params
params.require(:video).permit(:title, :file)
end
In the actual VideoUploader, I seem to have access to a number of variables derived from the :file column using class builtins (eg original_filename), and I can process the file using ffmpeg parameters. However, I want the parameters to be conditional based on the :title string, and I have no idea how to scope it or access it. What is the absolute simplest way to make sure this variable is accessible to those methods?
Edit: here's the uploader code:
class VideoUploader < CarrierWave::Uploader::Base
require 'streamio-ffmpeg'
include CarrierWave::Video
case #title # not working
when "tblend_glitch"
process encode_video: [:mp4,
resolution: "1280x960",
custom: %w(-to 5 -vf scale=-2:720,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference)]
...
end
def full_filename(for_file)
super.chomp(File.extname(super)) + '.mp4'
end
def filename
original_filename.chomp(File.extname(original_filename)) + '.mp4'
end
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
Thanks!
You should be able to access the instance in the uploader using the model method.
You haven't defined #title — it's nil. You can create a conditional version with the following code.
class VideoUploader < CarrierWave::Uploader::Base
version :tblend, if: :tblend_glitch? do # use a better version name
process encode_video: [:mp4,
resolution: "1280x960",
custom: %w(-to 5 -vf scale=-2:720,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference,spp=4:10,tblend=all_mode=average,tblend=all_mode=difference,tblend=all_mode=difference,tblend=all_mode=difference)]
end
# rest of the code
private
def tblend_glitch?
model.title == 'tblend_glitch'
end
end
Ref: https://github.com/carrierwaveuploader/carrierwave#conditional-versions
In theory you can access the model and its attributes from the uploader. However, it looks like the mounted uploader gets invoked before the other attributes are assigned.
For me it worked to create the model with the regular parameters first, and assign the attribute that has the uploader mounted (in your case :file) in a second step. Then I could read all model attributes correctly from within the uploader.
In your case that would be something like this in the controller:
#video = Video.new(video_params.except(:file))
#video.file = video_params[:file] # Invoke uploader last to access the other attributes

Rails paperclip default image only under certain conditions

I've got a Lead model which is splited by lead_status param on products and offers. Only leads with product status should contain images and offers should not. I've migrated attached product_image table to schema and tried to set a default image only for products. Like this:
class Lead < ApplicationRecord
has_attached_file :product_image, styles: { small: "150x150>", default: "350x350"}
validates_attachment_content_type :product_image, content_type: /\Aimage\/.*\z/
before_save :product_image_default_url
def product_image_default_url
if self.lead_status == "product" && self.product_image.url.nil?
self.product_image.url = "/images/:style/default_user_avatar.png"
end
end
Every time when I save a new lead without uploaded image I get "/product_images/original/missing.png" as a default url. No matter which status it has.
Model doesn't recognize new leads by it's status
How can I change that? Force my Lead model to save a default image url according to a "product" status and ignore all those with "offer" status?
My rails version is 5.2.1 and paperclip 6.0.0
Try with following,
has_attached_file
:product_image,
styles: { small: "150x150>", default: "350x350"},
default_url: ":style/default_user_avatar.png"
# app/assets/images/medium/default_user_avatar.png
# app/assets/images/thumbs/default_user_avatar.png
Existing method is,
def default_url
if #attachment_options[:default_url].respond_to?(:call)
#attachment_options[:default_url].call(#attachment)
elsif #attachment_options[:default_url].is_a?(Symbol)
#attachment.instance.send(#attachment_options[:default_url])
else
#attachment_options[:default_url]
end
end
In initializer, Provide monkey patch for following,
require 'uri'
require 'active_support/core_ext/module/delegation'
module Paperclip
class UrlGenerator
def default_url
if #attachment.instance.lead_status == 'product'
default_url = attachment_options[:default_url]
else
default_url = # provide another missing default_url
end
if default_url.respond_to?(:call)
default_url.call(#attachment)
elsif default_url.is_a?(Symbol)
#attachment.instance.send(default_url)
else
default_url
end
end
end
end
Update as per cases

How to change CarrierWave's min_size & max_size error messages?

I am using the following method to validate the file size of my images:
class ImageUploader < CarrierWave::Uploader::Base
def size_range
0..200.kilobytes
end
Unfortunately, the error message that my users are getting is a bit cryptic:
Image size should be less than 204800
Is there a way to change that to something more meaningful such as:
Image size should be less than 2 MB
I've tried various approaches but to no avail.
These are my localization files by the way:
errors:
messages:
min_size_error: size should be greater than %{min_size}
max_size_error: size should be less than %{max_size}
Thanks for any help.
To make an error message more friendly override carrierwave's check_size! method in your FileUploader with below code:
class ImageUploader < CarrierWave::Uploader::Base
def size_range
0..200.kilobytes
end
private
def check_size!(new_file)
size = new_file.size
expected_size_range = size_range
if expected_size_range.is_a?(::Range)
if size < expected_size_range.min
raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.min_size_error", :min_size => ApplicationController.helpers.number_to_human_size(expected_size_range.min))
elsif size > expected_size_range.max
raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.max_size_error", :max_size => ApplicationController.helpers.number_to_human_size(expected_size_range.max))
end
end
end
end
By using the built-in file size validator, you are unfortunately tied down to its implementation:
def check_size!(new_file)
size = new_file.size
expected_size_range = size_range
if expected_size_range.is_a?(::Range)
if size < expected_size_range.min
raise CarrierWave::IntegrityError,
I18n.translate(
:"errors.messages.min_size_error",
:min_size => expected_size_range.min
)
elsif size > expected_size_range.max
raise CarrierWave::IntegrityError,
I18n.translate(
:"errors.messages.max_size_error",
:max_size => expected_size_range.max
)
end
end
end
You could simply override that method to provide some other value for min_size and max_size (e.g. the number of Megabytes rather than Bytes).
Or alternatively, as explained the CarrierWave wiki, you could use a gem like file_validators, or copy this gist, or even quite easily write your own file validator rather than relying on the built-in CarrierWave version.
The simplest custom validator example given on that page (which I'm only copy+pasting here in case the link dies in the future) is:
# app/models/user.rb
class User< ActiveRecord::Base
attr_accessible :product_upload_limit
has_many :products
end
# app/models/brand.rb
class Product < ActiveRecord::Base
mount_uploader :file, FileUploader
belongs_to :user
validate :file_size
def file_size
if file.file.size.to_f/(1000*1000) > user.product_upload_limit.to_f
errors.add(:file, "You cannot upload a file greater than #{upload_limit.to_f}MB")
end
end
end
You can change the validation message by custom validator
like below
class Product < ActiveRecord::Base
mount_uploader :file, ImageUploader
validate :up_file_size
validate :down_file_size
def up_file_size
if file.file.size.to_f/(1000*1000) > 200.kilobytes
errors.add(:file, "size should be less than #{max_upload_limit.to_f}MB")
end
end
def down_file_size
if file.file.size.to_f/(1000*1000) < 0
errors.add(:file, "size should be greater than #{min_upload_limit.to_f}MB")
end
end
end

Rails + carrierwave not creating conditional versions

I'm trying to set up conditional versions with carrierwave in my Rails app. I've implemented what seems to be an exact duplicate of the examples provided here.
The version is never created though unless my is_ipod? simply returns true. The code below is what I currently have and is not working. Notice the commented sections I've used to verify the image_type attribute is actually set correctly.
version :ipod_portrait_thumb, :if => :is_ipod? do
process resize_to_fit: [150,200]
end
def is_ipod? image
model.image_type == 'iPod Screenshot'
#if (model.image_type == "iPod Screenshot")
#if (model.image_type!=nil)
#puts "+++++"+model.image_type
# if (model.image_type=="iPod Screenshot")
#puts "+++++++ I AM HERE"
# return true
# end
#end
end
If is_ipod? looks like this:
def is_ipod? image
true
end
the version is created as expected. What am I missing? Thanks!
UPDATE:
I've edited the is_ipod? method to look like this:
def is_ipod? image
puts (image.path || "") + ': ' + ((model.image_type||"") == 'iPod Screenshot').to_s
model.image_type == 'iPod Screenshot'
end
Which outputs this to the console:
/public/uploads/tmp/20130325-1024-15906-5363/drawing.png: false
/public/uploads/tmp/20130325-1024-15906-5363/drawing.png: false
/public/uploads/app_image/image/59/drawing.png: true
So the version is trying to be created three times, twice for temp files and once for the final file. The model attribute is only set for the final file. Is this related? Can anyone tell me how this is different than this example?
class MyUploader < CarrierWave::Uploader::Base
version :monkey, :if => :is_monkey?
protected
def is_monkey? picture
model.favorite_food == 'banana'
end
end
Here is my model class in case that helps:
class AppImage < ActiveRecord::Base
attr_accessible :app_id, :image, :image_type, :image_cache
belongs_to :app
mount_uploader :image, AppImageUploader
validates :image_type, presence: true
validates :image, presence: true
end
Thanks!
I had a similar problem^ and I tried to create different sizes depending on model attribute. My solution is simple: just recreate versions after save.
app/uploaders/project_item_picture_uploader.rb
class ProjectItemPictureUploader < CarrierWave::Uploader::Base
...
version :preview do
process dynamic_process: true
end
protected
def dynamic_process(*args)
resize_to_fill *(model.get_size) if model.persisted?
end
end
app/model/project_item.rb
class ProjectItem < ActiveRecord::Base
mount_uploader :picture, ProjectItemPictureUploader
validates_presence_of :picture
def get_size
double ? [168, 57] : [76, 57]
end
after_save :recreate_delayed_versions!
def recreate_delayed_versions!
picture.recreate_versions!
end
end
It turns out this is happening because the AppImage model is a child of another model and is being added here on a nested form. For whatever reason child models don't have their attributes set by the time carrierwave processes versions.
I've verified this by adding carrierwave attachments to my parent model App. When the versions are processed for an App attachment, the attributes are set.
Maybe later I'll dig deeper to try and understand better (I'm pretty new to Rails), but for now I'm working around the issue by not having conditional versions.
I have the same issue this night and i'm doing some experimentation with it, finally i figured out, it seem that when your create a version for the first time (when you post your form), if you have this line :
model.image_type == 'iPod Screenshot'
this means that you should obligatory have a field in your form named image_type and the value for this field should be "iPod Screenshot"
CarrierWave verify the same thing when you show your image like article.image_url(:ipod_portrait_thumb) which means that you should have in your database a field named image_type with value "iPod Screenshot"
so if you have this line :
version :ipod_portrait_thumb, :if => :is_ipod?
you are telling CarrierWave to execute ipod_portrait_thumb function each time you Create or Show a record
in Case that your form don't contains the field image_type with value 'iPod Screenshot', for example because you set it in your model/controller, the way is to check another field to permit CarrierWave create your conditional version, so it's simple you can do something like this :
model.image_type == 'iPod Screenshot' || another_field_in_my_form_help_you_to_know_type_is_ipode == "something" # or just .present? rather than == "something" switch your case
here when CarrierWave try to create version the first time it will check this another_field_...... , and when you have a page that show your record (product, article....) it verify model.image_type == 'iPod Screenshot' that is stored already in your database
Hope this help you :)

Resources