Skip to main content
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Peter Farrell and Matt Woodward and Kurt Wiersma
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Peter Farrell Matt Woodward Kurt Wiersma

Pixelating An Image With ColdFusion

By
Published in Comments (14)

Last night, I was building some internal company tools for prototyping when I needed to access the color of an area of a given image (in order to programmatically set a background color). While ColdFusion does not make this information available directly, it does provide a way to access the underlying buffered image of the ColdFusion image object. The buffered image is the collection of pixel data that represents the current image. Once we have access to the actual pixel data, we can really start to monkey around with the image.

For fun, I thought I would try to create a "pixelation" effect in which you can take a ColdFusion image and turn it into a series of larger pixels (abstractly speaking - in practice, pixels are still only 1x1 squares). The strategy behind this effect is one of averaging: to create a "single" pixel out of a 20x20 area of the image, I have to get all of the pixel values in that 20x20 area and then average their colors together.

The one caveat here is that the buffered image returns pixel color values as a single integer. When we average the colors together, we can't just average this value since it's not a "true" number; rather, it's a collection of bits that represent the Red, Green, Blue (and sometimes Alpha) channels of the given pixel color. As such, we need to extract and average each channel individually, and then merge them back together to create another single-integer representation.

I have taken this algorithm and wrapped it up in the following function, imagePixelate():

<cffunction
	name="imagePixelate"
	access="public"
	returntype="void"
	output="false"
	hint="I take a ColdFusion image object and pixelate it with the given pixel size. NOTE: To keep inline with the exisitng image functions, this will work directly on the existing image (rather than creating a new image).">

	<!--- Define arguments. --->
	<cfargument
		name="image"
		type="any"
		required="true"
		hint="I am the image being pixelated."
		/>

	<cfargument
		name="pixelSize"
		type="numeric"
		required="false"
		default="20"
		hint="I am size of the pixels to use in the effect."
		/>

	<!--- Define the local scope. --->
	<cfset var local = {} />

	<!---
		Get the underlying buffered image. This is going to give
		us access to the individual pixels that make up the image.
	--->
	<cfset local.bufferedImage = imageGetBufferedImage(
		arguments.image
		) />

	<!---
		Store the image dimensions - these will be referenced a
		lot, so let's create a short hand.
	--->
	<cfset local.imageWidth = imageGetWidth( arguments.image ) />
	<cfset local.imageHeight = imageGetHeight( arguments.image ) />

	<!---
		Loop over the image, examining a pixel chunk at a time.
		Each chunk will be the NxN pixel area size.
	--->
	<cfloop
		index="local.x"
		from="0"
		to="#(local.imageWidth - 1)#"
		step="#arguments.pixelSize#">

		<cfloop
			index="local.y"
			from="0"
			to="#(local.imageHeight - 1)#"
			step="#arguments.pixelSize#">


			<!---
				When grabbing the given area of pixels, we have
				to be sure not to go out of bounds on the image.
				As such, we cannot rely on the pixelSize to be
				available (this becomes likely only the last
				row and / or column).
			--->
			<cfset local.pixelWidth = min(
				arguments.pixelSize,
				(local.imageWidth - local.x)
				) />

			<cfset local.pixelHeight = min(
				arguments.pixelSize,
				(local.imageHeight - local.y)
				) />

			<!---
				Grab the collection of pixels from the
				underlying buffered image. This will return a
				single-dimension array of pixel values.
			--->
			<cfset local.pixelBuffer = local.bufferedImage.getRGB(
				javaCast( "int", local.x ),
				javaCast( "int", local.y ),
				javaCast( "int", local.pixelWidth ),
				javaCast( "int", local.pixelHeight ),
				javaCast( "null", "" ),
				javaCast( "int", 0 ),
				javaCast( "int", local.pixelWidth )
				) />

			<!---
				Now that we have the colors, we want to average
				them together. However, we can't simply use the
				integer to average; rather, we have to pick out
				the individual R,G,B channels and average them
				seperately.
			--->
			<cfset local.red = 0 />
			<cfset local.green = 0 />
			<cfset local.blue = 0 />

			<!---
				Loop over each pixel color value in our current
				pixel buffer.
			--->
			<cfloop
				index="local.pixelColor"
				array="#local.pixelBuffer#">

				<!---
					Using Bit-wise SHIFTing and ANDing, get
					each of the color channels and add it to
					the running sum.

					NOTE: I am not taking into account the ALPHA
					channel which would require another 24 bit
					right-shift.
				--->

				<!--- Red. --->
				<cfset local.red += bitAnd(
					bitSHRN( local.pixelColor, 16 ),
					255
					) />

				<!--- Green. --->
				<cfset local.green += bitAnd(
					bitSHRN( local.pixelColor, 8 ),
					255
					) />

				<!--- Blue. --->
				<cfset local.blue += bitAnd(
					local.pixelColor,
					255
					) />

			</cfloop>

			<!---
				Now that we have summed the R,G,B channels, we can
				average them together.
			--->

			<!--- Red. --->
			<cfset local.redAverage = fix(
				local.red / arrayLen( local.pixelBuffer )
				) />

			<!--- Green. --->
			<cfset local.greenAverage = fix(
				local.green / arrayLen( local.pixelBuffer )
				) />

			<!--- Blue. --->
			<cfset local.blueAverage = fix(
				local.blue / arrayLen( local.pixelBuffer )
				) />

			<!---
				Now that we have averaged the individual colors,
				we need to merge them together to create a full,
				RGB integer value.

				NOTE: Again, I am not taking the ALPHA channel
				into account.
			--->
			<cfset local.pixelAverage = bitOr(
				bitOr(
					bitSHLN( local.redAverage, 16 ),
					bitSHLN( local.greenAverage, 8 )
					),
				local.blueAverage
				) />

			<!---
				Now that we have the average, we want to
				overwrite every array value in the pixel buffer
				with the given, average color. However, since
				ColdFusion doesn't play nicely with true Java
				arrays, we'll create our own arrray.
			--->
			<cfset local.averagePixelBuffer = [] />

			<!---
				Set NxN indicies of the ColdFusion array with the
				pixel color average (we will convert it to a Java
				array in the succeeding step).
			--->
			<cfset arraySet(
				local.averagePixelBuffer,
				1,
				(local.pixelWidth * local.pixelHeight),
				local.pixelAverage
				) />

			<!---
				Now that we have our new average RGB color array,
				let's write it back to the buffered image.

				NOTE: We are casting our ColdFusion array to a
				Java int array.
			--->
			<cfset local.bufferedImage.setRGB(
				javaCast( "int", local.x ),
				javaCast( "int", local.y ),
				javaCast( "int", local.pixelWidth ),
				javaCast( "int", local.pixelHeight ),
				javaCast( "int[]", local.averagePixelBuffer ),
				javaCast( "int", 0 ),
				javaCast( "int", local.pixelWidth )
				) />

			<!---
				NOTE: Editing the buffered image has a direct
				affect on the image from which we retrieved the
				buffered image. It is passed by REFERENCE!

				As such, we don't need to explicitly move the
				updated buffered image data "back" into the
				image for the changes to take place.
			--->

		</cfloop>

	</cfloop>

	<!---
		Return out. No need to return the original image since
		it is be altered directly.... I don't really care for this
		strategy, but it keeps in line with the exisitng image
		functions provided by ColdFusion.
	--->
	<cfreturn />
</cffunction>

As you can see in the code above, I am getting a reference to the ColdFusion image's underlying BufferedImage object. This buffered image (pixel data) is passed by reference. This means that we are not working with a copy of it; as such, any changes that we make to the buffered image data will immediately affect the ColdFusion image from which we got it. That is why, in the above function, I never have to set the altered buffered image data back into the ColdFusion image object.

Once I had the imagePixelate() function in place, I wrote some test code to see how this would affect images at different "pixel" sizes:

<!--- Read in the image. --->
<cfimage
	name="muscle"
	action="read"
	source="./muscle2.jpg"
	/>

<cfoutput>

	<div align="center">

		<h3>
			Original Image
		</h3>

		<p>
			<cfimage
				action="writetobrowser"
				source="#muscle#"
				/>
		</p>


		<!---
			Now, let's loop over a few different pixel sizes and
			apply each to the *original* image.
		--->
		<cfloop
			index="pixelSize"
			from="5"
			to="30"
			step="5">

			<!---
				Copy the original image since the pixelation is
				applied directly to the image passed-in.
			--->
			<cfset testMuscle = imageCopy(
				muscle,
				0,
				0,
				imageGetWidth( muscle ),
				imageGetHeight( muscle )
				) />

			<!---
				Pixelate the image using the given pixel size of
				this iteration.
			--->
			<cfset imagePixelate( testMuscle, pixelSize ) />

			<!--- Output the image. --->
			<h3>
				Pixel Size: #pixelSize#
			</h3>

			<p>
				<cfimage
					action="writetobrowser"
					source="#testMuscle#"
					/>
			</p>

		</cfloop>

	</div>

</cfoutput>

As you can see in this code, we are iterating over an image, applying the effect with incrementing pixel dimensions. Notice that before I apply the pixelation effect, I have to create a copy of the image. This is due to the fact that effect works directly on the original image (via its buffered image data); I have to copy the image to make sure the subsequent effects are not all applied to the same variable.

When we run the above code, we get the following page output:

Pixelate A ColdFusion Image Using The Underlying Buffered Image Data.

Anyway, I just think that's a cool little effect.

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

Reader Comments

15,848 Comments

@Raymond,

Ah, good point!

@James,

This was built in CF8. I can't remember off-hand when images were introduced. I think they were in ColdFusion starting in CF8.

15,848 Comments

@Rick,

It might create something similar, but I don't think you would have any control over the size of the resultant pixels... or at least it might take a different set of calculations (ie. smaller size to resize to before re-enlarging).

7 Comments

I have done the same via actionscript and used to scale the 20*20 area to 1*1 and it merged the colors automatically, so you would just have to get the color of the scaled pixel.
Also, not sure it would be faster ...

15,848 Comments

@Philippe,

Oh that's an interesting idea; and I think it kind of goes nicely with what @Rick was talking about - just at the smaller scale.

2 Comments

To me this is Spanish and Chinese together at the same time. Right now Im trying to learn PHP which is enough for me so far :)

But thanks anyway, maybe in a few months I will understand that.

68 Comments

Tested and verified:

<cfset var origWidth = image.width>
<cfset var origHeight = image.height>
<cfset imageResize(arguments.image, arguments.image.width / arguments.pixelSize, "")>
<cfset imageResize(arguments.image, origWidth, origHeight, "nearest")>

By specifying the interpolation as "nearest", you disable any of the smarter interpolation that would lead to a blurry image.

(Not that your post isn't useful in its own right -- I'm just nit-picking.)

15,848 Comments

@Rick,

Very cool! I just tested this to see for myself and it worked perfectly. I guess this goes to show you how important it can be to understand the differences between all the various interpolation methods (is that the word I'm looking for)?

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