Fast way to get remote image dimensions - ruby-on-rails

I'm using the imagesize gem to check the sizes of remote images and then only push images that are big enough into an array.
require 'open-uri'
require 'image_size'
data = Nokogiri::HTML(open(url))
images = []
forcenocache = Time.now.to_i # No cache because jquery load event doesn't fire for cached images
data.css("img").each do |image|
image_path = URI.join(site, URI.encode(image[:src]))
open(image_path, "rb") do |fh|
image_size = ImageSize.new(fh.read).get_size()
unless image_size[0] < 200 || image_size[1] < 100
image_element = "<img src=\"#{image_path}?#{forcenocache}\">"
images.push(image_element)
end
end
end
I tried using JS on the front-end to check image dimensions but there seems to be a browser limit to how many images can be loaded at once.
Doing it with imagesize is much slower than using JS. Any better and faster ways to do this?

I think this gem does what you want https://github.com/sdsykes/fastimage
FastImage finds the size or type of an
image given its uri by fetching as
little as needed

Related

Carrierwave - Multiple images upload too slow when adding or removing images

I'm following this article in CarrierWave Wiki https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Add-more-files-and-remove-single-file-when-using-default-multiple-file-uploads-feature to implement adding more images and removing images for a model in my system using CarrierWave Multiple Uploads feature.
The main code in that article is
def add_more_images(new_images)
images = #gallery.images
images += new_images
#gallery.images = images
end
def remove_image_at_index(index)
remain_images = #gallery.images # copy the array
deleted_image = remain_images.delete_at(index) # delete the target image
deleted_image.try(:remove!) # delete image from S3
#gallery.images = remain_images # re-assign back
end
It works. However, it is too slooooow. I have looked at the log and the overall processing time is as follow:
Upload 1 image: it takes 5000ms for example
Add 1 more image: it takes 8500ms (2 images)
Add 1 more image: it takes 12000ms (3 images)
Remove 1 image: it takes 8400ms (back to 2 images)
I have tested the sample app of this solution written by the author on my local machine and it was very slow too.
It seems like CarrierWave reuploads and re-processes all images even we only add or remove 1 image. I think because we are re-assigning back new array of images to #gallery so that it treats old images as new ones.
Also there is a related issue here https://github.com/carrierwaveuploader/carrierwave/issues/1704#issuecomment-259106600
Does anyone have any better solution for adding and removing images using CarrierWave multiple upload feature?
Thanks.
when you call model.images = remain_images, carrierwave will upload all images. So the more images you stored in a column, the longer it will take.
See: mount.rb#L300, mounter.rb#L40
I had this problem before, the following is my code:
new_images = self.logo_images.clone
4.times do |t|
next if !(image = params[:"logo_image#{t + 1}"])
new_images[t] = image
changed = true
end
self.logo_images = new_images if changed
...
self.save if changed
And this is the hack...
(works fine with carrierwave 1.0.0 and carrierwave-aws 1.1.0)
mounter = self.send(:_mounter, :logo_images)
4.times do |t|
next if !(image = params[:"logo_image#{t + 1}"])
uploader = mounter.blank_uploader
uploader.cache!(image)
mounter.uploaders[t] = uploader
changed = true
end
mounter.uploaders.each{|s| s.send(:original_filename=, s.file.filename) if !s.filename} if changed
...
self.save if changed

Get image dimensions using Refile

Using the Refile gem to handle file uploading in Rails, what is the best way to determine image height and width during / after it has been uploaded? There is no built in support for this AFAIK, and I can't figure out how to do it using MiniMagick.
#russellb's comment almost got me there, but wasn't quite correct. If you have a Refile::File called #file, you need to:
fileIO = #file.to_io.to_io
mm = MiniMagick::Image.open(fileIO)
mm.width # image width
mm.height # image height
Yes, that's two calls to #to_io >...< The first to_io gives you a Tempfile, which isn't what MiniMagick wants. Hope this helps someone!
-- update --
Additional wrinkle: this will fail if the file is very small (<~20kb, from: ruby-forum.com/topic/106583) because you won't get a tempfile from to_io, but a StringIO. You need to fork your code if you get a StringIO and do:
mm = MiniMagick::Image.read(fileio.read)
So my full code is now:
# usually this is a Tempfile; but if the image is small, it will be
# a StringIO instead >:[
fileio = file.to_io
if fileio.is_a?(StringIO)
mm = MiniMagick::Image.read(fileio.read)
else
file = fileio.to_io
mm = MiniMagick::Image.open(file)
end
Refile attachments have a to_io method (see Refile::File docs) which returns an IO object that you can pass to MiniMagick.
Assuming you have an Image model with a file attachment (id stored in a file_id string column) and width and height columns you can use the following callback:
class Image < ActiveRecord::Base
attachment :file
before_save :set_dimensions, if: :file_id_changed?
def set_dimensions
image = MiniMagick::Image.open(file.to_io)
self.width = image.width
self.height = image.height
end
end
Hope that helps.
You can use MiniMagick to do this (but need to be using the latest version).
image = MiniMagick::Image.open('my_image.jpg')
image.height #=> 300
image.width #=> 1300
This is all pretty well documented in the README.md for the gem: https://github.com/minimagick/minimagick

Watermarking with MiniMagick

Versions:
Ruby 2.2.3
Rails 4.2.4
mini_magick: 4.2.10
Carrierwave 0.10.0
Description
I am trying to create a watermarker for a small gallery using CarrierWave as uploader.
I want the watermark to be sized compared to the current image. Therefore I am trying to use a .svg-file with different opacities and a transparent background.
I am using a watermarker based on Carrierwave add a watermark to processed images
require 'mini_magick'
class Watermarker
def initialize(original_path, watermark_path)
#original_path = original_path.to_s
#watermark_path = watermark_path.to_s
end
def watermark!(options = {})
options[:gravity] ||= 'SouthEast'
image = MiniMagick::Image.open(#original_path)
watermark = MiniMagick::Image.open(#watermark_path)
result = image.composite(watermark, 'png') do |c|
c.gravity options[:gravity]
end
result.write #original_path
end
end
And calling this as a process from my uploader.
My problems:
I cannot get the watermarker to input the picture with transparent background. I played around with:
https://github.com/minimagick/minimagick#composite
http://www.imagemagick.org/script/composite.php
But no progress.
I cannot adjust the size of the overlay image properly. There is a lot of settings for the geometry command but I'm stuck.
Any ideas and help would be great.

Ruby, RSVG and PNG streams

I'm trying to do an image conversion in a rails app from SVG to PNG. ImageMagick didn't work out for me, due to Heroku not able / wanting to upgrade IM at this time. I'm testing out some ideas of using RSVG2 / Cairo in dev but running into a roadblock.
I can easily convert and save the SVG to PNG like this:
#svg_test.rb
require 'debugger'
require 'rubygems'
require 'rsvg2'
SRC = 'test.svg'
DST = 'test.png'
svg = RSVG::Handle.new_from_file(SRC)
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, 800, 800)
context = Cairo::Context.new(surface)
context.render_rsvg_handle(svg)
surface.write_to_png(DST)
But this only lets me write PNG files out. In the app, I need to be able to generate these on the fly, then send them down to the client browser as data. And I can't figure out how to do this, or even if its supported. I know I can call surface.data to get the raw data at least, but I don't know enough about image formats to know how to get this as a PNG.
Thanks
Ah ha! I was so close and its pretty obvious in hindsight. Simply call the surface.write_to_png function with a StringIO object. This fills the string object, which you can then get the bytes for. Here's the finished svg_to_png function I wrote, along with a sample controller that calls it. Hope this helps someone else somewhere.
ImageConvert function:
def self.svg_to_png(svg)
svg = RSVG::Handle.new_from_data(svg)
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, 800, 800)
context = Cairo::Context.new(surface)
context.render_rsvg_handle(svg)
b = StringIO.new
surface.write_to_png(b)
return b.string
end
Test controller:
def svg_img
path = File.expand_path('../../../public/images/test.svg', __FILE__)
f = File.open(path, 'r')
t = ImageConvert.svg_to_png(f.read)
send_data(t , :filename => 'test.png', :type=>'image/png')
end

upload an RMagick-generated file from Heroku to Amazon S3

I am creating a Rails app which is hosted on Heroku and that allows the user to generate animated GIFs on the fly based on an original JPG that's hosted somewhere in the web (think of it as a crop-resize app). I tried Paperclip but, AFAIK, it does not handle dynamically-generated files. I am using the aws-sdk gem and this is a code snippet of my controller:
im = Magick::Image.read(#animation.url).first
fr1 = im.crop(#animation.x1,#animation.y1,#animation.width,#animation.height,true)
str1 = fr1.to_blob
fr2 = im.crop(#animation.x2,#animation.y2,#animation.width,#animation.height,true)
str2 = fr2.to_blob
list = Magick::ImageList.new
list.from_blob(str1)
list.from_blob(str2)
list.delay = #animation.delay
list.iterations = 0
That is for the basic creation of a two-frame animation. RMagick can generate a GIF in my development computer with these lines:
list.write("#{Rails.public_path}/images/" + #animation.filename)
I tried uploading the list structure to S3:
# upload to Amazon S3
s3 = AWS::S3.new
bucket = s3.buckets['mybucket']
obj = bucket.objects[#animation.filename]
obj.write(:single_request => true, :content_type => 'image/gif', :data => list)
But I don't have a size method in RMagick::ImageList that I can use to specify that. I tried "precompiling" the GIF into another RMagick::Image:
anim = Magick::Image.new(#animation.width, #animation.height)
anim.format = "GIF"
list.write(anim)
But Rails crashes with a segmentation fault:
/path/to/my_controller.rb:103: [BUG] Segmentation fault ruby 1.8.7 (2010-01-10 patchlevel 249) [universal-darwin11.0]
Abort trap: 6
Line 103 corresponds to list.write(anim).
So right now I have no idea how to do this and would appreciate any help I receive.
As per #mga's request in his answer to his original question...
a non-filesystem based approach is pretty simple
rm_image = Magick::Image.from_blob(params[:image][:datafile].read)[0]
# [0] because from_blob returns an array
# the blob, presumably, can have multiple images data in it
a_thumbnail = rm_image.resize_to_fit(150, 150)
# just as an example of doing *something* with it before writing
s3_bucket.objects['my_thumbnail.jpg'].write(a_thumbnail.to_blob, {:acl=>:public_read})
Voila! reading an uploaded file, manipulating it with RMagick, and writing it to s3 without ever touching the filesystem.
Since this project is hosted in Heroku I cannot use the filesystem so that is why I was trying to do everything via code. I found that Heroku does have a temporary-writable folder: http://devcenter.heroku.com/articles/read-only-filesystem
This works just fine in my case since I don't need the file after this request.
The resulting code:
im = Magick::Image.read(#animation.url).first
fr1 = im.crop(#animation.x1,#animation.y1,#animation.width,#animation.height,true)
fr2 = im.crop(#animation.x2,#animation.y2,#animation.width,#animation.height,true)
list = Magick::ImageList.new
list << fr1
list << fr2
list.delay = #animation.delay
list.iterations = 0
# gotta packet the file
list.write("#{Rails.root}/tmp/#{#animation.filename}.gif")
# upload to Amazon S3
s3 = AWS::S3.new
bucket = s3.buckets['mybucket']
obj = bucket.objects[#animation.filename]
obj.write(:file => "#{Rails.root}/tmp/#{#animation.filename}.gif")
It would be interesting to know if a non-filesystem-writing solution is possible.
I am updating this answer for AWS SDK Version 2 which should be:
rm_image = Magick::Image.from_blob(params[:image][:datafile].read)[0]
# [0] because from_blob returns an array
# the blob, presumably, can have multiple images data in it
a_thumbnail = rm_image.resize_to_fit(150, 150)
# just as an example of doing *something* with it before writing
s3 = Aws::S3::Resource.new
bucket = s3.bucket('mybucket')
obj = bucket.object('filename')
obj.put(body: background.to_blob)
I think there's a few things going on here. First, the documentation for RMagick is sub-par, and its easy to get side-tracked. The code you're using to generate the gif can be a little simpler. I cooked up a very contrived example here:
#!/usr/bin/env ruby
require 'rubygems'
require 'RMagick'
# read in source file
im = Magick::Image.read('foo.jpg').first
# make two slightly different frames
fr1 = im.crop(0, 100, 300, 300, true)
fr2 = im.crop(0, 200, 300, 300, true)
# create an ImageList
list = Magick::ImageList.new
# add our images to it
list << fr1
list << fr2
# set some basic values
list.delay = 100
list.iterations = 0
# write out an animated gif to the filesystem
list.write("foo.gif")
This code works -- it reads in a jpg I have locally, and writes out a 2-frame animation. Obviously I've hardcoded some values here, but there's no reason this shouldn't work for you, although I am running ruby 1.9.2 and probably a different version of RMagick, but this is basic code.
The second issue is totally unrelated -- is it possible to upload an image generated in IM to S3 without actually hitting the filesystem? Basically, will this ever work:
obj.write(:single_request => true, :content_type => 'image/gif', :data => list)
I'm not sure if it is or not. I experimented with calling list.to_blob, but it only outputs the first frame, and it's as a JPG, although I didn't spend much time on it. You might be able to fool list.write into outputting somewhere, but rather than going down that road, I would personally just output the file unless that is impossible for some reason.

Resources