Main Page Content
Image Manipulation With Cfmx And Jai
One of the most requested extensions to ColdFusion is the ability to do image
manipulation. Whether the request is as simple as just getting the rendered height and width of an image or as complex as creating a thumbnail in a different format, the Java Advanced Imaging API (JAI) may well hold the answer. In this article I will explain how to make use of JAI from CFMX to do the following four operations, thumbnail generation, format conversion, cropping, and border creation. Additionally, I will show how to obtain image properties such as the height and width of the rendered image.Before getting started using JAI, it is worth learning a little bit about
JAI. JAI is a set of interfaces that provide image manipulation for Java. JAI is an optional package that does not ship with Java 1.3. Since JAI is just a set of interfaces, an implementation of those interfaces is also required in order to make use of it. Sun provides a free implementation of the JAI interfaces along with the JAI package that you can download here. Sun does implement all of the interfaces, but may not provide all the functionality you are looking for. For example, the Sun interface can read BMP, JPG, GIF, TIF, and PNG image formats, while it can write DMP, JPG, TIF, PNG. If you need the ability to write GIF files than you will have to find another implementation (PNG is the generally accepted substitute for GIF).To easily make use of JAI I am going to create a Java class that accesses the JAI APIs and then a CFC that wraps my Java class. The first step is to create a shell class with the correct imports. I am also going to declare some private variables I�ll make use of later in the methods. The shell class is as follows.
import java.io.*;import java.util.*;import java.awt.image.renderable.*;import javax.media.jai.*;import com.sun.media.jai.codec.*;public class ImageUtils
{ private RenderedOp image = null; private RenderedOp result = null; private int height = 0; private int width = 0;}
After creating the shell class I am ready for my first method. Since all my image manipulation methods will depend on having an image loaded into memory, I will create a load method. Below is my method for loading the image into memory.
public void load(String file) throws IOException{ FileSeekableStream fss = new FileSeekableStream(file); image = JAI.create("stream", fss); height = image.getHeight(); width = image.getWidth();}
As you can see my method take a single parameter indicating the file it should
load into memory. This parameter needs to be the complete path to the file. First, I create a newFileSeekableStream
instance using the passed in path as a parameter. I then need to create an image stream. For convenience I will be using the provided static JAI factory. The static JAI.create
method’s first parameter is the type of object I want to create, while all the other parameters depend on what object I want to create. In this case I want to create a stream, so I pass it the FileSeekableStream
instance I just created. The JAI.create method
returns a RenderedOp
object. Now that I have my image in memory I am going to go ahead and get its height and width using the respective getHeight()
and getWidth()
methods of RenderedOp.No matter what type of image manipulation I want to do, I will always need
to write to disk the resulting image. In order to write an image to disk I will need to know what type of encoding to use as well as the name of the file to create. Below is the method I created for writing an image to disk.public void writeResult(String file, String type) throws IOException{ FileOutputStream os = new FileOutputStream(file); JAI.create("encode", result, os, type, null);}
With only two lines of code, the writeResult
method
JAI.create
method I encode my image by passing it a RenderedOp
(the image), the FileOutputStream
I just created, and the type of encoding to use. It then calls the encode method on my behalf, which write the image to disk in the appropriate format. Almost all of the popular image encodings are support except for GIF. Check the JAI documentation for a list of what encodings are supported. For the most part PNG is an acceptable substitute for GIF.Now that I have finished my writeResult
method
From here I can create new methods for each additional type of image manipulation
operation I want to support. As stated earlier, I also want to be able to generate thumbnails, crop images, and create borders. When creating thumbnails it is often easy to distort the image by not scaling each dimension according the image’s aspect ratio. For my thumbnail method I am going to accept a single number that represents what the longest edge of the resulting image should be. I will then scale the image according to its aspect ratio to the desired edge length. The following code is my thumbnail method.public void thumbnail(float edgeLength){ boolean tall = (height > width); float modifier = edgeLength / (float) (tall ? height : width); ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add(modifier);//x scale factor params.add(modifier);//y scale factor params.add(0.0F);//x translate params.add(0.0F);//y translate params.add(new InterpolationNearest());//interpolation method result = JAI.create("scale", params);}
My first step is to determine if the image is tall or wide. I do this simply
by seeing if the height is greater than the width. From there I create a modifier value based on the desired edge length divided by the longest edge. Now that I have my modifier value, I need to create aParameterBlock
to pass to the scale method. My first parameter is the image source. From there I add parameters for the x and y scale factor. Notice how I use the same x and y scale factor. This keeps the image from being distorted. The rest of the parameters aren’t so important for generating thumbnails and are more useful for scaling operations. If you are interested in different types of scaling operations the JAI documentation will describe how these additional parameters can be useful for you. After creating the ParameterBlock
I pass it to the static JAI.create
method, which calls scale and returns my result.For image cropping I decided to have my method crop the same amount for both
the height and width of the image. Thus, my method only takes a single parameter, how much edge to crop. The code for the method is below.public void crop(float edge){ ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add(edge);//x origin params.add(edge);//y origin params.add((float) width - edge);//width params.add((float) height - edge);//height result = JAI.create("crop", params);}
Again, I need to create a ParameterBlock
. My first
ParameterBlock
to the static JAI.create
method, which calls crop and returns my result.Much like the crop method, I decide to have my border method use the same
size border for each side of the image. Besides the width of the border, my method will also allow a color for the border to be specified. Thus, my method takes two parameters; the code is as follows.public void border(int edge, double edgeColor){ ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add(edge);//left pad params.add(edge);//right pad params.add(edge);//top pad params.add(edge);//bottom pad double fill[] = {edgeColor}; params.add(new BorderExtenderConstant(fill));//type params.add(edgeColor);//fill color result = JAI.create("border", params);}
Again, I create a ParameterBlock
and set the image
BorderExtenderConstant
and the color. There are more options for border file than using a single constant color, but that is out of the scope of this article. Again, the JAI documentation will provide the details of what additional border operations are possible and how to make use of them. Having created my ParameterBlock
, I pass it to the static JAI.create
method, which calls border and returns my result.With my Java class complete I can compile it. Make sure you include the JAI
jars in yourCLASSPATH
before attempting to compile it. Once my Java class is compiled I simply place in ColdFusion’s CLASSPATH
, so that I can make use of it from my CFC.To get started on my CFC, I am going to declare three variables in my component
body, iu, loaded, and result. The code for the three declarations is below.<cfobject type="java" name="iu" class="ImageUtils" action="create"><cfset loaded = false><cfset result = false>
As you can see, I named my Java class “ImageUtils”
<cfobject>
tag to create an instance of it. Since all of my CFC’s methods will be making use of the ImageUtils class I created the instance in my component body instead of in an individual method. My two other variables, loaded and result, are simply booleans representing the state of my CFC.Since all my CFC does is wrap my Java class, all the methods are very straight
forward. Each one is included below and briefly explained.<cffunction name="load" access="public"> <cfargument name="filename" type="string" required="true"> <cfscript> iu.load(arguments.filename); loaded = true; </cfscript></cffunction>
The above load method simple passes the file to load directly to ImageUtils
<cffunction name="writeResult" access="public"> <cfargument name="filename" type="string" required="true"> <cfargument name="type" type="string" required="true"> <cfif result> <cfscript> if(result) iu.writeResult(arguments.filename, arguments.type); </cfscript> </cfif></cffunction>
The above writeResult
method checks to see if a
ImageUtils writeResult
method.<cffunction name="thumbnail" access="public"> <cfargument name="edgeLength" type="numeric" required="true"> <cfif loaded> <cfscript> iu.thumbnail(arguments.edgeLength); result = true; </cfscript> </cfif></cffunction>
The above thumbnail method checks to see if an image has been loaded. Then
it calls theImageUtils
thumbnail method and sets the boolean result to true indicating that a result has been created.<cffunction name="crop" access="public"> <cfargument name="edge" type="numeric" required="true"> <cfif loaded> <cfscript> iu.crop(arguments.edge); result = true; </cfscript> </cfif></cffunction>
Just like the thumbnail method, the above crop method checks to see if an
image has been loaded. Then it calls theImageUtils
crop method and sets the boolean result to true indicating that a result has been created.<cffunction name="border" access="public"> <cfargument name="edge" type="numeric" required="true"> <cfargument name="edgeColor" type="numeric" required="true"> <cfif loaded> <cfscript> iu.border(arguments.edgeLength, arguments.edgeColor); result = true; </cfscript> </cfif></cffunction>
Finally, the above border method checks to see if an image has been loaded.
Then it calls theImageUtils
border method and sets the boolean result to true indicating that a result has been created.After creating my Java class and associated wrapper CFC, I am now able to
perform image manipulation from ColdFusion with ease. Further, both the Java class and the CFC are easily extendible to support additional operations that are implemented with JAI. While creating a wrapper CFC may seem like additional work for nothing, further additions to the Java class could prove challenging for ColdFusion to make use of. Since Java is a typed language and ColdFusion is typeless, it is often useful to have a wrapper class that can act as an adapter.