f all the extensions that developers would like to see for Macromedia's ColdFusion, one of the most requested is image manipulation. Whether it's to do something simple such as extract the rendered height and width of an image or something complex such as create a thumbnail in a different format, the secret to image manipulation in ColdFusion is the Java Advanced Imaging API (JAI). This article will demonstrate how to do four image manipulation operations, using JAI from ColdFusion MX: thumbnail generation, format conversion, cropping, and border creation. I'll also show how to obtain properties from a rendered image.
JAI is a set of interfaces whose specific role is to perform image manipulation for Java. It is an optional package that does not ship with Java 1.3, while it is included with Java 1.4. Because JAI is just a set of interfaces, an implementation of those interfaces is required in order to use it. Sun provides a free implementation of the JAI interfaces along with the JAI package that you can download. Sun's implementation addresses all of JAI's interfaces, but still this solution 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, and PNG. If you need the ability to write GIF files then you will have to find another implementation (PNG is the generally accepted substitute for GIF).
(In order to use JAI with ColdFusion it is necessary to add the two JAI jars to ColdFusion's classpath. This can be done through the CF Administrator under the Java and JVM settings section.)
Writing the First Method
To illustrate utilization of JAI, I need two things: a Java class that accesses the JAI APIs and a ColdFusion Component (CFC) that wraps that Java class. (For more information on CFCs, see Macromedia's ColdFusion MX Development Center. The first step is to create a shell class with the correct imports. Then I'll declare some private variables for later use. 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;
}
With the shell class completed, it's time to write the methods. All of the image manipulation methods in this article will require an image loaded into memory, therefore, the first step is to create a load method.
public void load(String file) throws IOException
{
FileSeekableStream fss = new FileSeekableStream(file);
image = JAI.create("stream", fss);
height = image.getHeight();
width = image.getWidth();
}
The load method takes a single parameter indicating the file it should load into memory. This parameter needs to be the complete path to the file. First, create a new FileSeekableStream instance using the passed-in path as a parameter. The next step is to create an image stream. For convenience, I decided to use the provided static JAI factory. The static JAI.create method's first parameter is the type of object you want to create and all the other parameters depend on upon it. For the example, I want to create a stream, so I pass the new FileSeekableStream instance. The JAI.create method returns a RenderedOp object. Now that the image is in memory, I can get its height and width using the getHeight() and getWidth() methods of RenderedOp.
No matter what type of image manipulation you want to do, you'll always need to write the resulting image to disk. In order to do that, you need to know what type of encoding to use and the name of the file to create. Here is a sample method 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);
}
The writeResult method is just two lines of code. You encode the image by passing it a RenderedOp (the image), the FileOutputStream, and the encoding type using the static JAI.create method. JAI supports all of the popular image encoding formats except GIF. Check the JAI documentation for a complete list.
Now that you've written the writeResult method you can load an image in any acceptable format and write it as any of the supported encodings. For example, you could load a BMP image and then write it as a JPG.
Creating Thumbnails
From here you can create new methods for each additional type of image manipulation operation you want to support, including thumbnail creation, cropping, and borders. When creating thumbnails it is 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 the maximum length of the longest edge of the resulting image. Then I can scale the image to the desired edge length according to its aspect ratio. Here is the 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);
}
The first step is to determine whether the image is horizontally or vertically oriented, by checking whether the height is greater or the width. From there create a modifier value based on the desired edge length divided by the longest edge and then create a ParameterBlock to pass to the scale method. In my code, the first parameter is the image source. From there I add parameters for the x and y scale factor, which I deliberately made the same to prevent distortion. The rest of the parameters are useful for scaling operations but aren't so important for generating thumbnails. (If you are interested in different types of scaling operations, the JAI documentation describes how these additional parameters can be useful for you.) After creating the ParameterBlock, pass it to the static JAI.create method, which calls scale and returns a result.
For image cropping I decided to write a method that crops the same amount from both the height and width of the image. Thus, it takes only a single parameter: how much edge to crop. The code for the method is:
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);
}
Here, too, you'll need a ParameterBlock. My first parameter is the image source. From there, I add the x and y origins, which is where the cropping should begin. Next, I add the width and height of crop, which I determine by subtracting the origin from its respective edge. Because I am cropping the same amount for both the height and the width, I use the same value for x and y and thus for subtracting from the width and height. Finally, I pass the ParameterBlock to the static JAI.create method, which calls crop and returns a result.
Bordering on Brilliant
To create borders on images, I decided to write a method that controlled two properties: border width and color. Thus, my method takes two parameters:
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, you need a ParameterBlock to set the image source. Then add a parameter for each side's border width. I decided to use the same border width for all sides, so the value is the same. Next comes the selection of the border color, which is done with two parameters: a BorderExtenderConstant and the color. (Though not covered here, there are other options besides a single constant border color. See the JAI documentation for details.). The code passes the ParameterBlock to the static JAI.create method, which calls the border and returns a result.
The class is now complete, but you want to make sure you include the JAI jars in your CLASSPATH before attempting to compile it. Because I want to use it from a ColdFusion Component (CFC), I place the compiled class in ColdFusion's CLASSPATH.
The CFC requires three declared variables: iu, loaded, and result. The code for the three declarations is:
<cfobject type="java" name="iu" class="ImageUtils" action="create">
<cfset loaded = false>
<cfset result = false>
I named the Java class "ImageUtils" and used the <cfobject> tag to create an instance of it. Because all of the CFC's methods will use of the ImageUtils class, I created the instance in my component body instead of in an individual method. The other two variables, loaded and result, are simply booleans representing the state of the CFC.
All the CFC does is wrap the ImageUtils class, so all the methods are very straightforward. But you should have a look at each one in turn.
<cffunction name="load" access="public">
<cfargument name="filename" type="string" required="true">
<cfscript>
iu.load(arguments.filename);
loaded = true;
</cfscript>
</cffunction>
The load method (above) simply passes the file directly to ImageUtils and then sets the boolean to true, indicating that an image has been loaded.
<cffunction name="writeResult" access="public">
<cfargument name="filename" type="string" required="true">
<cfargument name="type" type="string" required="true">
<cfscript>
if(result)
iu.writeResult(arguments.filename, arguments.type);
</cfscript>
</cffunction>
The writeResult method checks to see if a result has been created before calling the writeResult method in the ImageUtils class.
<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 thumbnail method checks to see if an image has been loadedand then calls the ImageUtils thumbnail method and sets the boolean 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 crop method checks to see if an image has been loaded. Then it calls the ImageUtils crop method and sets the boolean 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 border method checks to see if an image has been loaded, calls the ImageUtils border method, and sets the boolean result to true, indicating that a result has been created.
Together, the Java class created in this article and the associated wrapper CFC, enables several types of image manipulation from ColdFusion very easily. 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 unnecessary additional work, extending the Java class further would make it more difficult for ColdFusion to use it, and the wrapper acts as an adapter that will extend the life of your class.
 |
Matt Liotta has been a software architect for several start-ups in San Francisco and Atlanta. He is now President and CEO of Montara Software, which he founded to further education with technology. He can be reached at .
|
|