Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Getting The RGB Color Value Of An Image Pixel Using GraphicsMagick And Lucee CFML 5.2.9.31

By Ben Nadel on
Tags: ColdFusion

Now that I have my GraphicsMagick Docker playground up and running, it's time to start recreating some functionality with the gm command-line tool that I would have historically done with ColdFusion's CFImage tag. Starting out simple, I wanted to see if I could read in the Color value (Hex or RGB) of a given pixel from within an image using Lucee CFML 5.2.9.31. And, to make this demo a bit more interesting, the pixel coordinates can be selected by the user with a sprinkling of JavaScript.

CAUTION: I am very much a novice when it comes to GraphicsMagick. As such, please take the following as an exploration, not an explanation.

Going through the GraphicsMagick documentation, I couldn't find anything that really clued-me-in on how to do this. Luckily, I came across a StackOverflow post on how to perform pixel enumeration in ImageMagick. While GraphicsMagick and ImageMagick aren't completely compatible, there's enough overlapping functionality to make ImageMagick posts highly relevant.

From the StackOverflow post, I learned a number of helpful things about GraphicsMagick that come into play in this solution:

  • GraphicsMagick can write to a TXT file format. Using this format triggers "pixel enumeration", wherein GraphicsMagic will iterate over each pixel in the image and write its color-value, in order, to the given file.

  • Using :- as part of the destination - as in txt:- - will cause the result to be written to the standard-out instead of to a physical file. This configuration will allow us to read the results of the gm operation in the variable value of our CFExecute tag.

Given these behaviors, we can get the color value of a given pixel within an image by:

  • Reading in the image.
  • Cropping it to a 1x1 canvas at the given X,Y offset.
  • Writing the 1x1 canvas pixel value to the standard-out using pixel enumeration.
  • Parsing the color value from the standard-out.

When GraphicsMagick performs the pixel enumeration, the individual color values look like this:

0,0: (  0, 79,150,  0) #004F9600
1,0: (  0, 81,150,  0) #00519600
2,0: (  1, 83,150,  0) #01539600
3,0: (  1, 85,150,  0) #01559600
4,0: (  3, 87,152,  0) #03579800
5,0: (  2, 89,151,  0) #02599700
6,0: (  2, 92,152,  0) #025C9800
7,0: (  2, 93,151,  0) #025D9700
8,0: (  3, 96,151,  0) #03609700
9,0: (  4, 99,151,  0) #04639700
0,1: (  0, 79,152,  0) #004F9800
1,1: (  0, 81,151,  0) #00519700
2,1: (  1, 83,151,  0) #01539700
3,1: (  2, 85,152,  0) #02559800
4,1: (  3, 88,155,  0) #03589B00
5,1: (  2, 89,154,  0) #02599A00

This output contains the X/Y offset, the RGBA channels, and the HEX value for each pixel in the resultant image. And, of course, if we crop the image down to a 1x1 canvas, our output will only contain a single line of text. From that text, we can use a Regular Expression pattern (Video presentation) to extract the hexadecimal value.

Pulling these concepts into an interactive demo, here's the ColdFusion and JavaScript code that I came up with. It's mostly ColdFusion - the JavaScript bits just capture the click-event and translate the browser's Viewport coordinates into image-local coordinates, which it then uses to refresh the page. The color value at the given pixel offset is then used to change the background-color of the <body> tag:

<cfscript>

	// Default coordinates to a the bottom-left, which is White.
	param name="url.x" type="numeric" default="0";
	param name="url.y" type="numeric" default="376";

	startedAt = getTickCount();

	inputFilename = "palette.png";
	inputFilepath = expandPath( "../images/#inputFilename#" );

	// All of the image utilities are provided through the GraphicsMagick binary.
	command = "gm";

	// We're going to use the Convert utility to extract the pixel.
	utility = "convert";

	// We want to be EXPLICIT about which input reader GraphicsMagick should use.
	// If we leave it up to "automatic detection", a malicious actor could fake
	// file-type and potentially exploit a weakness in a given reader.
	// --
	// READ MORE: http://www.graphicsmagick.org/security.html
	utilityReader = "png";

	// Setup the options for the Convert utility.
	commandOptions = [
		utility,

		// Provide the source image to be read with the an explicit reader.
		( utilityReader & ":" & inputFilepath ),

		// Crop the image down to a 1x1 canvas that contains the single pixel that we are
		// trying to inspect.
		"-crop 1x1+#url.x#+#url.y#",

		// Ensure that the color-depth is 8-bits so that we get a byte per color channel
		// within the RGB color scheme.
		"-depth 8",

		// Output the results as a plain-text value to the standard-out so that we can
		// read it with our "successResult" variable.
		// --
		// NOTE: The "-" denotes standard-out instead of a destination file.
		"txt:-"
	];

	// Execute GraphicsMagick on the command-line.
	execute
		name = command
		arguments = commandOptions.toList( " " )
		variable = "successResult"
		errorVariable = "errorResult"
		timeout = 5
	;

	duration = ( getTickCount() - startedAt );

	// If the error variable has been populated, it means the CFExecute tag ran into
	// an error - let's dump-it-out and halt processing.
	if ( variables.keyExists( "errorVariable" ) && errorVariable.len() ) {

		dump( errorVariable );
		abort;

	}

	// At this point, the successResult, which contains the standard-output from the
	// command execution, should contains something that looks like:
	// --
	// 0,0: (215, 1,255, 0) #D701FF00
	// --
	// That's an rgba() value and a HEX value. We can parse the HEX value pattern.
	bgColor = successResult
		.reMatchNoCase( "##[0-9a-f]{6}" )
		.first()
	;

</cfscript>

<cfoutput>

	<body style="background-color: #bgColor#">

		<p>
			<!---
				This is the color-palette from which we will be selecting a pixel to
				inspect with GraphicsMagick.
			--->
			<img
				id="image"
				src="../images/#inputFilename#"
				width="500"
				height="377"
				style="cursor: pointer ;"
			/>
		</p>

		<p style="text-shadow: 1px 1px ##ffffff ; font-family: monospace ; font-size: 18px ; letter-spacing: 1px ;">
			Pixel Offset: { #encodeForHtml( url.x )# , #encodeForHtml( url.y )# }<br />
			Pixel Color: #encodeForHtml( bgColor )#<br />
			Red: #inputBaseN( bgColor.mid( 2, 2 ), 16 )#<br />
			Green: #inputBaseN( bgColor.mid( 4, 2 ), 16 )#<br />
			Blue: #inputBaseN( bgColor.mid( 6, 2 ), 16 )#<br />
			Duration: #numberFormat( duration )# ms
		</p>

		<script type="text/javascript">

			// When the user clicks anywhere on the color-palette image, we want to
			// translate the viewport coordinates to image-local coordinates and then
			// refresh the page with the given offset.
			document.getElementById( "image" ).addEventListener(
				"click",
				( event ) => {

					// Get viewport coordinate data.
					var viewportX = event.clientX;
					var viewportY = event.clientY;
					var imageRect = event.target.getBoundingClientRect();

					// Translate viewport coordinates to image-local coordinates.
					var localX = ( viewportX - imageRect.left );
					var localY = ( viewportY - imageRect.top );

					window.location.href = `./index.cfm?x=${ localX }&y=${ localY }`;

				}
			);

		</script>

	</body>

</cfoutput>

As you can see, we're using the ColdFusion CFExecute tag to read in the image file - palette.png - crop it down to a 1x1 canvas, ensure 8-bit colors, and then write the pixel-enumeration to the standard-out. We then grab the HEX value from our single pixel and use it to color the page's background-color.

And, when I run this page in my Lucee CFML Docker container and click on the image a few times, we get the following output:

GraphicsMagick being used to extract pixel RGB and Hex value in Lucee CFML.

It's pretty cool! I will say, however, that the image I am using is fairly small, so this run very quickly. As the image gets larger, however, the speed decreases, presumably because there's more image to read into memory; and then, more image to remove during the crop. I mean, we're still talking sub-second time; but, there's clearly a relationship between the file-size and the speed of this approach.

According to a forum-thread that I found about ImageMagick, it appears that, ImageMagick can combine the file-read with the crop-operation using a special syntax. I tried to do the same thing with GraphicsMagick, but it didn't work. I did create a new thread on SourceForge asking the GraphicsMagick maintainers if there was an equivalent type of syntax for gm convert. We'll see how that plays-out.

Anyway, this is just me trying to get familiar with the GraphicsMagick command-line tool; and, how I can consume it using CFExecute in Lucee CFML. If there are easier ways to extract a given color value, please let me know.



Reader Comments

Ben. Interesting stuff!

Even better. You could use an XHR request [Ajax] to get your RGB values. Then you don't need to refresh the page!

This is actually quite powerful stuff. Never even heard of GM/IM. I have always just used CFIMAGE...

Reply to this Comment

@Charles,

Very true! The full-page-refresh in this case was to make things as simple as possible.

At work, we use CFImage / new Image() as well. But, we use ImageMagick for some stuff. That said, I've been running into some memory / performance issues with the ColdFusion image functionality. So, I am hoping that I might be able to minimize the memory / CPU footprint if I move to GraphicsMagick for, at least, some of the larger operations. To be fair, I don't actually know if this will have a positive outcome; but, I figure it would at least be fun to play with it a bit.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
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.