Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Michael Evangelista

Image Manipulation ColdFusion Wrapper Component

By Ben Nadel on
Tags: ColdFusion

Back when Pete Freitag came to talk at the New York ColdFusion User Group, Peter Bell asked if there was a way to treat the ColdFusion image object as a more traditional object and call methods on it like Image.GetWidth() and Image.GetHeight(). Technically, you can do this, but it is not documented and leverages the underlying Java object. Of course, you could create a ColdFusion component wrapper that had all of the desired methods, but with over 50 new image manipulation functions, who would want to take the time to write it.

I knew with the highly dynamic and extremely powerful nature of ColdFusion 8, there had to be a way to write this without writing it. The key to my mad scheme was ColdFusion 8's new OnMissingMethod() functionality. If we would could trap the image method calls to our ColdFusion component wrapper, then we should be able to easily reroute the request with the proper names and proper arguments.

It took a very short time to write this component - I actually wrote the whole thing before testing it. But, when I went to fire it up, I started getting "null null" errors. This is when I discovered that ColdFusion doesn't like to Evaluate() built-in methods that return VOID:

  • Evaluate( "ImageBlur( Image )" )

I messed around with this issue for a while and finally, with great help of Elliott Sprehn, I was able to get this line to work, not through the standard built-in method call, but rather through the Page object gotten via GetPageContext():

  • Evaluate( "GetPageContext().GetPage().ImageBlur( Image )" )

For some reason, this worked fine even though ImageBlur() was returning null. Not sure why this one was successful while the one before it was bombing out - they are the same function after all.

Once I got over that hurdle, the ImageWrapper.cfc was completed. It is just a very thin decorator that wraps around an existing ColdFusion image object and returns the THIS reference whenever possible in order to allow for the chaining of methods calls. Let's take a look at an example:

  • <!---
  • Read in the image and then decorate it with an
  • ImageWrapper instance.
  • --->
  • <cfset objImage = CreateObject( "component", "ImageWrapper" )
  • .Init(
  • ImageNew( "./kissy_face.jpg" )
  • )
  • />
  •  
  • <!--- Manipulate the image. --->
  • <cfset objImage
  • .Crop( 31, 31, 576, 576 )
  • .SetAntialiasing( "on" )
  • .ScaleToFit( 535, 535 )
  • .SetDrawingTransparency( 50 )
  • .DrawRect( 0, 505, 535, 35, true )
  • .SetDrawingTransparency( 0 )
  • .SetDrawingColor( "##333333" )
  • .DrawText( "Image Wrapper Examle - Kinky Solutions", 10, 525 )
  • .DrawText( "Kiss Kiss Kiss!", 440, 525 )
  • .AddBorder( 5, "##454545" )
  • />
  •  
  • <!--- Write resultant image to browser. --->
  • <cfimage
  • action="writetobrowser"
  • source="#objImage.Get()#"
  • />

As you can see, when we create an instance of the ImageWrapper.cfc, we are passing it a new ColdFusion image object. Then, we proceed to call multiple image manipulation methods on this object in a chained fashion. We are able to do this because each call to the wrapper attempts to return THIS if no other valid value can be returned (as would be the case with GetWidth()).

Running the above code, we get the following image:


 
 
 

 
Image Manipulation Wrapper Example - Hot Girl Giving Kisses  
 
 
 

Now, did we write all those methods out? Absolutely not! I am using OnMissingMethod() to capture and reroute all these methods calls. The ImageWrapper.cfc is actually very light weight and leverages the fact that the Image functions all have a set pattern:

  • They all start with "Image"
  • They all take the ColdFusion image object as their first argument

Once we accept those facts, there really is nothing to it:

  • <cfcomponent
  • output="false"
  • hint="Wraps an image to provide dot-notation image functionality and chaining.">
  •  
  • <!--- Our target image. --->
  • <cfset VARIABLES.Image = "" />
  •  
  • <!---
  • The page context object that will give us access to the built-in
  • CF functions that will be used in dynamic evaluation.
  • --->
  • <cfset VARIABLES.Page = GetPageContext().GetPage() />
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Returns an initialized image wrapper.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Image"
  • type="any"
  • required="true"
  • hint="The ColdFusion image object that we are going to be modifying."
  • />
  •  
  • <!--- Store the image. --->
  • <cfset VARIABLES.Image = ARGUMENTS.Image />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Get"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Returns the target image.">
  •  
  • <cfreturn VARIABLES.Image />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnMissingMethod"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Handles missing methods that allow us to trap and leverage the ColdFusion image functionality with as little effort as possible.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="MissingMethodName"
  • type="string"
  • required="true"
  • hint="The method that was called."
  • />
  •  
  • <cfargument
  • name="MissingMethodArguments"
  • type="struct"
  • required="true"
  • hint="The arguments that were passed to the above mentioned method."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  •  
  • <!---
  • Set up a value to hold the return value from our
  • ColdFusion method. This needs to be a scoped value
  • since we are expecting it to be NULL sometimes.
  • --->
  • <cfset LOCAL.Return = "{null}" />
  •  
  • <!---
  • Build the name of the method that we want to access.
  • We are going to assume that this method's name is
  • the name that was accessed with "IMAGE" prepended.
  • --->
  • <cfset LOCAL.MethodName = ("Image" & ARGUMENTS.MissingMethodName) />
  •  
  •  
  • <!--- Check to see if this method is valid. --->
  • <cfif StructKeyExists( GetFunctionList(), LOCAL.MethodName )>
  •  
  • <!---
  • Now that we have a valid method to execute, we
  • need to build the proper string. Since, there is
  • no nice, clean way to dynamically execute a
  • built-in method in ColdFusion, we need to do all
  • this using Evaluate().
  •  
  • Since all of the ColdFusion image methods take
  • the image object as the first argument, let's put
  • that as the first element in our argument string.
  • --->
  • <cfset LOCAL.Arguments = "VARIABLES.Image" />
  •  
  • <!---
  • Loop over the missing method arguments to pass
  • them to the method.
  • --->
  • <cfloop
  • index="LOCAL.ArgumentIndex"
  • from="1"
  • to="#ArrayLen( ARGUMENTS.MissingMethodArguments )#">
  •  
  • <!--- Append argument. --->
  • <cfset LOCAL.Arguments &= (
  • ", " &
  • "ARGUMENTS.MissingMethodArguments[ #LOCAL.ArgumentIndex# ]"
  • ) />
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • Execute the given method. We have to execute this
  • method through the undocumented Page object since
  • ColdFusion doesn't like having VOID-returning
  • methods executed dynamically on their own.
  • --->
  • <cfset LOCAL.Return = Evaluate(
  • "VARIABLES.Page.#LOCAL.MethodName#( #LOCAL.Arguments# )"
  • ) />
  •  
  •  
  • <!---
  • At this point, our return value is either
  • nullified (and therefore doesn't exist) or it
  • contains a valid return value.
  • --->
  • <cfif (
  • StructKeyExists( LOCAL, "Return" )
  • AND
  • (
  • (
  • IsSimpleValue( LOCAL.Return ) AND
  • (LOCAL.Return NEQ "{null}")
  • )
  • OR
  • (NOT IsSimpleValue( LOCAL.Return ))
  • )
  • )>
  •  
  • <!--- We have a real value to return. --->
  • <cfreturn LOCAL.Return />
  •  
  • <cfelse>
  •  
  • <!---
  • The return value doesn't exist so the method
  • we called did not have a return value.
  • Return This reference for method chaining.
  • --->
  • <cfreturn THIS />
  •  
  • </cfif>
  •  
  • <cfelse>
  •  
  • <!--- That method was not supported. --->
  • <cfthrow
  • type="ImageWrapper.InvalidMethod"
  • message="The method you called does not exist."
  • detail="The method that you called, #UCase( ARGUMENTS.MissingMethodName )#, is not currently supported."
  • />
  •  
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

I am not a huge fan using the Evaluate() method, but I don't know any other way of dynamically calling built-in ColdFusion methods.




Reader Comments

@Pete,

Agreed. At first, I didn't really see the value in OnMissingMethod(), but it had proven useful on several occasions.

Reply to this Comment

Hah. That's a cool way to do this. You should really report the Image* function Evaluate() bug to Adobe. That's should work.

Evaluate() is actually the fastest way to do this too, and the most reliable. I guess the "proper" (or Javaish/OOish) way to do this kind of thing would be with reflection and calling:

page = getPageContext().getPage();
types = [ arg[1].getClass(), arg[2].getClass(), ... ];
method = page.getClass().getSuperClass().getMethod(methodName,types);
result = method.invoke(page,args);

Unfortunately this isn't terribly reliable due to the way CF converts types in some cases (I've had a lot of issues), worse yet, it's MUCH slower than using Evaluate().

Evaluate() isn't inherently wrong. And if we look at the ruby community it's used quite often, so we shouldn't be too afraid of it. :)

Reply to this Comment

This bug seem to be an issue with the Evaluate() compiler.

<!--- The top level class that powers Evaluate() --->
<cfset ExprClassLoader = createObject("java","coldfusion.compiler.ExprClassLoader")>
<cfset image = ImageNew("http://www.google.com/intl/en_ALL/images/logo.gif")>

<cfset result1 = ExprClassLoader.compileStatement("1+1")>
<cfset result2 = ExprClassLoader.compileStatement("notify()")>
<cfset result3 = ExprClassLoader.compileStatement("ImageBlur(image)")>

result1 will instead be an instance of a class that implements the "coldfusion.compiler.EvaluateFunction" interface.

However, result2 and result3 will both be null, which is why Evaluate() throws a NullPointerException! Doh.

When you call Evaluate() it seems that CF really does:

function Evaluate( expr ) {
return ExprClassLoader.compileStatement(expr).evaluate(getPageContext().getPage(),javaCast("null",0));
}

Not sure what the second argument is for, but passing null works fine.

It seems that the reason you can't Evaluate functions that return void is because the compileStatement() call returns null, so then calling evaluate() throws a NPE.

Definitely a bug somewhere in the compilation code. Hopefully Adobe can fix this. :)

Reply to this Comment

@Elliott,

Very cool. I have no idea where you are getting that info, but it's very interesting insight. Any ideas why the Evaluate() of the built-in method directly would function any different from the Evaluate() of the built-in method via GetPageContext().GetPage()? Very odd as they both return void.

Reply to this Comment

Very cool, Ben -- thanks!. I was planning on putting together something like this, but you beat me to it (and I didn't even think of onMissingMethod). I added a little bit of logic to the init() method so you can initialize the wrapper with an image object or just a path to an image file.
One thing to keep in mind, though, is that in some cases some of the methods will return THIS when you don't want them to. For example, I tested the component on a jpg image that had no EXIF data, so when I tried to do objImage.getExifTag("compression"), which would normally return the EXIF compression information, I got the image object back (because the method returned null since there was no EXIF data). So cfoutputting #objImage.getExifTag("compression")# will give you an error for trying to cfoutput a complex variable, when, in this case, null would be the appropriate return value.
I guess one could just check for whether the method name is one in which you might expect null to be a possible return value. I'm not sure if there are any others besides the ImageGetEXIFTag or ImageGetIPTCTag functions.

Reply to this Comment

@Tony,

Hmm, good tip. I have never used those methods - I didn't realize they could possibly return different types of value. I'll see about updating the code. Thanks.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.