carrierwave: creating version based on image properties - ruby-on-rails

I want to keep my uploaded images upto 700 pixels.
If any image gets bigger I use the following code to get a new width.
This is my uploader file.
def store_dimensions
if file && model
width, height = ::MiniMagick::Image.open(file.file)[:dimensions]
if width>700
return 700
else
return width
end
end
Then I created a version named best_fit
process :store_dimensions
version :best_fit do
process :resize_to_fill => [store_dimensions,200]
end
It can't find the store_dimensions method. On the other hand, if I use self keyword while declaring store_dimensions method, then it works, but then the "file" identifier is become an unknown entity.
How can I get the value of the uploaded image and according to that I can create a new version of it.

The following code saved my ass today. I'm happy that I solved it.
def store_dimensions
if file && model
width, height = ::MiniMagick::Image.open(file.file)[:dimensions]
if width>700
Rails.logger.info "#{width}"
finalHeight=((700*height)/width)
self.class.version :best_fit do
process :resize_to_fill => [700,finalHeight]
end
else
Rails.logger.info "#{width}"
self.class.version :best_fit do
process :resize_to_fill => [width,height]
end
end
end
end
#run the store_dimensions methods
process :store_dimensions

For any lost wanderers, this is what you're looking for:
class Uploader
version :my_version do
process :my_processor
end
def my_processor
# model is available here!
manipulate! do |img|
img.combine_options do |cmd|
cmd.resize [model.width, model.height].join('x')
end
img
end
end
end
Example of custom process method

Related

ActiveStorage - get image dimensions after upload

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

Carrierwave and Jcrop, delete original after crop?

I'm having a difficult time getting Carrierwave to delete the original file after the cropped versions are made. I'm making a 600 pixel version of the upload for the user to crop but after the crop I want that version to get deleted since it's never used on the site.
I've tried several solutions found online but they all delete the large version before the crop, not after.
Here is my Carrierwave uploader:
# encoding: utf-8
class AvatarUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
storage :file
# storage :fog
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# Only allows jpg, jpeg, or png
def extension_white_list
%w(jpg jpeg png)
end
resize_to_limit(600, 600)
version :profile do
process :crop
resize_to_fill(120, 120)
def full_filename (for_file = model.file)
"profile.png"
end
end
version :timeline do
process :crop
resize_to_fill(50, 50)
def full_filename (for_file = model.file)
"timeline.png"
end
end
version :navigation do
process :crop
resize_to_fill(20, 20)
def full_filename (for_file = model.file)
"navigation.png"
end
end
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
end
You could either use an after_store callback to remove the original file (using File.delete) or you could modify the original so that it's the largest size you need:
version :normal do
process :resize_to_limit => [600,600]
end
For those who will try to solve the issue for the case of adding some revision information to the image name (e.g. cropping), I found a way to do this.
# :large is the cropped version
version :small_square, from_version: :large do
before :cache, :reset_revision
after :store, :remove_old_revision
def full_filename(for_file)
fname = super(for_file)
"#{ fname.pathmap('%n') }_#{ model.image_revision }#{ fname.pathmap('%x') }"
end
def full_original_filename
"#{ super.pathmap('%n') }_#{ model.image_revision }#{ super.pathmap('%x') }"
end
def remove_old_revision(file = nil)
File.delete(#old_path) if #old_path && File.exists?(#old_path)
end
def reset_revision(file = nil)
# Otherwise we'll get `cannot update new record` error
if model.persisted?
# Creating an instance variable that will be used after storing the cropped version
#old_path = File.join(Rails.public_path, model.public_send(mounted_as).store_dir, full_original_filename)
# I use :image_revision column in the DB
model.update_columns(image_revision: SecureRandom::hex(8))
else
model.image_revision = SecureRandom::hex(8)
end
end
process :crop_small_square
end
So, it's storing the path of the first version of the cropped version before changing the resulting image name and then deleting the first version.
Took me some time to figure out the way to attach some abracadabra to the end of a definite version of file: it takes the update action, rather than just assigning the attribute. Carrierwave's docs aren't that accurate.

Duplicating Carrierwave images makes them darker?

I have an Event model that has many photographs. I have an image uploader mounted to the Photographs attribute, and regular uploads and everything is working fine.
However, when I try and duplicate an event, recreating a new photograph object for the new event, the new image is darker than the original, and if I duplicate the duplicate event, it gets darker still.
I have played around with it, but have no solution.
My Uploader code:
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
include CarrierWave::Processing::RMagick
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def cache_dir
"#{Rails.root}/tmp/carrierwave"
end
process :colorspace => :rgb
# Remove EXIF data
process :strip
# Create different versions of your uploaded files:
version :thumb do
process :resize_to_limit => [640, 640]
end
version :preview_thumb do
process :resize_to_limit => [600, 600]
end
version :wine_thumb do
process :resize_to_limit => [160, 440]
end
version :logo_thumb do
process :resize_to_limit => [90, 90]
end
end
And my duplcation code (in Active Admin):
member_action :create_duplicate_event, method: :post do
old_event = Event.find(params[:id])
photograph_urls = old_event.photographs.map(&:image_url)
attributes = old_event.attributes.except("photographs", "id")
new_photos = []
photograph_urls.each do |photo|
new_photo = Photograph.new({
remote_image_url: photo
})
if new_photo.save
new_photos << new_photo
end
end
#event = Event.new(attributes)
#event.photograph_ids = new_photos.map(&:id)
render "/admin/events/_events_form/"
end
The :rgb tag was an attempt to fix. But no luck.
Ruby 2.1 and Rails 4.0
Ok, after a lot of playing around and searching I managed to fix this problem.
First, you need to download the .icc color profiles, which can be found here. It says for windows but they seemed to work for me on my Mac.
Once you have put the .icc files into a /lib/color_profiles directory, add the following code to your uploader:
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
process :convert_image_from_cmyk_to_rgb
#versions, and any other uploader code go here
def convert_image_from_cmyk_to_rgb
manipulate! do |image|
if image.colorspace == Magick::CMYKColorspace
image.strip!
image.add_profile("#{Rails.root}/lib/USWebCoatedSWOP.icc")
image.colorspace == Magick::SRGBColorspace
image.add_profile("#{Rails.root}/lib/sRGB.icc")
end
image
end
end
end
This converts CMYK images to RGB, and keeps the profiles keeping nice, while keeping RGB images as they were, and not ruining them.
I hope this helps someone in the future, and saves them the hours I spent working this out.

CarrierWave: create 1 uploader for multiple types of files

I want to create 1 uploader for multiple types of files (images, pdf, video)
For each content_type will different actions
How I can define what content_type of file?
For example:
if image?
version :thumb do
process :proper_resize
end
elsif video?
version :thumb do
something
end
end
I came across this, and it looks like an example of how to solve this problem: https://gist.github.com/995663.
The uploader first gets loaded when you call mount_uploader, at which point things like if image? or elsif video? won't work, because there is no file to upload defined yet. You'll need the methods to be called when the class is instantiated instead.
What the link I gave above does, is rewrite the process method, so that it takes a list of file extensions, and processes only if your file matches one of those extensions
# create a new "process_extensions" method. It is like "process", except
# it takes an array of extensions as the first parameter, and registers
# a trampoline method which checks the extension before invocation
def self.process_extensions(*args)
extensions = args.shift
args.each do |arg|
if arg.is_a?(Hash)
arg.each do |method, args|
processors.push([:process_trampoline, [extensions, method, args]])
end
else
processors.push([:process_trampoline, [extensions, arg, []]])
end
end
end
# our trampoline method which only performs processing if the extension matches
def process_trampoline(extensions, method, args)
extension = File.extname(original_filename).downcase
extension = extension[1..-1] if extension[0,1] == '.'
self.send(method, *args) if extensions.include?(extension)
end
You can then use this to call what used to be process
IMAGE_EXTENSIONS = %w(jpg jpeg gif png)
DOCUMENT_EXTENSIONS = %(exe pdf doc docm xls)
def extension_white_list
IMAGE_EXTENSIONS + DOCUMENT_EXTENSIONS
end
process_extensions IMAGE_EXTENSIONS, :resize_to_fit => [1024, 768]
For versions, there's a page on the carrierwave wiki that allows you to conditionally process versions, if you're on >0.5.4. https://github.com/jnicklas/carrierwave/wiki/How-to%3A-Do-conditional-processing. You'll have to change the version code to look like this:
version :big, :if => :image? do
process :resize_to_limit => [160, 100]
end
protected
def image?(new_file)
new_file.content_type.include? 'image'
end

Reprocessing images of different versions in Carrierwave

Using Carrierwave, I created 3 versions of an avatar - an original, a small_thumb and a large_thumb using the following lines:
process :resize_to_limit => [400, 400]
version :big_thumb do
process :resize_to_limit => [80, 80]
end
version :small_thumb do
process :resize_to_limit => [50, 50]
end
I added an additional method in my AvatarUploader class:
def reprocess(x,y,w,h)
manipulate! do |img|
img.crop(x.to_i, y.to_i, w.to_i, h.to_i, true)
end
resize_to_limit(180,180)
end
which is called in my model after an update is performed:
attr_accessor :crop_x, :crop_y, :crop_w, :crop_h
after_update :reprocess_image, :if => :cropping?
def cropping?
!crop_x.blank? && !crop_y.blank? && !crop_w.blank? && !crop_h.blank?
end
private
def reprocess_image
image.reprocess(crop_x,crop_y,crop_w,crop_h)
end
I have managed to crop and resize the original version, but I can't seem to update the 2 thumbnails along with it. I tried a few different techniques to no avail.
Any suggestions?
Try
image.recreate_versions!
Sorry, on the bus. I can't expound on that.
You need to call image.cache_stored_file! before calling recreate_versions!
It's weird because the method itself calls that if the file is cached, but for some reason it wasn't working.
So that would be something like:
def reprocess_image
image.reprocess(crop_x, crop_y, crop_w, crop_h)
image.cache_stored_file!
image.recreate_versions!
end
This HowTo was most helpful for me (even if I don't use fog):
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Recreate-and-reprocess-your-files-stored-on-fog
I added a reprocess method on my model and then called it in for each loop in my rake task:
def reprocess
begin
self.process_photo_upload = true
self.photo.cache_stored_file!
self.photo.retrieve_from_cache!(photo.cache_name)
self.photo.recreate_versions!
self.save!
rescue => e
STDERR.puts "ERROR: MyModel: #{id} -> #{e.to_s}"
end
end
Rake:
task :reprocess_photos => :environment do
MyModel.all.each{|mm| mm.reprocess}
end
PS: Rails 4.2
I haven't tried but maybe putting something like.
def reprocess_image
image.reprocess(crop_x,crop_y,crop_w,crop_h)
image.recreate_versions!
end
check this latest RailsCast:
http://railscasts.com/episodes/182-cropping-images-revised
after cropping one version of the image, you can then either calculate the cropping coordinates for the other versions, or probably easier, scale down the cropped image with the same aspect ratios as the other versions of the original image

Resources