mga
mga

Reputation: 1970

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.

Upvotes: 2

Views: 3151

Answers (4)

user1697751
user1697751

Reputation: 21

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)

Upvotes: 2

masukomi
masukomi

Reputation: 10902

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.

Upvotes: 10

mga
mga

Reputation: 1970

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.

Upvotes: 5

muffinista
muffinista

Reputation: 6736

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.

Upvotes: 0

Related Questions