Skip to main content
Ben Nadel at InVision Office 2011 (New York City) with: Lindsey Root and Adam Root and Clark Valberg
Ben Nadel at InVision Office 2011 (New York City) with: Lindsey Root ( @lowcarblindsey ) Adam Root ( @adamroot ) Clark Valberg ( @clarkvalberg )

Image Manipulation ColdFusion Wrapper Component

By on
Tags:

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()).

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.

Want to use code from this post? Check out the license.

Reader Comments

15,640 Comments

@Pete,

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

132 Comments

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. :)

132 Comments

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. :)

15,640 Comments

@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.

24 Comments

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.

15,640 Comments

@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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel