Skip to page content or skip to Accesskey List.
Search evolt.org
evolt.org login: or register

Work

Main Page Content

Image Manipulation with CFMX and JAI

Rated 3.74 (Ratings: 5) (Add your rating)

Log in to add a comment
(13 comments so far)

Want more?

 
Picture of DevilM

Matt Liotta

Member info | Full bio

User since: March 11, 2002

Last login: March 11, 2002

Articles written: 6

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 new FileSeekableStream 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 is quite simple. Using the static 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 I have actually gotten image format conversion for free. This is because I can load an image in any acceptable format and then write it as any of the supported encodings. For example, I could load a BMP image and then write it as a JPG thus converting the image from a bitmap to a jpeg.

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 a ParameterBlock 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 parameter is the image source. From there I need to add the x and y origins. The origin is where the cropping should start. Next, I add the width and height of crop. I determine the width and height by subtracting the origin from its respective edge. Since 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 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 source. From there I add a parameter for each side’s border width. Since I am using the same border width for all sides, this value is the same. Next I need to add the border color, which is done with two parameters, a 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 your CLASSPATH 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 &#8220;ImageUtils&#8221; and I am using the &lt;cfobject&gt; 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 and then sets the boolean loaded 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">
  <cfif result>
    <cfscript>
      if(result)
        iu.writeResult(arguments.filename, arguments.type);
    </cfscript>
  </cfif>
</cffunction>

The above writeResult method checks to see if a result has been created before calling the 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 the ImageUtils 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 the ImageUtils 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 the ImageUtils 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.

Matt Liotta started his development career at the age of twelve by building C applications for faculty at Emory University. He built his first web page soon after the release of Mosaic 1.0. Excited by early web applications, Matt saw the potential to replace legacy client server applications. At Emory University he built an enterprise calendaring system, the faculty poster project, a Y2K compliance tracking application, and a prototype for an electronic research administration system.

Since then he worked with an early ASP, Cignify, to build their transaction processing system for payroll time data. For this project, Matt created a message queuing system to connect significant bodies of code in C++ and VB with the main application server. He also built a code distribution system for Consumer Financial Networks, as well as the first online account management system for Grizzard Communications. Matt did consulting around San Francisco for companies such as Williams Sonoma and Yipes Communications.

Soon after, he built gMoney's Group Transaction System using an innovative XML messaging architecture for ColdFusion that matches conceptually with the now popular web services paradigm. He also wrote a C++ knapsack algorithm to realize nearly a 20-fold improvement over a similar approach written entirely in CFML. Later at TeamToolz, he designed a highly secure and scalable network architecture for ColdFusion to support N-tier transport agnostic distributed applications. He then went on to implement a cutting-edge content management system for DevX. He is now President & CEO of Montara Software, which he recently founded.

Matt is also a frequent speaker on web architecture:

  • Moving Legacy Applications to the Web (Emory Web Developers Users Group, Atlanta --Feb, 98)
  • The Benefits of Web-based Enterprise Calendaring (Emory Web Developers Users Group, Atlanta -- Aug, 98)
  • Monitoring and Managing Services Remotely Using TAPI (Atlanta Visual Basic Users Group, Atlanta -- Nov, 99)
  • Scalable, Extensible Cold Fusion Architecture (Bay Area ColdFusion Users’ Group, San Francisco; Aug, 00)
  • Scalable, Extensible Cold Fusion Architecture II (CF_Scale Conference, Washington, D.C. -- Nov, 00)
  • Cold Fusion Scalability Panel (CF_Scale Conference, Washington D.C. -- Nov, 00)
  • Introducing CF Espresso (including white paper) (CF_South Conference, Orlando -- Feb, 01)
  • Utilizing Reverse Proxies (Web Services World, San Jose -- Apr, 01)
  • Cold Fusion on Linux (A CF Odyssey Conference, Washington, D.C. -- Jun 01)
  • Architecting Web Services (Web Show 2001, San Francisco -- Sep, 01)
  • Code Techniques in MX Panel (Bay Area ColdFusion Users' Group, San Francisco -- Jul, 02)
  • ColdFusion Cruise, May, 03

com/sun/media/jai/codec/FileSeekableStream error

Submitted by pmccluskey on July 25, 2002 - 02:24.

Matt, I get the following error when trying to run the cfc: com/sun/media/jai/codec/FileSeekableStream. The class compiled ok. Any insights?

login or register to post comments

re: error

Submitted by DevilM on July 25, 2002 - 10:18.

You comment didn't include the error, so it is hard to respond directly. However, if the class compiled ok and you are just having trouble running it; then it may be that you forgot to put the two JAI jars in ColdFusion's classpath.

login or register to post comments

re: error

Submitted by pmccluskey on July 25, 2002 - 19:46.

That was it! Thanks for the great article.

login or register to post comments

Small issue with code.

Submitted by jedimaster on July 29, 2002 - 15:02.

Why does your writeResult method do:

<cfif result>
<cfscript>
if(result)

Why not simply check result once? As the subject says - this is a small issue. All in all - a very interesting article!

login or register to post comments

re: issue

Submitted by DevilM on July 29, 2002 - 17:03.

It was a mistake of course. Obviously, you only need to check the result boolean once.

login or register to post comments

How do I adapt this to work in CF5?

Submitted by ebrittwebb on August 29, 2002 - 19:06.

Don't have CFMX yet, but am desparately seeking the functionality you have described here. How can I adapt this to work in CF5?

login or register to post comments

Interpolation

Submitted by hibachiken on September 18, 2002 - 06:18.

no matter what Interpolation type i used in thumbnail method, the resulting image is full of "jaggies" even with bicubic, just wondering what have i done wrong and how can i get it to produce real smooth thumbnails

login or register to post comments

Not a Java guy...

Submitted by antistatic on October 21, 2002 - 23:51.

I was able to compile the shell class. But now I don't know what to do to implement the methods. Can anyone give me the next step?

login or register to post comments

Coldfusion classpaths

Submitted by wills on November 6, 2002 - 09:16.

I'm getting the same error as pmccluskey - what exactly does adding the .jar files to coldfusion's classpath require? I've tried administrator>server settings>java & jvm & adding the folder with the .jars in to class path there but it doesn't seem to make any difference?

login or register to post comments

An easier solution...

Submitted by antistatic on November 6, 2002 - 15:02.

I don't know if comments like this one are welcome here, but this is an issue I have struggled with for months. After beating my head into a wall trying to get JAI to work, I decided someone must have an easier solution.

This company makes a custom cfx_ tag, that calls a compiled DLL. It is fast, the results are beautiful, it caches the resized images, supports SMP, and it is dirt cheap. $149.00 You can download a free 30 day trial, and see for yourself.

http://efflare.com/products/cfx_imagecr/

BTW, I don't work for Efflare, or have any affiliation, except for being a satisfied customer.

login or register to post comments

Coldfusion 5 and JAI

Submitted by aktizol on March 3, 2003 - 05:55.

Does anybody know if this code works with Coldfusion 5 and how??? I'm using Coldfusion 5 and this doesn't work for me.. :(

login or register to post comments

Problem Compiling

Submitted by delatush on October 19, 2006 - 19:16.

Admittedly, I am very new to java, compiling etc.. However I believe this could be very useful in some of my CF apps.

When I try to compile the .java file from the code above, I error out with:

"java class or interface expected plublic void load(string file) throws ioexception."

any ideas?

Also, I am supposed to have this all in one .java file ?

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; }

public void load(String file) throws IOException { FileSeekableStream fss = new FileSeekableStream(file); image = JAI.create("stream", fss); height = image.getHeight(); width = image.getWidth(); }

public void writeResult(String file, String type) throws IOException { FileOutputStream os = new FileOutputStream(file); JAI.create("encode", result, os, type, null); }

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); }

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); }

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); }

login or register to post comments

Compiling Solution

Submitted by carmond on May 28, 2007 - 14:12.

I too am fairly new to Java. Here is what I was able to do to successfully compile the source code.

First, I set my CLASSPATH variable to the path that contained all of my JAI jar files that I downloaded from Sun.

Next, I had to unpack all of the jar files (jar xvf ___.jar).

Last, I put all of the methods within the class shell.

Probably not the standard way to do it, but it worked.

login or register to post comments

The access keys for this page are: ALT (Control on a Mac) plus:

evolt.orgEvolt.org is an all-volunteer resource for web developers made up of a discussion list, a browser archive, and member-submitted articles. This article is the property of its author, please do not redistribute or use elsewhere without checking with the author.