ImageMagick scales down the processed images on Heroku - ruby-on-rails

Here is a simple raw Rails model with single url attribute.
class Photo < ActiveRecord::Base
validates :url, presence: true
def watermark
remote_photo = MiniMagick::Image.open(url).write("#{Rails.public_path}/photos/photo-#{id}.jpg")
photo = MiniMagick::Image.open("#{Rails.public_path}/photos/photo-#{id}.jpg")
marked_photo = photo.composite(mark, 'jpg') do |c|
c.gravity 'SouthEast'
c.geometry '+15'
end
marked_photo.write(marked_path)
File.delete("#{Rails.public_path}/photos/photo-#{id}.jpg")
end
def marked_path
"#{Rails.public_path}/photos/photo-marked-#{id}.jpg"
end
private
def mark(mark = 'mark.jpg')
#mark ||= MiniMagick::Image.open("#{Rails.public_path}/#{mark}")
end
end
The #watermark method puts a mark image to the bottom right of the photo and writes the file into public/photos/photo-marked-#{id}.jpg. It works perfectly on my local machine, however on Heroku the returned images that are ~600x800px become 10x15px.

Ok, the problem was in the way I've specified the geometry attribute value. In case of c.geometry '+15' ImageMagick takes the 15 as a width and resizes the image preserving the aspect ratio. In order to keep the original size and apply a horizontal offset the value should look like 'x+15', which is for widthxheight+offset.
All of the above is about the version 6.5.7 of ImageMagick, which is the one installed on Heroku. The initial '+15' works fine in the latest version.
Thanks to #mschoening, who helped me out with this.

Related

Can't see rotated picture after reloading page

I've made a rotate action for uploaded pictures using carrierwave. Pictures are rotated and saved correctly, BUT after they're saved, page is reloaded and I can't see rotated pictures. (in development this works, but not in production - Heroku) From S3 bucket I can see pictues are rotated correctly instantly. Only after I do in browser:
Open image in new tab > manually reload page > then I see correctly rotated picture.
How can I solve this? Thanks.
class Ad:
attr_accessor :rotate_picture_side
attr_accessor :picture_index
after_save {rotate_selected_picture if should_rotate_picture?}
private
def rotate_selected_picture
self.pictures[picture_index].recreate_versions!
end
def should_rotate_picture?
self.rotate_picture_side.present? && self.picture_index.present?
end
class PicturesController:
def update
#ad.rotate_picture_side = params[:rotate_picture_side]
#ad.picture_index = params[:id].to_i
#ad.save
redirect_to edit_ad_path(#ad, anchor: "rotate-picture #
{#ad.picture_index}")
end
class PicturesUploader:
def rotate_picture
manipulate! do |pic|
if model.rotate_picture_side == "right"
pic.rotate "90"
elsif model.rotate_picture_side == "left"
pic.rotate "270"
end
pic #returns the manipulated picture
end
end

Why won't Carrierwave and MiniMagick images composite correctly?

I am using carrierwave (0.9.0) with fog (1.18.0), mini_magick (3.6.0), and Rackspace CloudFiles. I want to composite images.
I have two models: Products and Emails. In the products uploader I am trying to create a process that loops through all the emails and composites the product image on top of each email.background_image. I also want to control the name of this image so that is:
"#{email.name}_#{product.product_number}.jpg"
I have included my process and method below. Test result in a file being uploaded to cloudfiles named "email_123.png" and the original image. The name doesn't currently include the email's name and images are not compositing, I just receive the original back. Anything obvious I am missing here? Thanks!
version :email do
process :email_composite
def email_composite
Email.all.each do |email|
email_image = email.secondary.url
email_name = email.name
merge(email_name, email_image)
end
end
def merge(email_name, email_image)
manipulate! do |img|
#product = Product.find(model.id)
email_image = ::MiniMagick::Image.open(email_image)
img = email_image.composite(img, "jpg") do |c|
c.gravity "Center"
end
# img = yield(img) if block_given?
img = img.name "#{email_name}_#{#product.product_number}.png"
img
end
end
end
UPDATED
First I wanted to thank you for your thorough answer, the amount of effort involved is obvious and appreciated.
I had a few more questions when trying to implement this solution.
Inside the #compose method you have:
email_image = ::MiniMagick::Image.open(email_image)
Should this be:
email_image = ::MiniMagick::Image.open(email_image_url)
I assume this from seeing the define_method call included below ( which was awesome by the way - I didn't know you could do that):
define_method(:email_image_url) { email.secondary.url }
After getting Undefined method "versions_for" I switched the #versions_for method as I will always want all email to have a secondary version.
versions_for before
def self.versions_for emails
reset_versions!
email.find_each { |e| version_for_email(e) }
end
versions_for after changes
def self.versions_for
reset_versions!
Email.all.find_each { |e| version_for_email(e) }
end
This change allowed me get rid of the error.
The end result of all of this is that everything appears as if it works, I can upload imagery without error, but now I have no resulting image for the emails versions. I still get my other versions like thumb etc. Any idea what could cause this? Again thanks for all the help you have provided thus far.
Calling manipulate! multiple times within the same version applies the changes in its block to the same image; what you want is to create many versions, one corresponding to each Email. This is tricky because CarrierWave really wants you to have a small set of statically defined versions in your code; it actually builds a new, anonymous Uploader class to handle each version.
We can trick it to build versions dynamically, but it's pretty ugly. Also, we have to be careful to not keep references to stale uploader classes around, or we'll accumulate classes endlessly and eventually run out of memory!
# in ProductUploader:
# CarrierWave stores its version classes in two places:
#
# 1. In a #versions hash, stored within the class; and
# 2. As a constant in your uploader, with a name based on its #object_id.
#
# We have to clean out both of them to be sure old versions get garbage
# collected!
def self.reset_versions!
versions.keys.select { |k| k =~ /^email_/ }.each do |k|
u = versions.delete(k)[:uploader]
remove_const("Uploader#{u.object_id}".gsub('-', '_'))
end
end
def self.version_name_for email
"email_#{email.name}".to_sym
end
# Dynamically generate the +version+ that corresponds to the image composed
# with the image from +email+.
def self.version_for_email email
version(version_name_for(email)) do
process :compose
# Use +define_method+ here so that +email+ in the block refers to the
# argument "email" from the outer scope.
define_method(:email_image_url) { email.secondary.url }
# Use the same trick to override the filename for this version.
define_method(:filename) { "#{email_name}_#{model.product_number}.png" }
# Compose +email_image+ on top of the product image.
def compose
manipulate! do |img|
email_image = ::MiniMagick::Image.open(email_image_url)
# Do the actual composition.
img = email_image.composite(img, "jpg") do |c|
c.gravity "Center"
end
img
end
end
end
end
# Clear out any preexisting versions and generate new ones based on the
# contents of +emails+.
def self.versions_for emails
reset_versions!
email.find_each { |e| version_for_email(e) }
end
# After you call Product.versions_for(emails), you can use this to fetch
# the version for a specific email:
def version_for_email email
versions[self.class.version_name_for(email)]
end
To use this, be sure to call Product.versions_for(Email.all) (possibly in a before_filter), Then, you can access the version for a specific email with #p.product.version_for(email):
# in your controller:
def action
# Recreate a +version+ for each email.
# If you don't want to overlay *every* email's image, you can also pass a
# subset here.
ProductUploader.versions_for(Email.all)
# Upload a file and its versions to Cloud Files...
#p = Product.find(params[:id])
#p.product = File.open(Rails.root.join('clouds.jpg'), 'r')
#p.save!
end
In your views, you can use url or any other helpers as usual:
# in a view:
<% Email.all.find_each do |email| %>
<%= image_tag #p.product.version_for_email(email).url %>
<% end %>

Carrierwave on the fly resize

I'm using carrierwave and I have this problem:
Suppose once the project has been delivered you need to add a section where the images in the system need to be displayed with a different size. I don' t want to regenerate the new dimension for each one of the images already in the system. I want to be able to generate (and cache it) whenever a view demands. Something like: " /> . If the new size 500x150 already exists, then returns the cached url, else generate it and return the cached url
I like pretty much Carrierwave but unfortunately doesn't have any on the fly resize feature out of the box. Everyone says it should be pretty simple add this feature but I found almost nothing. The only thing which goes pretty close is this uploader https://gist.github.com/DAddYE/1541912
I had to modify it to make it work so here is my version
class ImageUploader < FileUploader
include CarrierWave::RMagick
#version :thumb do
# process :resize_to_fill => [100,100]
#end
#
#version :thumb_square do
# process :resize_to_fill => [100,100]
#end
#
#version :full do
# process :resize_to_fit => [550, 550]
#end
def re_size(string_size)
if self.file.nil?
return self
end
begun_at = Time.now
string_size.gsub!(/#/, '!')
uploader = Class.new(self.class)
uploader.versions.clear
uploader.version_names = [string_size]
img = uploader.new(model, mounted_as)
img.retrieve_from_store!(self.file.identifier)
cached = File.join(CarrierWave.root, img.url)
unless File.exist?(cached)
img.cache!(self)
img.send(:original_filename=, self.file.original_filename)
size = string_size.split(/x|!/).map(&:to_i)
resizer = case string_size
when /[!]/ then :resize_to_fit
# add more like when />/ then ...
else :resize_to_fill
end
img.send(resizer, *size)
FileUtils.mv(img.file.file, cached)
#img.store!
end
img
end
def extension_white_list
%w[jpg jpeg gif png]
end
def filename
Digest::MD5.hexdigest(original_filename) << File.extname(original_filename) if original_filename
end
def cache_dir
"#{Rails.root}/tmp/uploads"
end
def default_url
'/general/no-image.png'
end
end
The problem with this version is that apparently when calling re_size("100x100").url, the url gets generated and returned before the actual resized image is created resulting in a page with broken links which displays good on any subsequent refresh.
Anyone achieved better results willing to share? :)
Please don't tell me to switch to Dragonfly. I'm using Carrierwave and i really like it. Also it seamlessly integrates with RailsAdmin which is part of my projects too.
Why don't you just generate a different version of the image, such as a thumbnail? In your image_uploader.rb
# Create different versions of your uploaded files:
include CarrierWave::RMagick
version :thumb do
process :resize_to_limit => [100, 100]
end
Then in your view just call
<%= image_tag nameofimage.image_url(:thumb).to_s %>
You could create multiple versions of our original image without resizing the original image. The processing is done by RMagick which you'll need to install.
RMagick requires you to have ImageMagick, so you'll need to install that as well. These can be a little tricky to get installed and working but well worth it. Plus the stackoverflow community has provided a lot of help with this issue.
Error installing Rmagick on Mountain Lion
rmagick gem install "Can't find Magick-config"

Adding tags to images in RefineryCMS

I'm trying to add tags to the Image model in RefineryCMS (trying on 1.0.8 and 2.0.4), have added attr_accessible :tag_list, required acts-as-taggable and setup the views, but the problem is that the tags only save when editing/updating a previously uploaded image - not when uploading for first time, even though it uses the same form...
Any ideas?
It happens on every version of rails and Refinery I have tried...
The tags are going through in the post when looking at logs, just not saving...
I had a similar issue and eventually find the cause of the extra-attributes (in your case the :tag_list) not being saved on new image upload.
If you look at ::Refinery::ImageController you'll see that the create action actyally create the image with :
unless params[:image].present? and params[:image][:image].is_a?(Array)
#images << (#image = ::Refinery::Image.create(params[:image]))
else
params[:image][:image].each do |image|
#images << (#image = ::Refinery::Image.create(:image => image))
end
end
params[:image][:image] is an Array when multiple multiple file uploed is enabled (by default it is). But then the action only use take the array values when creating the images, ignoring the other params.
I quickly write the below work-around that allow to save the other params on multiple image upload :
unless params[:image].present? and params[:image][:image].is_a?(Array)
#images << (#image = ::Refinery::Image.create(params[:image]))
else
images_params = params[:image].dup
images_params.delete(:image)
params[:image][:image].each do |image|
#images << (#image = ::Refinery::Image.create({:image => image}.merge(images_params)))
end
end
It's probably not the most elegant solution bu it does the trick.
To use it in your app, you'll have to create a decorator for the ::Refinery::ImageController to copy and edit the create action in it. (see 'Extending a Controller' in Refinery's Guides)

Accelerate S3 upload with paperclip

I'm using paperclip for uploading images in S3.
But I've noted that this upload is very slow. I think because before complete the submit the file has to pass by my server, be processed and be sent to the S3 server.
Is there a method for accelerate this?
thanks
You did not post any code so I'm going to make a few assumptions here:
in your project you have an Album and Image model
An Album has_many :images
You already have
paperclip and
aws-sdk
set up correctly with buckets and all else
You are uploading many images at once
In order to upload many images, your form will look something like this:
<%= form_for #album, html: { multipart: true } do |f| %>
<%= f.file_field :files, accept: 'image/png,image/jpeg,image/gif', multiple: true %>
<%= f.submit %>
<% end %>
Your controller will look something like this
class AlbumsController < ApplicationController
def update
#album = Album.find params[:id]
#album.update album_params
redirect_to #album, notice: 'Images saved'
end
def album_params
params.require(:album).permit files: []
end
end
In order to manipulate images using an album you'll need
class Album < ApplicationRecord
has_many :images, dependent: :destroy
accepts_nested_attributes_for :images, allow_destroy: true
def files=(array = [])
array.each do |f|
images.create file: f
end
end
end
Your Image file will look like this
class Image < ApplicationRecord
belongs_to :album
has_attached_file :file, styles: { thumbnail: '500x500#' }, default_url: '/default.jpg'
validates_attachment_content_type :file, content_type: /\Aimage\/.*\Z/
end
This is just the important stuff. With this setup, an upload of 22 images with a total of 12MB takes the :files= method 41.1806895 seconds to execute on average on my local server. To check how long a method takes to run, use:
def files=(array = [])
start = Time.now
array.each do |f|
images.create file: f
end
p "ELAPSED TIME: #{Time.now - start}"
end
You ask for a faster upload of many images. There are a few ways to do this. Using
jobs
won't work because you can't pass complex data like images to a job.
Use delayed_paperclip instead. It moves image styles creation (like thumbnail: '500x500#') into background jobs.
Gemfile
source 'https://rubygems.org'
ruby '2.3.0'
...
gem 'delayed_paperclip'
...
Image file
class Image < ApplicationRecord
...
process_in_background :file
end
It speeds up the :files= method. The same upload as before (22 images, 12MB) with this setup took 23.13998 seconds on my machine. That's 1.77963 times faster than before.
Another way of speeding things up is by using Threads. Remove delayed_paperclip from the Gemfile and the process_in_background :file line. Update your :files= method:
def files=(array = [])
threads = []
array.each do |f|
threads << Thread.new do
images.create file: f
end
end
threads.each(&:join)
end
You might try this, but get some weird error and only see that 4 images saved. You must also use Mutex. Also, you must not use :join on the threads because if you join, the method will wait until the threads are done running.
def files=(array = [])
semaphore = Mutex.new
array.each do |f|
Thread.new do
semaphore.synchronize do
images.create file: f
end
end
end
end
With this simple change to the method and no added gems, the same upload as before runs in 0.017628 seconds. That is 1,313 times faster than delayed_paperclip. It's also 2,336 times faster than the regular setup.
What happens if you use delayed_paperclip AND Threads?
Don't change the :files= method. Just turn delayed_paperclip back on in your Gemfile and add back the process_in_background :file line.
With this setup on my machine, the method runs in 0.001277 seconds on average. That's
13.8 times faster than Threads
18,120.6 times faster than delayed_paperclip
32,248.0 times faster than regular setup
Remember, this is on my machine and I have not tested this in production. I am also on wifi, not ethernet. All these things can change the results but I think the numbers speak for themselves.
Upload images faster. Done.
UPDATE: Don't use delayed_paperclip. It can cause a busy database, and some images might not get saved. I've tested it. I think just using threads is fast enough. Remove the process_in_background line from the Image file. Also, here's what my files= method looks like:
def files=(array = [])
Thread.new do
begin
array.each { |f| images.create file: f }
ensure
ActiveRecord::Base.connection_pool.release_connection
end
end
end
Note: Since we push the image saving to a background task and then redirect. The page that loads will not have images on them yet. The user has to
refresh
to update the page. One way around this is to use
polling.
Polling is when JavaScript checks for any changes every 5 seconds or so and makes changes if any to the page.
Another option is to use
Web Sockets.
Now that we have Rails 5, we can use ActionCable. Every time an image gets created, we broadcast an update for the album. If the user is on that page for that album, they will see updates happen as soon as they happen on the database without having the user refresh or the browser make a request every 5 seconds on an infinite loop.
Cool stuff.
Do you want to improve the appearance of the upload being faster or actually make the upload faster?
If it's the former you can put your image handling logic into a background task using something like delayed_job. This way when a user clicks the button they'll immediately go to their next page while you process the image (you can show a "processing in progress" image placeholder until the task is finished).
If it's the latter then it's entirely down to your server and internet connection. Where are you hosting?
How about uploading direct to S3?
Not sure if paperclip does this out of the box, but you could make it.
http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?UsingHTTPPOST.html
Use delayed jobs, this is a good example here
Or you can use flash upload.
If you end up going the route of uploading directly to S3 which offloads the work from your Rails server, please check out my sample projects:
Sample project using Rails 3, Flash and MooTools-based FancyUploader to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-FancyUploader
Sample project using Rails 3, Flash/Silverlight/GoogleGears/BrowserPlus and jQuery-based Plupload to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-Plupload
By the way, you can do post-processing with Paperclip using something like this blog post describes:
http://www.railstoolkit.com/posts/fancyupload-amazon-s3-uploader-with-paperclip
As cwninja recommends, we upload direct to s3 so as to get rid of this extra upload. We use a modified version of the plugin described in this blog post:
http://elctech.wpengine.com/2009/02/updates-on-rails-s3-flash-upload-plugin/
Ours is modified to handle multiple file uploads (rewrote the the flex object
Not sure how well this plays with paperclip, we use attachment_fu, but it wasn't so bad to get it to work with that.

Resources