Justin Timberlake may be a little more popular than ColdFusion for bringing sexy back, but let me share how ColdFusion 8 is bringing the web counter back. What's the web counter?
Back in the "old" days, your web site wasn't truly a web site until it had a web counter. This little graphical doodad would show you how many visitors had hit your web site. Here is an example:
You would paste once of these bad boys on your site and reload your page watching the numbers go up. And if the number actually went up twice in one hit - holy smokes - that means someone else actually hit your web page!
Various services were set up to serve up these counters, and some, like Site Meter are still around.
For fun, I decided to try to build one of these myself. I began by creating a simple CFC to handle storing and retrieving counter information. I didn't want to bother with a database and instead just relied on the Server scope. Yes, this means the stats won't really count, but this is all just for fun anyway. Here is what I started off with:
<cffunction name="init" access="public" returnType="counter" output="false">
<cfset initData()>
<cfreturn this>
</cffunction>
<cffunction name="initData" access="public" returnType="void" output="false">
<!--- setup initial data store. --->
<cfif not structKeyExists(server, "counter")>
<cfset server.counter = structNew()>
</cfif>
</cffunction>
<cffunction name="getCount" access="public" returnType="numeric" output="false">
<cfargument name="client" type="string" required="true">
<cfif not structKeyExists(server.counter, arguments.client)>
<cfset server.counter[arguments.client] = 0>
</cfif>
<cfreturn ++server.counter[arguments.client]>
</cffunction>
I should have a few CFLOCKs in there, but again, I wanted to keep it simple. The code will accept any "client" variable which represents the counter. When requested, I use ++X form to both increase the value and return the increased value. (Boy do I love the new syntax features in ColdFusion 8!) I tested this code by itself before adding the image stuff to ensure everything worked fine.
Next I created a method that would generate an image. "Real" hit counters give you multiple options for sizes, counter styles, etc., but I kept mine simple. You can provide an optional background and text color only. I hard coded sizes that I thought made sense. The only complex part was handling the positioning of the numbers. I blogged about how to handle this a few weeks ago. I haven't yet had a chance to add this to ImageUtils (the project Ben and I started to wrap up common image tasks) but will get to that soon. Here is the code for the function to return the counter image:
<cffunction name="makeCounter" access="public" returnType="any" output="false">
<cfargument name="client" type="string" required="true">
<cfargument name="bgcolor" type="string" required="false" default="black">
<cfargument name="textcolor" type="string" required="false" default="white">
<!--- get the number --->
<cfset var count = getCount(arguments.client)>
<!--- make the canvas --->
<cfset var img = imageNew("", 150, 40, "rgb", arguments.bgcolor)>
<!--- set the text props --->
<cfset var tProps = { style='bold', size=24, font='Arial-BoldMT' }>
<cfset var buffered = imageGetBufferedImage(img)>
<cfset var context = buffered.getGraphics().getFontRenderContext()>
<cfset var font = createObject("java", "java.awt.Font")>
<cfset var textFont = Font.init(tProps.font, Font.BOLD, javacast("int", tProps.size))>
<cfset var textLayout = createObject("java", "java.awt.font.TextLayout").init(javaCast("string",count), textFont, context)>
<cfset var textBounds = textLayout.getBounds()>
<cfset var twidth = textBounds.getWidth()>
<cfset var theight = textBounds.getHeight()>
<cfset var newx = "">
<cfset var newy = "">
<!--- text color --->
<cfset imageSetDrawingColor(img, arguments.textColor)>
<!--- handle centering crap --->
<!---
when drawing text, you specify X, Y as the bottom left corner.
So we need to position ourselves at Total Height / 2 + Height of Text / 2
--->
<cfset newx = (img.width/2 - tWidth/2)>
<cfset newy = (img.height/2 + tHeight/2)>
<cfset imageDrawText(img, count, newx, newy, tProps)>
<cfreturn img>
</cffunction>
As you can see, more than 50% of the code is just for positioning the darn text, so if I did have that nice UDF handy, the function would have been a heck of a lot slimmer.
All that's left now is a file to spit out the image. Let's look at how I did that:
<cfparam name="url.client" default="">
<cfparam name="url.bgcolor" default="">
<cfparam name="url.textcolor" default="">
<cfif not len(url.client)>
<cfthrow message="You must supply a Client variable in the query string.">
</cfif>
<cfinvoke component="#application.counter#" method="makeCounter" returnVariable="img">
<cfinvokeargument name="client" value="#url.client#">
<cfif len(url.bgcolor)>
<cfinvokeargument name="bgcolor" value="#url.bgcolor#">
</cfif>
<cfif len(url.textcolor)>
<cfinvokeargument name="textcolor" value="#url.textcolor#">
</cfif>
</cfinvoke>
<cfset bos = CreateObject("java","java.io.ByteArrayOutputStream").init()/>
<cfset imageio = CreateObject("java","javax.imageio.ImageIO").write(imagegetbufferedimage(img), "PNG", bos)/>
<cfset bos.close()/>
<cfcontent type="images/gif" variable="#bos.toByteArray()#" reset="true">
The code begins with a few cfparams and validation on the client variable. I then call my component and pass in the relevant values. So far so good. Now what the heck is the Java?
ColdFusion 8 has a bug with images created using imageNew that are not based on a 'real' image. If you try to get the binary data from the image, or try to pass it to cfcontent, you will get an image.
Jason Delmore of Adobe came up with this solution. It provides an alternate way to get binary data out and then supply it to the cfcontent tag. I'll be adding this to ImageUtils too!
So now that I've gone through all of that, here is a simple counter for the client parishilton:
Sorry, old demos are removed.
The URL I used was http://www.coldfusionjedi.com/demos/counterservice/image.cfm?client=parishilton.
Now let's make it a bit fancier and supply some colors:
Sorry, old demos are removed.
Note - I ran into a font issue when moving my sample code from my Mac to my Windows machine. The code on the live server is using "Arial Black" for the font instead of the one specified in the code above.
So a total waste of time - but a fun way to spend lunch. Enjoy!