Tuesday, 9 June 2009

Creating Images For Rails With Java

Some time ago I had a look at RMagick to create images on-the-fly in a Rails application. The problem with RMagick is that it is not compatible with JRuby, and if you are going to be using TomCat as your webserver, you need to be using JRuby. However, JRuby does present a ready alternative through Java.

To look at this I created a new controller, images_controller, in an existing Rails project. In hindsight, images might have been a bad name, as there is a folder called public/images already. However, Rails was able to work out whether the image should come from public/images or via my images controller, so the issue is really only whether it is confusing to the developer.

The first thing you need to do is set up the new MIME types. There are various places you can do this, but the correct one, I believe, is in config/initializers/mime_types.rb. This is what I added:
Mime::Type.register "image/jpg", :jpg
Mime::Type.register "image/png", :png
Mime::Type.register "image/gif", :gif

Now let us create the controller. The first thing you need to do is include Java. Then you should import all the classes you will need. The second step is optional, but the alternative is writing out the full name of he Java classes every time you use them, which leads to longer and harder to read code. Note that unlike in Java you cannot use the * wildcard to import a whole bunch of classes in one line. Here is the start of my controller:
class ImagesController < ApplicationController
include Java

java_import java.io.ByteArrayOutputStream
java_import java.io.File

java_import java.awt.image.BufferedImage
java_import java.awt.Color
java_import java.awt.RenderingHints
java_import java.awt.geom.GeneralPath
java_import java.awt.Font

java_import javax.imageio.ImageIO
end

Now I need an action defined by a method, image. A clever feature of Rails is that you can respond differently depending on the file extention, which is what I do here. The respond_to block directs the action to the correct format (as long as the format is a known MIME type).

The HTML response is blank. Rails will do the default action, which is to return a page derived from the view images/image.html.erb. The other three responses follow the same format. Each invokes the send_data method, with the data (the image) as the first parameter, followed by a hash of options. The :type is simply the MIME type. The :disposition determines if the data is to be displayed ("inline"), or downloaded ("attachment"). For attachments, you can set a :filename parameter too, and there is also a :status parameter, which conveniently defaults to 200, success.

API for send_data:
http://api.rubyonrails.org/classes/ActionController/Streaming.html
def image
@name = params[:id]

respond_to do |format|
format.html
format.jpg do
send_data get_jpeg(@name), :type => "image/jpeg",
:disposition => "inline"
end
format.png do
send_data get_png(@name), :type => "image/png",
:disposition => "inline"
end
format.gif do
send_data get_gif(@name), :type => "image/gif",
:disposition => "inline"
end
end
end

So that just leaves the methods that generate the image data. These should all be protected, by the way.

This first method simply grabs a file from public/images and displays it. Once it has the file, it creates an output stream, and writes the image file to that stream in the specified format. It then converts from the stream to a Ruby string, via a Java byte array.
def get_jpeg filename
imagefile = File.new("#{RAILS_ROOT}/public/images/#{filename}.jpg")
os = ByteArrayOutputStream.new
ImageIO.write(ImageIO.read(imagefile), "jpeg", os)
String.from_java_bytes(os.toByteArray)
end

Converting to another format is trivial (though noticeably slower).
def get_png filename
imagefile = File.new("#{RAILS_ROOT}/public/images/#{filename}.jpg")
os = ByteArrayOutputStream.new
ImageIO.write(ImageIO.read(imagefile), "png", os)
String.from_java_bytes(os.toByteArray)
end

This version does an operation, converting the image to its negative.
def get_jpeg filename
imagefile = File.new("#{RAILS_ROOT}/public/images/#{filename}.jpg")
bi = ImageIO.read(imagefile)
big = bi.getGraphics

# Create a look-up table as an array
lut = Array.new(256) { |j| 256 - j }
# Convert to Java byte array
jlut = lut.to_java :byte
# Convert to Java byte look-up table
blut = java.awt.image.ByteLookupTable.new(0, jlut)
# Convert to Java operation look-up table
op = java.awt.image.LookupOp.new(blut, nil)
# Apply the operation
dest = op.filter(bi, nil)
# Draw the new image
big.drawImage(dest, 0, 0, nil);
# Create an output stream, and write the image to it
os = ByteArrayOutputStream.new
ImageIO.write(dest, "jpeg", os)
String.from_java_bytes(os.toByteArray)
end

The next method actually generates a GIF image on-the-fly, putting the filename in the middle of a simple image. The basic methodology is to create a blank BufferedImage object (rather than from a file), use that to create a Graphics2d object (just as before). The image is built by calling methods on the Graphics2d object, and finally the last three lines create the output stream and write the image to it (just as before).
WIDTH = 400
HEIGHT = 300

def get_gif filename
off_image = BufferedImage.new(WIDTH, HEIGHT,
BufferedImage::TYPE_INT_ARGB)

g2 = off_image.createGraphics()
g2.setRenderingHint(RenderingHints::KEY_ANTIALIASING,
RenderingHints::VALUE_ANTIALIAS_ON)

# Set up background
g2.setPaint(Color.lightGray)
g2.draw3DRect(0, 0, WIDTH - 1, HEIGHT - 1, true);
g2.draw3DRect(3, 3, WIDTH - 7, HEIGHT - 7, false);

x = 7;
y = 7;
x3_points = [x, WIDTH - 2 * x, x, WIDTH - 2 * x]
y3_points = [y, HEIGHT - 2 * y, HEIGHT - 2 * y, y]
filled_polygon = GeneralPath.new(GeneralPath::WIND_EVEN_ODD,
x3_points.length);
filled_polygon.moveTo(x3_points[0], y3_points[0])
(1...x3_points.length).each do |index|
filled_polygon.lineTo(x3_points[index], y3_points[index])
end
filled_polygon.closePath()
g2.setPaint(Color.red)
g2.fill(filled_polygon)
g2.setPaint(Color.black)
g2.draw(filled_polygon)
g2.setPaint(Color.yellow)
g2.setFont(Font.new("Helvetica", Font::PLAIN, 22))
g2.drawString(filename, WIDTH / 2, HEIGHT / 2)

os = java.io.ByteArrayOutputStream.new
ImageIO.write(off_image, "gif", os)
String.from_java_bytes(os.toByteArray)
end

Okay, so we can generate images; how do we get them on to a web browser? The easy way (to test the above work) is to invoke it though the address bar. Say I have an image saved in public/images called sheep.jpg, I can use the following:
http://localhost:3000/images/image/sheep.jpg
# => Gives a negative of the image
http://localhost:3000/images/image/sheep.png
# => Gives the PNG converted image
http://localhost:3000/images/image/sheep.gif
# => Gives the generated GIF with "sheep" written on it

What we really want to to have those images embedded in a web page. So here is a view, views/images/image.html.erb, that will do just that:
<h1>My <%= @name %> image</h1>

<%# This image has been generated on the fly and served via my controller %>
<%= image_tag "/images/image/#{@name}.gif", :size => '400x300' %>

<%# This image has been processed and served via my controller %>
<%= image_tag "/images/image/#{@name}.jpg", :size => '400x300' %>

<%# This image has been processed and served via my controller; uses HTML tags directly %>
<img alt="Sheep" height="300" src="/images/image/<%= @name %>.jpg" width="400" />

<%# This image has been processed and served via my controller as a PNG %>
<%= image_tag "/images/image/#{@name}.png", :size => '400x300' %>

<%# This image came directly from public/images %>
<%= image_tag "/images/#{@name}.jpg", :size => '400x300' %>

<%# This image has been processed and served via my controller, va a helper method %>
<%= images_image_tag @name, :jpg, :size => '400x300' %>

One of those methods uses a helper method, and that would be my prefered way. Here is that method:
def images_image_tag name, type, options = {}
image_tag "/images/image/#{@name}.#{type.to_s}", options
end

Struggling with Ruby: Contents Page

No comments: