Wednesday, January 30, 2008

Submit PDF Form as FDF with ColdFusion

In a previous entry I showed how I used iText to add a button to an existing PDF form that would submit the information as an html form. You could also use the default option, which submits the information as FDF. Simply change the last parameter from PdfAction.SUBMIT_HTML_FORMAT to zero (0). You can view the full code at the bottom of the entry here.


<cfscript>
PdfAction = createObject("java", "com.lowagie.text.pdf.PdfAction");
buttonAction = PdfAction.createSubmitForm( javacast("string", submitFormDataToURL),
javacast("null", ""),
0
);
</cfscript>


When the form is submitted, one way to process the received information from ColdFusion is to use iText's FdfReader class. Simply extract the bytes submitted using the GetHttpRequestData() function and pass them into the FdfReader class.


<cfset bytes = GetHttpRequestData().content>
<cfset reader = createObject("java", "com.lowagie.text.pdf.FdfReader").init(bytes)>


Then use the getFields() method to extract the names and values of the form fields that were submitted.


<cfset fields = reader.getFields()>

<cfloop collection="#fields#" item="key">
<!--- extract the value for this key --->
<cfset value = reader.getFieldValue(key)>
<!--- the value may be null --->
<cfif IsDefined("value")>
<cfset form[ key ] = trim(value) >
<cfelse>
<cfset form[ key ] = "" >
</cfif>
</cfloop>


Here is a simple example of processing form information submitted as FDF. It is intended as an example only and could certainly use improvement. But I will let you do that on your own ;)

RecieveForm.cfm

<!--- disable output so it does not interfere with fdf response --->
<cfsetting enablecfoutputonly="true" showdebugoutput="false">


<!--- use an FdfReader to extract the fields from the request content ---->
<cfset bytes = GetHttpRequestData().content>
<cfset reader = createObject("java", "com.lowagie.text.pdf.FdfReader").init(bytes)>
<cfset fields = reader.getFields()>

<cfloop collection="#fields#" item="key">
<!--- extract the value for this key --->
<cfset value = reader.getFieldValue(key)>
<!--- the value may be null --->
<cfif IsDefined("value")>
<cfset form[ key ] = trim(value) >
<cfelse>
<cfset form[ key ] = "" >
</cfif>
</cfloop>


<!--- verify the required fields exist and are non empty --->
<cfset wasValidated = true>
<cfset requiredFields = "Name,Address,Postal_Code,Email">
<cfloop list="#requiredFields#" index="fieldName">
<cfif NOT structKeyExists(form, fieldName) OR NOT len( form[fieldName] ) >
<cfset wasValidated = false>
<cfbreak>
</cfif>
</cfloop>

<cftry>
<cfquery name="logForm" datasource="MyDatasource">
INSERT INTO LogPDFForm ( PersonName, Address, PostalCode, Email, SubmitDate )
VALUES
(
<cfqueryparam value="#trim(form.Name)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Address)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Postal_Code)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Email)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">
)
</cfquery>
<cfset responseMessage = "Your form was successfully submitted!">

<cfcatch>
<cfset responseMessage = "Unable to process your form at this time. Please try again later.">
</cfcatch>
</cftry>

<!--- send a response --->
<cfcontent type="application/vnd.fdf">
<cfoutput>%FDF-1.2
%âãÏÓ
1 0 obj
<<
/FDF << /Status (#responseMessage#)>>
>>
endobj
trailer
<<
/Root 1 0 R

>>
%%EOF
</cfoutput>

...Read More

Tuesday, January 29, 2008

Submit PDF Form as HTML with ColdFusion - Part 2

In Part 1 I showed how I used iText to add a simple button to an existing form, that would submit the information as an HTML form. In Part 2 I will show the ColdFusion code I used to process the PDF form information and save it to a database table. I deliberately kept it simple for clarity. So you could certainly improve upon it.

Anyway, since I wanted to store the information in my database, the first thing I did was create a simple table. Note, the syntax below is MS SQL Server specific.


CREATE TABLE LogPDFForm (
PdfFormID int identity(1,1),
PersonName varchar(100) NULL,
Address varchar(100) NULL,
PostalCode varchar(20) NULL,
Email varchar(100) NULL,
SubmitDate datetime NOT NULL
)

Next, I created the cfm page that will do the processing. Due to the settings used, the pdf form information will be submitted using http post. However, the form field values will be submitted as a delimited string like this

address=98+booch+street&email=test%40test.com&name=test+%3d+abc&postal_code=1234&sendButton=


So I first grabbed the form content using the GetHttpRequestData() function. Then used a simple loop to extract and decode the form field values. I chose to store them values in the FORM scope, but you could use a different scope if you prefer.

<!--- extract the fields from the request content ---->
<cfset queryString = GetHttpRequestData().content>
<cfloop list="#queryString#" index="pair" delimiters="&">
<cfif listLen(pair, "=")>
<cfset key = URLDecode(listFirst(pair, "="))>
<cfset value = URLDecode(listRest(pair, "="))>
<cfset form[ trim(key) ] = trim(value)>
</cfif>
</cfloop>

Next, I performed a bit of validation and then inserted the extracted values into my database table. As you can see the code sets a status variable to indicate whether the insert succeeded or failed. You will see why in a moment.

<cftry>
<cfquery name="logForm" datasource="MyDataSource">
INSERT INTO LogPDFForm ( PersonName, Address, PostalCode, Email, SubmitDate )
VALUES
(
<cfqueryparam value="#trim(form.Name)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Address)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Postal_Code)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Email)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">
)
</cfquery>
<cfset responseMessage = "Your form was successfully submitted!">

<cfcatch>
<cfset responseMessage = "Unable to process your form at this time. Please try again later.">
</cfcatch>
</cftry>

Finally, the code returns the status message back to Adobe Reader using FDF. The Reader will process this like a flash or javascript alert box. So the end user will receive feedback about whether or not their form information was processed.

<!--- send a response --->
<cfsetting enablecfoutputonly="true" showdebugoutput="false">
<cfcontent type="application/vnd.fdf">
<cfoutput>%FDF-1.2
%âãÏÓ
1 0 obj
<<
/FDF << /Status (#responseMessage#)>>
>>
endobj
trailer
<<
/Root 1 0 R

>>
%%EOF
</cfoutput>

Then I was ready to test the form. So I opened up the new PDF created in Part 1, entered some information, clicked Send Form and voila! It worked.



That is it for now. As always, comments/corrections/suggestion are welcome!

ReceivePDFForm.cfm

<!--- disable output so it does not interfere with fdf response --->
<cfsetting enablecfoutputonly="true" showdebugoutput="false">

<cfset wasValidated = true>
<cfset responseMessage = "">

<!--- extract the fields from the request content ---->
<cfset queryString = GetHttpRequestData().content>
<cfloop list="#queryString#" index="pair" delimiters="&">
<cfif listLen(pair, "=")>
<cfset key = URLDecode(listFirst(pair, "="))>
<cfset value = URLDecode(listRest(pair, "="))>
<cfset form[ trim(key) ] = trim(value)>
</cfif>
</cfloop>

<!--- verify the required fields exist and are non empty --->
<cfset requiredFields = "Name,Address,Postal_Code,Email">
<cfloop list="#requiredFields#" index="fieldName">
<cfif NOT structKeyExists(form, fieldName) OR NOT len( form[fieldName] ) >
<cfset wasValidated = false>
<cfbreak>
</cfif>
</cfloop>

<cfif wasValidated>
<cftry>
<cfquery name="logForm" datasource="MyDataSource">
INSERT INTO LogPDFForm ( PersonName, Address, PostalCode, Email, SubmitDate )
VALUES
(
<cfqueryparam value="#trim(form.Name)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Address)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Postal_Code)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#trim(form.Email)#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">
)
</cfquery>
<cfset responseMessage = "Your form was successfully submitted!">

<cfcatch>
<cfset responseMessage = "Unable to process your form at this time. Please try again later.">
</cfcatch>
</cftry>
<cfelse>
<cfset responseMessage = "Unable to process your form due to missing information. All fields are required.">
</cfif>

<!--- send a response --->
<cfcontent type="application/vnd.fdf">
<cfoutput>%FDF-1.2
%âãÏÓ
1 0 obj
<<
/FDF << /Status (#responseMessage#)>>
>>
endobj
trailer
<<
/Root 1 0 R

>>
%%EOF
</cfoutput>

...Read More

Submit PDF Form as HTML with ColdFusion - Part 1

I saw an interesting question on Raymond Camden's blog about adding a button to a PDF form with ColdFusion 8. I do not know if the asker was looking for a simple email hyperlink or a full fledged button. But the question got me to thinking about other things like: How do you add a button to a PDF form that will submit the form as HTML to a ColdFusion page?

It was not as difficult as I thought. Part of the process is covered in the iText API. The critical part was constructing the ColdFusion page to return a response that Adobe Reader can understand. Otherwise, the cfm script would successfully process the form information, but the end user would not know that. They would see only a cryptic message like "Cannot handle content type: text/html".

Creating the new form

Adding a button to my form, was simple. I used a PdfReader object to read in my source PDF, and PdfStamper to create a copy of the form. Then I created a new button and added a button action. The action is like a javascript onClick event. So when the button is clicked, the pdf form information will be submitted to the given URL as an HTML form.


<cfscript>
PdfAction = createObject("java", "com.lowagie.text.pdf.PdfAction");
buttonAction = PdfAction.createSubmitForm( javacast("string", submitFormDataToURL),
javacast("null", ""),
PdfAction.SUBMIT_HTML_FORMAT
);
</cfscript>
...


That is all there was to adding the form button. When you run the code below, it will create a new PDF file, with a submit button. In Part 2 I will show the ColdFusion code I used to process the form information and save it to a database table.

What you will need to run this example

  1. This example requires ColdFusion 8. It should also run under MX7 if you are using a newer version of iText


  2. A sample PDF Form. Download SimpleRegistrationForm.pdf from the iText site. Place it in the same web directory as your cfm script


  3. By default, the sample code will submit the form to the URL http://localhost/receivePDFForm.cfm. Just replace this value with the URL of the page you will use to process the form fields. For now it can be an empty .cfm script. I will show the ColdFusion code for this page in Part 2


Complete Code

<h1>Submit form as HTML example</h1>

<cfscript>
savedErrorMessage = "";

submitFormDataToURL = "http://localhost/receivePDFForm.cfm";
fullPathToInputFile = ExpandPath("SimpleRegistrationForm.pdf");
fullPathToOutputFile = ExpandPath("SendFormAsHTML.pdf");

try {
// step 1: read in the source pdf
reader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( fullPathToInputFile );
// step 2: create an output stream for the destination file
outStream = createObject("java", "java.io.FileOutputStream").init( fullPathToOutputFile );
// step 3: open a stamper for generating the new pdf file
stamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( reader, outStream );

// step 4: set the button dimensions
Rectangle = createObject("java", "com.lowagie.text.Rectangle");
buttonDimen = Rectangle.init( javacast("float", 200),
javacast("float", 600),
javacast("float", 300),
javacast("float", 650)
);

// step 5: create a pushbutton field
PushbuttonField = createObject("java", "com.lowagie.text.pdf.PushbuttonField");
newButton = PushbuttonField.init( stamper.getWriter(), buttonDimen, "sendButton" );
newButton.setText(" Send Form ");
newButton.setOptions( PushbuttonField.VISIBLE_BUT_DOES_NOT_PRINT );
Color = createObject("java", "java.awt.Color");
newButton.setBackgroundColor( Color.LIGHT_GRAY );

// step 6: create an action that will submit the form data
PdfAction = createObject("java", "com.lowagie.text.pdf.PdfAction");
buttonAction = PdfAction.createSubmitForm( javacast("string", submitFormDataToURL),
javacast("null", ""),
PdfAction.SUBMIT_HTML_FORMAT
);

// step 7: add the action to the button, and add the button to the pdf
field = newButton.getField();
field.setAction( buttonAction );
stamper.addAnnotation( field , javacast("int", 1) );

WriteOutput("Done!");
}
catch (java.language.Exception de) {
savedErrorMessage = de;
}

// step 8: close pdf objects
if (IsDefined("stamper")) {
stamper.close();
}
if (IsDefined("outStream")) {
outStream.close();
}
</cfscript>

<!--- show any errors --->
<cfif len(savedErrorMessage) gt 0>
Error - unable to create document(s)
<cfdump var="#savedErrorMessage#">
</cfif>

...Read More

Monday, January 28, 2008

The Forgotten Cross-Join

Sometimes I forget the simplicity and power of cross joins. When first learning sql, I thought cross joins were the most useless of all joins. Later on, after accidentally doing a partial cross join on two very large tables, I quickly decided they just might be the satan of all joins ;)

The truth is they are neither. Cross joins are certainly used far less frequently than other joins. But a well thought out cross join can be quite useful. On the flip side, an accidental or poorly designed cross join can be dangerous! It could probably bring down a database in short order. Not to mention bringing your boss and/or DBA, to your doorstep, wanting to know what the hell you did to crash the database! Now, I do not say that to discourage people from using cross joins. Merely to emphasize that it is important to understand how they work and how to use them properly.

If you are not familiar with cross joins, they produce what is called a cartesian product. The rows from each table are joined, but unlike inner joins, the result is the total number of rows in the first table multiplied by the number of rows in the second table. So a cross join on two tables that contain a mere one thousand (1,000) records each, will produce a resultset of 1 million records! Why would you ever want to do this? Usually you would not. Yet sometimes a cross join is exactly what is needed.

Let us say you have three tables: Users, Groups and UserGroups. The table UserGroups stores the ID of each user, and the groups to which they are assigned. Note, I am using the plural here because user and group are likely reserved words.



You also have a simple form that allows you to select multiple users and assign them all to a selected set of groups.



Now when you process the form, you could loop through the list of userID's and perform a separate query to assign each one to the selected groupID's. But using a cross join provides a simpler and more elegant way to the insert all of the records in a single query.


<cfquery name="addAssignments" datasource="#dsn#">
INSERT INTO UserGroups ( UserID, GroupID )
SELECT u.UserID, g.GroupID
FROM Users AS u CROSS JOIN Groups AS g
WHERE u.UserID IN
(
<cfqueryparam value="#form.userIDList#" cfsqltype="cf_sql_integer" list="true">
)
AND g.GroupID IN
(
<cfqueryparam value="#form.groupIDList#" cfsqltype="cf_sql_integer" list="true">
)
</cfquery>


As you can see from the results, the cross join inserts one record for each of the selected groups, for each user.



Now, there are also other uses for cross joins. In a pinch, you could even use them to simulate a crosstab report, with a small dataset. Though most databases provide a more efficient means for producing that type of output. But all in all, cross joins are not useless or evil after all ;)

...Read More

Friday, January 25, 2008

iText in Action (Yay!)

I finally got my copy of iText in Action and am really jazzed about it. I have been working off of tutorials, articles, and mailing list examples for a while. All of which have been great resources, but it is nice to finally have a more complete reference.

It may sound silly, but when I received it I felt like a kid opening up a present at Christmas time. For some reason it never occurred to my non-geek family and friends that a book like this would make a great Christmas gift. Maybe they knew I would ditch them the moment I saw the book and spend the rest of the day reading and trying out sample code on the computer. (Or would that be rude? ;) Perhaps I should be grateful they did not give me some fru-fru book with a title like "A gentle introduction to the art of etiquette and social graces (for the socially inept challenged)".

Strangely, I even read the introduction of this book, which I never do! It has a few funny anecdotes about how Lowagie got started in programming. I am only up to Chapter 5, but so far I am really pleased with it. The last technical book I read was .. to put it politely .. a bit on the dry and boring side. So needless to say I am enjoying the writing style of this book much more, not to mention the loads of useful examples.

Now if only people would stop bothering me so I could get to the rest of it! ;)

...Read More

Wednesday, January 23, 2008

I am just a lazy programmer in love with my reflection

Okay, so I am not really narcissistic. Though I am a lazy programmer. Sometimes when I am coding, I want to get a quick glimpse of a java object's properties, without hand coding all of the method names. Enter my love of reflection.

The java.lang.reflect (reflection) class allows you to dynamically access an object's properties, methods, etcetera. So being the lazy programmer that I am, I often use it to do a quick dump of an object's simpler methods. Your basic getX() or boolean isSomething() methods.

Take my previous post on using POI to get MS Word metadata. I did not actually type the code for outputting the summary properties. I used reflection. (Told you I was lazy). Not that you would want to, but you could probably recreate the entire API for a class using only reflection.

Here is an example using iText's PdfReader class. I picked it mainly because it has a few interesting methods and will work with both MX7 and CF8. Though to limit the output, the example only grabs methods that start with "get" or "is" and does not display a full cfdump. But you could certainly change that to get more detail.


<!--- read in a PDF file using iText --->
<cfset filePath = ExpandPath("./SimpleRegistrationForm.pdf")>
<cfset reader = createObject("java", "com.lowagie.text.pdf.PdfReader").init(filePath)>
<cfset Modifier = createObject("java", "java.lang.reflect.Modifier")>
<cfset methods = reader.getClass().getDeclaredMethods()>

<cfset quickDump( reader )>


<cffunction name="quickDump" returntype="void" output="true" access="public">
<cfargument name="object" type="any" required="true">
<cfset var Local = structNew()>

<cfset Local.modifier = createObject("java", "java.lang.reflect.Modifier")>
<cfset Local.methods = arguments.object.getClass().getDeclaredMethods()>

<cfloop from="1" to="#arrayLen(Local.methods)#" index="Local.m">
<cfset Local.currMethod = Local.methods[Local.m]>

<!--- find only simple, PUBLIC methods with zero arguments --->
<cfset Local.isSimpleMethod = reFindNoCase("^(get|is)", Local.currMethod.getName() )>
<cfset Local.hasZeroArguments = NOT arrayLen( Local.currMethod.getParameterTypes() )>
<cfset Local.isPublic = Local.currMethod.getModifiers() EQ Local.modifier.PUBLIC>

<!--- if this is a valid method, display the results --->
<cfif Local.isPublic AND Local.isSimpleMethod AND Local.hasZeroArguments>
<cfoutput>
<cftry>
<!--- call the method and get the returned value --->
<cfset Local.results = Local.currMethod.invoke( arguments.object, javacast('null', ""))>
<cfif NOT structKeyExists( Local, "results")>
<cfset Local.results = "(null)">
</cfif>

<b>method name: </b> #Local.currMethod.getName()#
<b>value:</b> #Local.results.toString()#
<cfcatch>
<b> error: </b>
<cfif structKeyExists(cfcatch, "cause")>
#cfcatch.cause.type# : #cfcatch.cause.message#
<cfelse>
#cfcatch.type# : #cfcatch.message#
</cfif>
</cfcatch>
</cftry>
</cfoutput>
<br>
</cfif>
</cfloop>
</cffunction>

...Read More

MS Word metadata with POI and ColdFusion

I have been playing around with POI's HWPF library (Horrible Word Processing Format) and found it provides an easy way to extract metadata from an MS Word file. Now CF and java gurus were probably aware of this already ;) but it was a cool find for me.

I used the JavaLoader.cfc and POI 3.0.1 from poi.apache.org. As you can see it returns the key summary information. Everything from subject and comments to number of words in the document.



Here is the sample code I used. If you already have the right version of POI installed in your classpath, you can simply replace the javaLoader.create(..) statement with a call to the createObject(..) function.

Time to see what else POI can do ;)

Code


<!--- NOTE I am storing my javaLoader in the server scope --->
<!--- read why here; http://www.compoundtheory.com/?action=displayPost&ID=212 --->
<cfset javaLoader = server[MyUniqueKeyForJavaLoader]>

<!--- open a word document with POI and get the summary information --->
<cfset inputFilePath = ExpandPath('fromMSWord.doc')>
<cfset inputStream = createObject("java", "java.io.FileInputStream").init( inputFilePath )>
<cfset document = createObject("java", "org.apache.poi.hwpf.HWPFDocument").init( inputStream )>
<cfset summary = document.getSummaryInformation()>

<b>HWPF Summary Information:</b><br>
<cfoutput>
getSubject = #summary.getSubject()# <br>
getTemplate = #summary.getTemplate()# <br>
getAuthor = #summary.getAuthor()# <br>
getTitle = #summary.getTitle()# <br>
getSecurity = #summary.getSecurity()# <br>
getApplicationName = #summary.getApplicationName()# <br>
getKeywords = #summary.getKeywords()# <br>
getComments = #summary.getComments()# <br>
getLastAuthor = #summary.getLastAuthor()# <br>
getRevNumber = #summary.getRevNumber()# <br>
getEditTime = #summary.getEditTime()# <br>
getLastPrinted = #summary.getLastPrinted()# <br>
getCreateDateTime = #summary.getCreateDateTime()# <br>
getLastSaveDateTime = #summary.getLastSaveDateTime()# <br>
getPageCount = #summary.getPageCount()# <br>
getWordCount = #summary.getWordCount()# <br>
getCharCount = #summary.getCharCount()# <br>
</cfoutput>

...Read More

Tuesday, January 22, 2008

Who knew you could create CFCHART Maps with ColdFusion 8

Ever since reading Raymond Camden's entry on creating gauge charts with ColdFusion 8, I have had charts on the brain. I continued to experiment with the webcharts utility and discovered yet another neat webcharts tool. One that can be used to create map charts like this:



The tool is called Map Editor and it allows you to edit SHP or CSHP files and save them as a webcharts3d .MAP file. You can then use the .MAP file with the webcharts utility to create your chart. You can launch the Map Editor from the command line.

C:\ColdFusion8\lib>  java -jar wc50.jar --mapedit


Now I did not have any .SHP files lying around, so I downloaded one from esri.com. Then I opened up the file in Map Editor and modified the xml to add a few country names.



After saving the file with a .MAP extension, I opened up the webcharts utility and selected a map chart. Now initially the chart preview was blank, because ColdFusion 8's jar does not ship with any .MAP files. But after changing the source to point to my new .MAP file the preview appeared.



Then I saved my project as a WCP file, ran my test code and it produced a great looking map chart. Pretty cool stuff!


09/03/2009: Just like the "url" element of cfchart, you can display another url when one of the chart elements is clicked. Simply use the webcharts utility to set the action attribute to the desired cfm page. You can use pseudo variables like $(value) to pass information about the selected element as url parameters. Example action:

somePage.cfm?elemValue=$(value)





Test Code

Aside from minor changes for the chart type, the code is entirely borrowed from Raymond Camden's entry on gauge charts . (His example was good, so why re-invent the wheel ;) Anyway, all credit and thanks for the code go to him.

<!--- Get base server url --->
<cfif len(CGI.HTTPS)>
<cfset baseURL = "https://"& CGI.HTTP_HOST &"/">
<cfelse>
<cfset baseURL = "http://"& CGI.HTTP_HOST &"/">
</cfif>

<!--- Extract chart style from the WCP file --->
<cfset wcp = XMLParse( ExpandPath("./testMap.wcp") )>
<cfset chartStyle = ToString(wcp.project.style.map)>

<!--- Create sample chart data --->
<cfsavecontent variable="chartModel"><?xml version="1.0" encoding="UTF-8"?>
<map name="Europe">
<item name="Country" popup="$(name)">
<item name="United Kingdom" backColor="#ECC362"/>
<item name="Spain" backColor="#F1D372"/>
<item name="Portugal" backColor="#C7BE7B"/>
<item name="France" backColor="#989A74"/>
<item name="Item" backColor="#D17610"/>
</item>
</map>
</cfsavecontent>

<!--- Initialize chart settings --->
<cfscript>
oMyWebChart = createObject("Java","com.gp.api.jsp.MxServerComponent");
oMyApp = getPageContext().getServletContext();
oSvr = oMyWebChart.getDefaultInstance(oMyApp);
oMyChart2 = oSvr.newImageSpec();
oMyChart2.width = 500;
oMyChart2.height= 570;
oMyChart2.type = "png";
oMyChart2.style = "#chartStyle#";
oMyChart2.model = "#chartModel#";
</cfscript>

<!--- Create html tag set --->
<cfsavecontent variable="chartImgTag">
<cfoutput>#oSvr.getImageTag(oMyChart2, baseURL& "CFIDE/GraphData.cfm?graphCache=wc50&graphID=")#</cfoutput>
</cfsavecontent>

<!--- Good old Webcharts loves to add an extra /Images/ to the URL --->
<cfset chartImgTag = replace(chartImgTag, baseURL &"Images/", baseURL, "All")>

<h2>Map Chart</h2>
<cfoutput>
#chartimgtag#
</cfoutput>

...Read More

Sunday, January 20, 2008

Dial, Radar and Bubble Charts with ColdFusion MX7

On Friday I noticed Raymond Camden posted a cool tip about how to create gauge charts using ColdFusion 8's built-in webcharts utility. Though he credited Simon Haddan and Christopher Wigginton for the idea.

Yesterday, I received a comment from Mike Kelp. He mentioned that he had been able to do some neat and unusual stuff with webcharts, including spark lines. Thanks to his encouragement I decided to explore the idea a bit further. I started with MX7, because anything it can do CF8 can probably do better. Now, I have not figured out how to do spark lines yet, but give me time ;)

Here are just a few examples of what you can do under MX7. The source code is entirely from Raymond Camden's blog entry. So all credit goes to him, Simon Haddan and Christopher Wigginton. Thanks also go to Mike Kelp for pointing out that the utility is well worth exploring!





What you need to run the examples below

Use the instructions in the documentation to open the webcharts utility. Then create three (3) WCP files for the different chart types. Save all WCP files in the same directory as your .CFM script.
  • Select Dial Chart > Design Tab > File > Save As testDial.wcp
  • Select Radar Chart > Design Tab > File > Save As testRadar.wcp
  • Select Bubble Chart > Design Tab > File > Save As testBubble.wcp

Why the empty cfchart?

The webcharts FAQ's mention that the charting engine is "fully initialized only after at least one chart was produced using CFCHART tag. If your application does not do so, you might want to insert an empty CFCHART tag". In my tests the charts did not display until after I used the empty cfchart. You may not need it.


Dial Chart

<!--- See http://www.webcharts3d.com/website/WebCharts50/cf/faq.jsp --->
<cfchart chartwidth="1" chartheight="1"/>

<!--- Get base server url --->
<cfif len(CGI.HTTPS)>
<cfset baseURL = "https://"& CGI.HTTP_HOST &"/">
<cfelse>
<cfset baseURL = "http://"& CGI.HTTP_HOST &"/">
</cfif>

<!--- Extract the style and sample model from the WCP file --->
<cfset wcp = XMLParse( ExpandPath("./testDial.wcp") )>
<cfset chartStyle = ToString(wcp.project.style.dialChart)>

<!--- create sample chart data --->
<cfsavecontent variable="chartModel"><?xml version="1.0" encoding="UTF-8"?>
<XML type="default">
<COL>2000</COL>
<COL>2001</COL>
<COL>2002</COL>
<COL>2003</COL>
<COL>2004</COL>
<ROW col0="120.0" col1="0.0" col2="100.0" col3="180.0" col4="200.0">Sample 0:</ROW>
</XML>
</cfsavecontent>

<!--- initialize chart settings --->
<cfscript>
oMyWebChart = createObject("Java","com.gp.api.jsp.MxServerComponent");
oMyApp = getPageContext().getServletContext();
oSvr = oMyWebChart.getDefaultInstance(oMyApp);
oMyChart2 = oSvr.newImageSpec();
oMyChart2.width = 400;
oMyChart2.height= 300;
oMyChart2.type = "png";
oMyChart2.style = "#chartStyle#";
oMyChart2.model = "#chartModel#";
</cfscript>

<!--- Create html tag set --->
<cfsavecontent variable="chartImgTag">
<cfoutput>#oSvr.getImageTag(oMyChart2, baseURL& "CFIDE/GraphData.cfm?graphCache=wc50&graphID=")#</cfoutput>
</cfsavecontent>

<!--- Good old Webcharts loves to add an extra /Images/ to the URL --->
<cfset chartImgTag = replace(chartImgTag, baseURL &"Images/", baseURL, "All")>

<h2>Dial Chart</h2>
<cfoutput>
#chartimgtag#
</cfoutput>


Radar Chart

<!--- See http://www.webcharts3d.com/website/WebCharts50/cf/faq.jsp --->
<cfchart chartwidth="1" chartheight="1"/>

<!--- Get base server url --->
<cfif len(CGI.HTTPS)>
<cfset baseURL = "https://"& CGI.HTTP_HOST &"/">
<cfelse>
<cfset baseURL = "http://"& CGI.HTTP_HOST &"/">
</cfif>

<!--- Extract the chart style from the WCP file --->
<cfset wcp = XMLParse( ExpandPath("./testRadar.wcp") )>
<cfset chartStyle = ToString(wcp.project.style.radarChart)>

<!--- create sample chart data --->
<cfsavecontent variable="chartModel"><?xml version="1.0" encoding="UTF-8"?>
<XML type="default">
<COL>100</COL>
<COL>200</COL>
<COL>300</COL>
<COL>350</COL>
<COL>400</COL>
<ROW col0="100.0" col1="200.0" col2="100.0" col3="180.0" col4="200.0">Sample 0:</ROW>
<ROW col0="150.0" col1="300.0" col2="250.0" col3="230.0" col4="250.0">Sample 1:</ROW>
<ROW col0="200.0" col1="400.0" col2="400.0" col3="280.0" col4="300.0">Sample 2:</ROW>
<ROW col0="250.0" col1="500.0" col2="550.0" col3="330.0" col4="350.0">Sample 3:</ROW>
</XML>
</cfsavecontent>

<!--- initialize chart settings --->
<cfscript>
oMyWebChart = createObject("Java","com.gp.api.jsp.MxServerComponent");
oMyApp = getPageContext().getServletContext();
oSvr = oMyWebChart.getDefaultInstance(oMyApp);
oMyChart2 = oSvr.newImageSpec();
oMyChart2.width = 400;
oMyChart2.height= 300;
oMyChart2.type = "png";
oMyChart2.style = "#chartStyle#";
oMyChart2.model = "#chartModel#";
</cfscript>

<!--- Create html tag set --->
<cfsavecontent variable="chartImgTag">
<cfoutput>#oSvr.getImageTag(oMyChart2, baseURL& "CFIDE/GraphData.cfm?graphCache=wc50&graphID=")#</cfoutput>
</cfsavecontent>

<!--- Good old Webcharts loves to add an extra /Images/ to the URL --->
<cfset chartImgTag = replace(chartImgTag, baseURL &"Images/", baseURL, "All")>

<h2>Radar Chart</h2>
<cfoutput>
#chartimgtag#
</cfoutput>


Bubble Chart

<!--- See http://www.webcharts3d.com/website/WebCharts50/cf/faq.jsp --->
<cfchart chartwidth="1" chartheight="1"/>

<!--- Get base server url --->
<cfif len(CGI.HTTPS)>
<cfset baseURL = "https://"& CGI.HTTP_HOST &"/">
<cfelse>
<cfset baseURL = "http://"& CGI.HTTP_HOST &"/">
</cfif>

<!--- Extract the chart style from the WCP file --->
<cfset wcp = XMLParse( ExpandPath("./testBubble.wcp") )>
<cfset chartStyle = ToString(wcp.project.style.frameChart)>

<!--- create sample chart data --->
<cfsavecontent variable="chartModel"><?xml version="1.0" encoding="UTF-8"?>
<XML type="default">
<COL>100</COL>
<COL>200</COL>
<COL>300</COL>
<COL>350</COL>
<COL>400</COL>
<ROW col0="100.0" col1="200.0" col2="100.0" col3="180.0" col4="200.0">Sample 0:</ROW>
<ROW col0="150.0" col1="300.0" col2="250.0" col3="230.0" col4="250.0">Sample 1:</ROW>
<ROW col0="200.0" col1="400.0" col2="400.0" col3="280.0" col4="300.0">Sample 2:</ROW>
<ROW col0="250.0" col1="500.0" col2="550.0" col3="330.0" col4="350.0">Sample 3:</ROW>
</XML>
</cfsavecontent>

<!--- initialize chart settings --->
<cfscript>
oMyWebChart = createObject("Java","com.gp.api.jsp.MxServerComponent");
oMyApp = getPageContext().getServletContext();
oSvr = oMyWebChart.getDefaultInstance(oMyApp);
oMyChart2 = oSvr.newImageSpec();
oMyChart2.width = 400;
oMyChart2.height= 300;
oMyChart2.type = "png";
oMyChart2.style = "#chartStyle#";
oMyChart2.model = "#chartModel#";
</cfscript>

<!--- Create html tag set --->
<cfsavecontent variable="chartImgTag">
<cfoutput>#oSvr.getImageTag(oMyChart2, baseURL& "CFIDE/GraphData.cfm?graphCache=wc50&graphID=")#</cfoutput>
</cfsavecontent>

<!--- Good old Webcharts loves to add an extra /Images/ to the URL --->
<cfset chartImgTag = replace(chartImgTag, baseURL &"Images/", baseURL, "All")>

<h2>Bubble Chart</h2>
<cfoutput>
#chartimgtag#
</cfoutput>

...Read More

Friday, January 18, 2008

Damn.. I hate these guys!

A few weeks ago I discovered the webcharts utility that has been bundled with ColdFusion for oh ... say a few years ;) I noticed its gauge chart and thought it would be neat if that worked in ColdFusion. Well I had not forgotten the idea, but I got busy with other things. Long story short today I see an entry on Raymond Camden's blog about .. wait for it .. creating gauge charts in ColdFusion! He posted a really cool example, crediting Simon Haddan and Christopher Wigginton for the idea.

All I can say is damn Simon Haddan, Christopher Wigginton, Raymond Camden, their great ideas .. "and their little dog too!" ;)

...Read More

Thursday, January 17, 2008

Text Wrapping with Images and ColdFusion 8

One question that has plagued me since I started using ColdFusion 8's image functions, is how to wrap text on an image. Today I discovered a real jewel in the java API that demonstrates how to wrap text. I ported it ColdFusion and so far it seems to work equally well in CF. Who knew documentation could be so useful? ;)

Now the function may seem complex at first, but when you break it down it is surprisingly simply. I will just highlight the important parts of the code. Otherwise this entry will be the size of War and Peace. (Okay, it might be anyway so consider yourself warned .. and consider refreshing your coffee or beer now).

Where do they get these names?

The heart of the work is performed by two classes: LineBreakMeasurer and AttributedCharacterIterator. As the name implies, the LineBreakMeasurer class is used to break text into lines. Despite the intimidating name, AttributedCharacterIterator just provides a method for looping through a string of text. The "Attributed" part comes from the fact that the class stores more than just text. It also stores related attributes like font information. With that in mind, time to jump into the code.

War and Peace - The abbreviated version

Now the first thing the function does is extracts the underlying graphics object from our ColdFusion image. This allows us to grab what is called the FontRendererContext which is used for measuring graphical text.

<cfscript>
Local.buffered = ImageGetBufferedImage(arguments.image);
Local.graphics = Local.buffered.getGraphics();
Local.context = Local.graphics.getFontRenderContext();
</cfscript>

Next we create a Font object with the desired style, font, and size for our text. This is important because they all contribute to how much space the graphical text occupies. Once we have the basic information, we create what is called an AttributedString. That just means a string of text with some associated properties, like our font. Then we use the AttributedString to get our iterator. (We are almost there)

<cfscript>
// create a font object that will be used to draw the text
Local.textFont = createJavaFont( argumentCollection = arguments.fontAttributes );

Local.attrString = createObject("java", "java.text.AttributedString").init( arguments.text );
Local.TextAttribute = createObject("java", "java.awt.font.TextAttribute");
Local.attrString.addAttribute( Local.TextAttribute.FONT, Local.textFont );
Local.attrIterator = Local.attrString.getIterator();
</cfscript>

Finally, we create the last and most important object: our LineBreakMeasurer. We pass in both the AttributedString and the FontRendererContext so the object can accurately measure our text.

<cfscript>
Local.measurer = createObject("java", "java.awt.font.LineBreakMeasurer").init(
Local.attrIterator,
Local.context
);
</cfscript>

Then we use the measurer to loop through the text and split it into lines. The measurer does all of the hard work. We only need to tell it the width of the wrapping area and it auto-magically calculates how much of the text can fit on the current line. The measurer passes back a very useful object called a TextLayout. It contains lots of good information about the dimensions of the current line of text. Our function code uses both the layout and measurer to extract and store the dimensions and number of characters in each line. I will leave you to explore the rest of the function code on your own.

<cfscript>
...
// while there are still characters to read
while ( Local.measurer.getPosition() LT Local.attrIterator.getEndIndex() ) {

// get the layout (bounds) of the current line of text
Local.layout = Local.measurer.nextLayout( Local.wrapWidth );
...

// get the y coordinate
Local.posY = Local.posY + Local.layout.getAscent();

// get the x coordinate
if ( Local.layout.isLeftToRight()) {
Local.posX = arguments.x;
}
else {
Local.posX = arguments.x + Local.maxWidth - Local.layout.getAdvance();
}

...
// calculate the final y position
Local.posY = Local.posY + Local.layout.getDescent() + Local.layout.getLeading();
}
</cfscript>



Are we there yet?


Now for the good part. Here is an example of how you could use the attached functions to draw two equal sized columns of wrapped text on an image.


<!--- create the image --->
<cfset img = ImageNew("", 600, 200, "rgb", "lightgray")>
<cfset textProp = { font="Arial", size="14" }>
<cfset text = "Most modern calendars mar the sweet simplicity of our lives by">
<cfset text = text &" reminding us that each day that passes is the anniversary of">
<cfset text = text &" some perfectly uninteresting event.">

<!--- this will create a margin of 15 on both sides of the column --->
<cfset x = 15>
<cfset y = 15>
<cfset margin = 15>
<cfset columnWidth = (ImageGetWidth(img)/2) - (margin*2)>

<!--- draw the wrapped text in the left column --->
<cfset ImageSetDrawingColor( img, "black")>
<cfset dimen = ImageDrawWrappedText( img, text, x, y, columnWidth, textProp )>
<cfset ImageSetDrawingColor(img, "blue")>
<cfset ImageDrawRect(img, x, y, dimen.width, dimen.height)>

<!--- draw it one more time on in the right column --->
<!--- adjust the x coordinate to take into account the column we just drew --->
<cfset x = (ImageGetWidth(img)/2) + margin>
<cfset ImageSetDrawingColor( img, "black")>
<cfset dimen = ImageDrawWrappedText( img, text, x, y, columnWidth, textProp )>
<cfset ImageSetDrawingColor(img, "red")>
<cfset ImageDrawRect(img, x, y, dimen.width, dimen.height)>

<cfimage action="writeToBrowser" source="#img#">


The results should look like this.



As they say "That's all she wrote". Comments/questions/suggestions are welcome.


Updated 14-10-2008: Updated to included alignment option (left,center,right)


ImageDrawWrappedText Function

<cffunction name="ImageDrawWrappedText" returntype="struct" access="public" output="false">
<cfargument name="image" type="any" required="true" hint="ColdFusion image object">
<cfargument name="text" type="string" required="true" hint="The text to draw on the image. Minimum of 1 character.">
<cfargument name="x" type="numeric" required="true" hint="The starting x coordinate. Minimum value is 0.">
<cfargument name="y" type="numeric" required="true" hint="The starting y coordinate. Minimum value is 0.">
<cfargument name="wrapWidth" type="numeric" required="true" hint="Wrap the text within this width. Minimum value is 1.">
<cfargument name="fontAttributes" type="struct" required="false" default="#structNew()#" hint="Text font, style and or size">
<cfargument name="align" type="string" required="false" default="left" hint="Text alignment (Left, Right or Center).">

<cfset var Local = structNew()>

<!--- get the dimensions of the wrapped text --->
<cfset Local.dimen = GetTextWrapInfo( argumentCollection = arguments )>

<!--- loop through the array of lines, and draw each line of text --->
<cfloop from="1" to="#arrayLen(Local.dimen.lines)#" index="Local.x">
<cfset Local.line = Local.dimen.lines[ Local.x ]>
<cfset Local.text = Mid( arguments.text, Local.line.startAt, Local.line.endAt )>
<cfset ImageDrawText( arguments.image, Local.text, Local.line.x, Local.line.y, arguments.fontAttributes) >
</cfloop>

<cfreturn Local.dimen>
</cffunction>


GetTextWrapInfo Function

<cffunction name="GetTextWrapInfo" returntype="struct" access="public" output="true"
hint="Calculates the amount of text to draw on each line. Returns the overall text dimensions and an array of lines. ">

<cfargument name="image" type="any" required="true" hint="ColdFusion image object">
<cfargument name="text" type="string" required="true" hint="The text to draw on the image. Must be at least 1 character.">
<cfargument name="x" type="numeric" required="true" hint="The starting x coordinate. Minimum value is 0.">
<cfargument name="y" type="numeric" required="true" hint="The starting y coordinate. Minimum value is 0.">
<cfargument name="wrapWidth" type="numeric" required="true" hint="Wrap the text within this width. Minimum value is 1.">
<cfargument name="fontAttributes" type="struct" required="false" default="#structNew()#" hint="Text font, style and or size">
<cfargument name="align" type="string" required="false" default="left" hint="Text alignment (Left, Right or Center).">

<cfset var Local = structNew()>

<!--- verify the supplied arguments --->
<cfif NOT IsImage(arguments.image) >
<cfthrow message="Argument.Image must be a ColdFusion image object">
</cfif>
<cfif NOT len(arguments.text) >
<cfthrow message="Argument.text must contain at least 1 character">
</cfif>
<cfif arguments.x LT 0 OR arguments.y LT 0>
<cfthrow message="The x and y coordinates cannot be less than 0.">
</cfif>
<cfif arguments.wrapWidth LT 1>
<cfthrow message="The minimum wrapWidth value allowed is 1.">
</cfif>
<cfif not listFindNoCase("left,right,center", trim(arguments.align))>
<cfthrow message="Valid align values are: left,right,center.">
</cfif>

<cfscript>
// create an array for storing the results
Local.lines = [];

// get the underlying graphics object and the renderer context for measuring the text
Local.buffered = ImageGetBufferedImage(arguments.image);
Local.graphics = Local.buffered.getGraphics();
Local.context = Local.graphics.getFontRenderContext();

// create a font object that will be used to draw the text
Local.textFont = createJavaFont( argumentCollection = arguments.fontAttributes );

// the AttributedString holds information about the image text. namely the
// characters to be drawn and the font used to determine the size of the text
Local.attrString = createObject("java", "java.text.AttributedString").init( arguments.text );
Local.TextAttribute = createObject("java", "java.awt.font.TextAttribute");
Local.attrString.addAttribute( Local.TextAttribute.FONT, Local.textFont );
// the iterator allows us to loop over the characters in our text string below
Local.attrIterator = Local.attrString.getIterator();

// create a LineBreakMeasurer for breaking our text into individual lines
Local.measurer = createObject("java", "java.awt.font.LineBreakMeasurer").init(
Local.attrIterator,
Local.context
);

Local.LEFT_ALIGN = "left";
Local.RIGHT_ALIGN = "right";
Local.CENTER_ALIGN = "center";
Local.leftMargin = arguments.x;
Local.rightMargin = arguments.wrapWidth + arguments.x;
Local.alignment = trim( arguments.align );

Local.posY = arguments.y;
Local.posX = arguments.x;
Local.wrapWidth = javacast("float", arguments.wrapWidth);
Local.maxWidth = 0;
Local.maxHeight = 0;

// while there are still characters to read
while ( Local.measurer.getPosition() LT Local.attrIterator.getEndIndex() ) {
// get the starting character position
Local.startAt = Local.measurer.getPosition();

// get the layout of the current line of text
Local.layout = Local.measurer.nextLayout( Local.wrapWidth );
Local.posY = Local.posY + Local.layout.getAscent();

/* 2008-10-14: replace with alignment attribute
if ( Local.layout.isLeftToRight()) {
Local.posX = arguments.x;
}
else {
Local.posX = arguments.x + Local.maxWidth - Local.layout.getAdvance();
}
*/
/////////////////////////////////////////////////////////////////////
// Calculate the x coordinate of this line based on alignment
/////////////////////////////////////////////////////////////////////
if ( Local.layout.isLeftToRight()) {

if ( Local.alignment == Local.RIGHT_ALIGN ) {
Local.posX = Local.rightMargin - Local.layout.getVisibleAdvance();
}
else if ( Local.alignment == Local.CENTER_ALIGN ) {
Local.posX = ( Local.leftMargin + Local.rightMargin - Local.layout.getVisibleAdvance()) / 2;
}
else {
// Otherwise, use left alignment
Local.posX = arguments.x;
}

}
else {

if ( Local.alignment == Local.LEFT_ALIGN ) {
Local.posX = arguments.x + Local.maxWidth - Local.layout.getAdvance();
}
else if ( Local.alignment == Local.CENTER_ALIGN ) {
Local.posX = ( Local.leftMargin + Local.rightMargin + Local.layout.getAdvance()) / 2 - Local.layout.getAdvance();
}
else {
// Otherwise, use right alignment
Local.posX = Local.leftMargin + ( Local.layout.getVisibleAdvance() - layout.getAdvance() );
}

}
// save the current line information. note, we add 1 to the character
// positions because CF string functions are 1-based and java's are 0-based
Local.currentLine = { x = Local.posX,
y = Local.posY,
startAt = Local.startAt + 1,
endAt = Local.measurer.getPosition() - Local.startAt
};
arrayAppend( Local.lines, Local.currentLine );

// save the maximum line width
Local.currentWidth = Local.layout.getBounds().getWidth();
if ( Local.maxWidth LT Local.currentWidth ) {
Local.maxWidth = Local.currentWidth;
}

// calculate the final y position
Local.posY = Local.posY + Local.layout.getDescent() + Local.layout.getLeading();

}

Local.graphics.dispose();
Local.results = { x = arguments.x,
y = arguments.y,
width = Local.maxWidth,
height = Local.posY - arguments.y,
lines = Local.lines
};
</cfscript>

<cfreturn Local.results>
</cffunction>


CreateJavaFont Function

<cffunction name="CreateJavaFont" returntype="any" access="public" output="true"
hint="Creates a java.awt.Font object using the supplied text attributes. All attributes are optional.">

<cfargument name="font" type="string" required="false" hint="Font name (example: Arial)">
<cfargument name="style" type="string" required="false" default="plain" hint="Font style (Plain, Italic, Bold, BoldItalic)">
<cfargument name="size" type="numeric" required="false" hint="Font size in points">
<cfset var Local = structNew()>

<cfscript>
Local.Font = createObject("java", "java.awt.Font");

// get the default font properties
Local.DefaultFont = Local.Font.init( javacast("null", "") );
Local.prop = { font = Local.DefaultFont.getName(),
style = Local.DefaultFont.getStyle(),
size = Local.DefaultFont.getSize()
};

// extract the supplied font name
if ( structKeyExists(arguments, "font") ) {
Local.prop.font = arguments.font;
}
// extract the supplied font size
if ( structKeyExists(arguments, "size") ) {
Local.prop.size = arguments.size;
}
// extract and convert the supplied font style
if ( structKeyExists(arguments, "style") ) {
switch (arguments.style) {
case "bold":
Local.prop.style = Local.Font.BOLD;
break;
case "italic":
Local.prop.style = Local.Font.ITALIC;
break;
case "bolditalic":
Local.prop.style = BitOr( Local.Font.BOLD, Local.Font.ITALIC);
break;
case "plain":
Local.prop.style = Local.Font.PLAIN;
break;
default:
Local.prop.style = Local.Font.PLAIN;
break;
}
}

// create a java font using the current properties
Local.javaFont = Local.Font.init( javacast("string", Local.prop.font),
javacast("int", Local.prop.style),
javacast("int", Local.prop.size)
);
</cfscript>

<cfreturn Local.javaFont>
</cffunction>

...Read More

Wednesday, January 16, 2008

Measuring image text width and height using ColdFusion 8 (Follow up)

In the entry Measuring image text width and height using ColdFusion 8 I talked about two methods for measuring the width and height of text string drawn on an image. But I never got around to posting the function I was working on last night!

Before I do, I wanted to mention one gotcha I discovered about images and default font information. Font properties supplied to the ImageDrawText function are optional. If you omit one of the attributes like style or size, ColdFusion simply uses default values instead. As you will see in the function below, you can use the graphics of an image to obtain the default font information. With one exception: the size. The ColdFusion documentation indicates it uses a default font size of 10 points. On my system the default font size in java is 12. So you must override the default font size or you may get the wrong results when a size is not specified.


UPDATE: After running some checks on the Fonts, I am not convinced the default font size is 10 points. So I have updated the code to use the java defaults instead.


Example

<!--- create the image --->
<cfset img = ImageNew("", 300, 80, "rgb", "lightgray")>
<!--- get the text dimensions --->
<cfset prop = { font="Times New Roman", style="bolditalic", size=18}>
<cfset dimen = ImageGetTextDimensions(img, "Draw this! ÀÇŸÍÊÃ", prop)>
<!--- draw the text --->
<cfset ImageSetAntiAliasing(img, "on")>
<cfset ImageSetDrawingColor(img, "black")>
<cfset ImageDrawText(img, "Draw this! ÀÇŸÍÊÃ", 15, 55, prop)>

<!--- display the image and dimensions--->
<cfimage action="writeToBrowser" source="#img#">
<cfdump var="#dimen#">


Function Code

<cffunction name="ImageGetTextDimensions" returntype="struct" access="public" output="false">
<cfargument name="image" type="any" required="true">
<cfargument name="text" type="string" required="true">
<cfargument name="fontAttributes" type="any" required="false" default="#structNew()#">

<cfset var Local = structNew()>

<cfscript>
// create a reusable font object
Local.Font = createObject("java", "java.awt.Font");
Local.DefaultFont = Local.Font.init( javacast("null", "") );

// determine if any font attributes were supplied
Local.fontPropertiesFound = structKeyExists(arguments, "fontAttributes");

// extract the supplied font name
if (Local.fontPropertiesFound AND structKeyExists(arguments.fontAttributes, "font") ) {
Local.text.font = arguments.fontAttributes.font;
}
else {
// if no font was given, get default system font from the graphics object
Local.text.font = Local.DefaultFont.getName();
}

// extract the font size
if (Local.fontPropertiesFound AND structKeyExists(arguments.fontAttributes, "size") ) {
Local.text.size = arguments.fontAttributes.size;
}
else {
Local.text.size = Local.DefaultFont.getSize();
}

// extract the font style
if (Local.fontPropertiesFound AND structKeyExists(arguments.fontAttributes, "style") ) {
switch (arguments.fontAttributes.style) {
case "bold":
Local.text.style = Local.Font.BOLD;
break;
case "italic":
Local.text.style = Local.Font.ITALIC;
break;
case "bolditalic":
Local.text.style = BitOr( Local.Font.BOLD, Local.Font.ITALIC);
break;
case "plain":
Local.text.style = Local.Font.PLAIN;
break;
}
}
else {
// if no style was given, use the default "plain" (per CF8 documentation)
Local.text.style = Local.DefaultFont.getStyle();
}

// create a font for drawing the text
Local.textFont = Local.Font.init( javacast("string", Local.text.font),
javacast("int", Local.text.style),
javacast("int", Local.text.size)
);

// extract the graphics from the underlying buffered image
Local.graphics = ImageGetBufferedImage(arguments.image).getGraphics();

// get the renderer context for measuring the text
Local.context = Local.graphics.getFontRenderContext();

// use a TextLayout to obtain the graphical representation of the text
Local.textLayout = createObject("java", "java.awt.font.TextLayout").init(
javacast("string", arguments.text),
Local.textFont,
Local.context
);

// get the text bounds
Local.textBounds = Local.textLayout.getBounds();

// save the text dimensions
Local.dimensions.width = Local.textBounds.getWidth();
Local.dimensions.height = Local.textBounds.getHeight();
Local.dimensions.leading = Local.textlayout.getLeading();
Local.dimensions.ascent = Local.textlayout.getAscent();
Local.dimensions.descent = Local.textlayout.getDescent();
Local.dimensions.advance = Local.textlayout.getAdvance();
Local.dimensions.visibleAdvance = Local.textlayout.getVisibleAdvance();

Local.graphics.dispose();
</cfscript>

<cfreturn Local.dimensions >
</cffunction>

...Read More

Measuring image text width and height using ColdFusion 8

Yesterday I saw an interesting question on houseoffusion.com about how to measure the width and height of a text string drawn on an image. Curious, I checked the image functions in the documentation but did not find anything suitable. So I checked the java API's and found that the TextLayout object can be used to obtain the width and height of a graphical text string.

Now originally I had thought of using a method called getStringBounds() in the FontMetrics class. But I decided against it after reading in the API that "the logical bounds does not always enclose all the text... To obtain a visual bounding box, which encloses all the text, use the getBounds method of TextLayout".

So I ran some tests using TextLayout with a few different characters, fonts and styles and it appeared to work correctly. So I suggested the poster try using TextLayout and started to write up an entry about it.

As often happens, you get a neat idea and then find out someone else beat you to it. Case in point, today I saw a post by Ben Nadel mentioning an entry he did a few months ago, based on Barney Boisvert's work. The entry dealt with images and text measurement. I went to read the entry and the first thing that caught my attention was that it uses the getStringBounds() method. That piqued my curiosity. So I decided to test the veracity of the API and run the two methods side by side.

First I created a new image and drew some text. I deliberately selected a few funky characters that were likely to stretch outside the normal bounds. For illustration purposes, I also drew a rectangle to show the text bounds as reported by FontMetrics and TextLayout.

On the left (in red) are the dimensions reported by FontMetrics. If you look closely, you will see the text is slightly wider than the FontMetrics width would lead you to believe. The TextLayout dimensions (in blue) appear to be slightly more accurate.



Note, the positioning of the rectangles is slightly off. Since I am not drawing the text directly onto the image I cannot take advantage of the boundary x,y coordinates.


Another interesting difference is the FontMetric height includes the leading value. Whereas the TextLayout height does not. Roughly translated, the leading value is the distance between lines of text.

So it looks like the API was correct and that TextLayout provides a more accurate measurement of text width and height. Having said that, from everything I have read so far, font measurement is a complex topic. Especially when handling some of the more exotic character sets. So I would not expect this technique to be as useful with those character sets.

If you are interested in learning more about the java classes have a look at the API and/or graphics tutorials on the sun.com site.


Test code - Create Image


<cfscript>
// initialize the text properties
Font = CreateObject("java", "java.awt.Font");
text = { x = 50, y = 100, string = "ÀÇŸÍÊÃ" };
text.prop = { font="Times New Roman", style="bolditalic", javaStyle= BitOr(Font.BOLD, Font.ITALIC), size=25 };

// create a new image and draw the text
img = ImageNew("", 450, 150, "rgb", "lightgray");
ImageSetDrawingColor( img, "black" );
ImageDrawText( img, text.string, text.x, text.y, text.prop );
</cfscript>


Test FontMetrics dimensions

<cfscript>
// get the underlying graphic of the image
graphics = ImageGetBufferedImage( img ).getGraphics();

// recreate the font used to draw the text
currentFont = Font.init( javacast("string", text.prop.font),
javacast("int", text.prop.javaStyle),
javacast("int", text.prop.size)
);

// get text measurements using font metrics
fontMetrics = graphics.getFontMetrics( currentFont );
fontBounds = fontMetrics.getStringBounds( javacast("string", text.string), graphics );

// draw a rectangle indicatating the font bounds
Color = createObject("java", "java.awt.Color");
graphics.setColor(Color.RED);
graphics.drawRect(
javacast("int", text.x),
javacast("int", text.y) - fontMetrics.getAscent(),
fontBounds.getWidth(),
fontBounds.getHeight()
);

// get the dimensions
dimensions.type = "FontMetrics";
dimensions.width = fontBounds.getWidth();
dimensions.height = fontBounds.getHeight();
dimensions.leading = fontMetrics.getLeading();
dimensions.ascent = fontMetrics.getAscent();
dimensions.descent = fontMetrics.getDescent();
graphics.dispose();
</cfscript>

<cfdump var="#dimensions#">
<cfimage action="writeToBrowser" source="#img#">


Test TextLayout dimensions

<cfscript>
// get the text measurements using text layout
graphics = ImageGetBufferedImage( img ).getGraphics();
context = graphics.getFontRenderContext();
TextLayout = createObject("java", "java.awt.font.TextLayout");
layout = TextLayout.init( text.string, currentFont, context );
layoutBounds = layout.getBounds();

// draw a rectangle to indicating the layout bounds
Color = createObject("java", "java.awt.Color");
graphics.setColor( Color.BLUE );
graphics.drawRect( javacast("int", text.x) - layout.getLeading() - 2,
javacast("int", text.y) - layout.getAscent() + 1,
layoutBounds.getWidth(),
layoutBounds.getHeight()
);

// get the dimensions
dimensions.type = "TextLayout";
dimensions.width = layoutBounds.getWidth();
dimensions.height = layoutBounds.getHeight();
dimensions.leading = layout.getLeading();
dimensions.ascent = layout.getAscent();
dimensions.descent = layout.getDescent();

graphics.dispose();
</cfscript>

<cfdump var="#dimensions#">
<cfimage action="writeToBrowser" source="#img#">

...Read More

Tuesday, January 15, 2008

Determining ColdFusion's class path and user account programatically

Two questions that come up frequently on blogs or forums is how to check ColdFusion's class path and how to determine the user account used by the ColdFusion service. The usual answer is to check the jvm.config file (classpath), and the windows o/s control panel > services (user account).

While working with the java.lang.System class today, I stumbled across two properties that appear to reveal that information. Now you may already be familiar with this. But I myself am still slightly incredulous I never noticed it before.

Note, property names are case sensitive!


<!--- cfSearching: display the CF classpath and user account name --->
<cfset sys = createObject("java", "java.lang.System")>

<!--- cfSearching: display the CF classpath and user account name --->
<cfoutput>
<b>user.name</b> = #sys.getProperty("user.name", "not found")#<hr>
<b>coldfusion.classPath</b> = #sys.getProperty("coldfusion.classPath")#<br>
</cfoutput>


To display all System properties


<!--- cfSearching: show all properties --->
<cfset prop = createObject("java", "java.lang.System").getProperties()>
<cfdump var="#prop#">


This one will definitely be added to my utility library.

...Read More

Sunday, January 13, 2008

Creating Transparent Gif's with ColdFusion 8 - Part 2

In Part 1 (or as I like to think of it "More than you ever wanted to know about GIF's") we learned how to extract the color components from a GIF's color model, and use the Raster to obtain the color index of a specific pixel. We can now use that information to construct a new color model, but this time with a transparent color.

<cfscript>
// cfSearching: colors[1] - red, colors[2] - green, colors[3] - blue
IndexColorModel = createObject("java", "java.awt.image.IndexColorModel");
newModel = IndexColorModel.init(javacast("int", 8),
javacast("int", colorSize),
colors[1],
colors[2],
colors[3],
javacast("int", transparentColorIndex)
);
</cfscript>
Now that we have a model that supports transparency, we can use it to create our transparent GIF. This is done by creating a new BufferedImage., which is then passed into the ImageNew() function, to create a ColdFusion compatible image object. That is it. We now have a transparent GIF!
<cfscript>
BufferedImage = createObject("java", "java.awt.image.BufferedImage");
newBufferedImage = BufferedImage.init( newModel,
sourceImage.getRaster(),
sourceImage.isAlphaPremultiplied(),
javacast("null", "")
);

transparentImage = ImageNew( newBufferedImage );
</cfscript>

Caveats

Now the thing to remember about GIF's is that they only support fully opaque or fully transparent pixels. So GIF's with curved edges may appear jagged. You can create smoother edges by using anti-aliasing. However, this effect is achieved by blending the edges with the image background color, which results in a faint halo of color around the edges. So you must display the image on a similar background color to maintain the illusion of transparency. This is just the nature of GIF's. The PNG format on the other hand, is able to achieve smooth edges by using variable transparency. By using semi-transparent colors, it creates the illusion of smooth edges without the halo effect.

Finally, the code

This function is rough, but you can test it out using one of the examples below. I have included examples of how to create a new transparent GIF and convert an existing one. Comments, corrections or suggestions are always welcome.

Enjoy!

Use an existing GIF - Example 1
<!--- use the pixel at position x=0, y=0 for the transparent color --->
<cfset sourceImage = ImageNew("https://www.google.com/accounts/reader/screenshot_en.gif")>
<cfset newImage = convertToTransparentGif(source=sourceImage, x=0, y=0)>
<div style="clear: both;">
Use an existing GIF - Example 1<br>
Notice the faint halo of color around the edges due to anti-aliasing<br><br>
<div style="background-color: #ffffcc; padding: 10;float: left;">
<b>Original (opaque)</b><br><br>
<img src="https://www.google.com/accounts/reader/screenshot_en.gif">
</div>
<div style="background-color: #ffffcc; padding: 10;float: right;">
<b>New (transparent, but with anti-aliasing)</b><br><br>
<cfimage action="writeToBrowser" source="#newImage#" format="gif">
</div>
<br><br>
</div>
Use an existing GIF - Example 2
<!--- use the pixel at position x=0, y=0 for the transparent color --->
<cfset sourceImage = ImageNew("http://www.sun.com/software/images/promotions/getjava.gif")>
<cfset newImage = convertToTransparentGif(source=sourceImage, x=0, y=0)>
<div style="clear: both;">
<div style="clear: both;">
Use an existing GIF - Example 2<br>
Notice the faint halo of color around the edges due to anti-aliasing<br><br>
</div>
<div style="background-color: #b0c4de; padding: 10;float: left;">
<b>Original (opaque)</b><br><br>
<img src="http://www.sun.com/software/images/promotions/getjava.gif">
</div>
<div style="background-color: #f5f5f5; padding: 10;float: left;">
<b>New (transparent, but with anti-aliasing)</b><br><br>
<cfimage action="writeToBrowser" source="#newImage#" format="gif">
</div>
</div>
Create a New Transparent GIF - Example 1
<!--- cfSearching: Note the type is *ARGB* and anti-aliasing is ON --->
<cfset imageBackgroundColor = "##ffffff">
<cfset img = ImageNew("", 400, 200, "argb", imageBackgroundColor)>
<cfset ImageSetAntiAliasing(img, "ON")>
<cfset ImageSetDrawingColor(img, "##336600")>
<cfset ImageDrawRoundRect(img, 125, 26, 200, 99, 20, 20, "true")>
<cfset ImageSetDrawingColor(img,"##003366")>
<cfset fontAttr = StructNew()>
<cfset fontAttr.font = "Arial">
<cfset fontAttr.size = 32>
<cfset fontAttrr.style = "bold">
<cfset ImageDrawText(img, "Can you see ..", 65, 80, fontAttr)>
<cfset ImageSetDrawingColor(img, "##003366")>
<cfset ImageDrawRoundRect(img, 32, 90, 200, 99, 20,20, "true")>
<cfset ImageSetDrawingColor(img,"##336600")>
<cfset ImageDrawText(img, "right through me?", 92, 155, fontAttr)>

<cfset newImage = convertToTransparentGif(img, imageBackgroundColor)>
<cfset ImageWrite(newImage, ExpandPath("newGifExample1.gif"))>
<div style="clear: both;">
Create a new GIF - Example 1<br>
Notice the faint halo of color around the edges due to anti-aliasing<br><br>
</div>
<br>
<div style="background-color: #f5f5f5; width: 400; height: 200;">
<cfimage action="writeToBrowser" source="#newImage#" format="gif">
</div>
Create a New Transparent GIF - Example 2
<!--- cfSearching: Note the type is *RGB* and anti-aliasing is OFF --->
<cfset img = ImageNew("", 400, 200, "rgb")>
<cfset ImageSetAntiAliasing(img, "OFF")>
<cfset ImageSetDrawingColor(img, "##336600")>
<cfset ImageDrawRoundRect(img, 125, 26, 200, 99, 20, 20, "true")>
<cfset ImageSetDrawingColor(img,"##003366")>
<cfset fontAttr = StructNew()>
<cfset fontAttr.font = "Arial">
<cfset fontAttr.size = 32>
<cfset fontAttrr.style = "bold">
<cfset ImageDrawText(img, "Can you see ..", 65, 80, fontAttr)>
<cfset ImageSetDrawingColor(img, "##003366")>
<cfset ImageDrawRoundRect(img, 32, 90, 200, 99, 20, 20, "true")>
<cfset ImageSetDrawingColor(img,"##336600")>
<cfset ImageDrawText(img, "right through me?", 92, 155, fontAttr)>

<cfset newImage = convertToTransparentGif(source=img, x=0, y=0)>
<cfset ImageWrite(newImage, ExpandPath("newGifExample2.gif"))>
<div style="clear: both;">
Create a new GIF - Example 2<br>
Notice the edges are jagged because we did not use anti-aliasing<br><br>
</div>
<br>
<div style="background-color: #b0c4de; width: 400; height: 200;">
<cfimage action="writeToBrowser" source="#newImage#" format="gif">
</div>
ConvertToTransparentGif Function
Based on code from PaulFMendler
http://forum.java.sun.com/thread.jspa?forumID=20&threadID=425160
<cffunction name="convertToTransparentGif" returntype="any" access="public" output="true"
hint="Converts a ColdFusion image, or BufferedImage object, into a transparent GIF">

<cfargument name="source" type="any" required="true" hint="A ColdFusion image object or a BufferedImage">
<cfargument name="color" type="string" required="false" default="" hint="The transparent color in hexadecimal format">
<cfargument name="x" type="numeric" required="false" default="-1" hint="Use the pixel at position x,y as the transparent color">
<cfargument name="y" type="numeric" required="false" default="-1" hint="Use the pixel at position x,y as the transparent color">
<cfset Local = structNew()>

<!--- cfSearching: validate that a valid image object was supplied --->
<cfif NOT IsImage(arguments.source) AND NOT IsInstanceOf(arguments.source, "java.awt.image.BufferedImage")>
<cfthrow message="Invalid argument. Source must be a ColdFusion image or a BufferedImage">
</cfif>

<!--- cfSearching: verify the correct transparency arguments were supplied --->
<cfset Local.transparentColor = replace(trim(arguments.color), "##", "", "all")>
<cfif Local.transparentColor eq "" AND arguments.x EQ -1 AND arguments.y EQ -1>
<cfthrow message="Missing argument. You must supply either a transparent color OR the X and Y coordinates of the transparent pixel.">

<cfelseif Local.transparentColor neq "" AND (arguments.x NEQ -1 OR arguments.y NEQ -1)>
<cfthrow message="Too many arguments. Supply EITHER a transparent color OR the X and Y coordinates of the transparent pixel.">
</cfif>

<cfset Local.useTransparentColor = len(Local.transparentColor)>
<cfif Local.useTransparentColor>
<!--- cfSearching: transparent color must be a 6 character hex value --->
<cfif len(Local.transparentColor) NEQ 6 OR ReFindNoCase("[^0-9a-f]", Local.transparentColor)>
<cfthrow message="Invalid argument. Transparent color must be a 6 character hexidecimal value.">
</cfif>
<cfelse>
<!--- cfSearching: x,y coordinates must be >= zero --->
<cfif arguments.x LTE -1 OR arguments.y LTE -1>
<cfthrow message="Invalid or missing argument. The minimum value for X and Y coordinates is 0.">
</cfif>
</cfif>

<cfscript>
Local.sourceImage = arguments.source;

// cfSearching: extract the BufferedImage if needed
if ( IsImage(arguments.source) ) {
Local.sourceImage = ImageGetBufferedImage(arguments.source);
}

// cfSearching: encode the image as a gif to create the correct color model
if ( NOT IsInstanceOf(Local.sourceImage.getColorModel(), "java.awt.image.IndexColorModel")) {
Local.ImageIO = createObject("java", "javax.imageio.ImageIO");
Local.bao = createObject("java", "java.io.ByteArrayOutputStream").init();
Local.ImageIO.write( Local.sourceImage, "gif", Local.bao);
Local.bai = createObject("java", "java.io.ByteArrayInputStream").init( Local.bao.toByteArray() );
Local.sourceImage = Local.ImageIO.read( Local.bai );
}

// cfSearching: extract the color model information
Local.sourceModel = Local.sourceImage.getColorModel();
Local.colorSize = Local.sourceModel.getMapSize();

// cfSearching: Construct byte array to store rgb component values from the color model
// cfSearching: Based on source from Christian Cantrell's blog http://weblogs.macromedia.com/cantrell/archives/2004/01/byte_arrays_and_1.cfm
Local.byteClass = createObject("java", "java.lang.Byte").TYPE;
Local.reflectArray = createObject("java","java.lang.reflect.Array");
Local.dimen = [ 3, Local.colorSize ];
Local.colors = Local.reflectArray.newInstance( Local.byteClass, javacast("int[]", Local.dimen) );

// cfSearching: Grab the rgb color components from source color model
// cfSearching: colors[1] - red, colors[2] - green, colors[3] - blue
Local.sourceModel.getReds( Local.colors[1] );
Local.sourceModel.getGreens( Local.colors[2] );
Local.sourceModel.getBlues( Local.colors[3] );

// cfSearching: initialize the transparent color index to "none"
Local.transIndex = -1;

if (Local.useTransparentColor) {
// cfSearching: Extract decimal RGB values of transparent color
Local.transRed = InputBaseN( Left(Local.transparentColor, 2), 16);
Local.transGreen = InputBaseN( Mid(Local.transparentColor, 3, 2), 16);
Local.transBlue = InputBaseN( Right(Local.transparentColor, 2), 16);

// cfSearching: Find the index of the transparent color in the source color model
for (Local.index = 1; Local.index LTE Local.colorSize; Local.index = Local.index + 1) {

// cfSearching: RGB color component arrays should all be the same size
Local.currRed = BitAnd( Local.colors[1][Local.index], 255);
Local.currGreen = BitAnd( Local.colors[2][Local.index], 255);
Local.currBlue = BitAnd( Local.colors[3][Local.index], 255);

if ( (Local.currRed EQ Local.transRed) AND (Local.currGreen EQ Local.transGreen)
AND (Local.currBlue EQ Local.transBlue)) {

// cfSearching: must subtract 1 because java arrays are zero based
Local.transIndex = Local.index - 1;
break;
}
}
}
else {
// cfSearching: Otherwise, use the pixel at the given coordinates as the transparent color
Local.transIndex = Local.sourceImage.getRaster().getSample(
javacast("int", arguments.x),
javacast("int", arguments.y),
javacast("int", 0)
);
}

// cfSearching: create new color model with transparent color
Local.IndexColorModel = createObject("java", "java.awt.image.IndexColorModel");
Local.newModel = Local.IndexColorModel.init(javacast("int", 8),
javacast("int", Local.colorSize),
Local.colors[1],
Local.colors[2],
Local.colors[3],
javacast("int", Local.transIndex)
);

Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.newBufferedImage = Local.BufferedImage.init(
Local.newModel,
Local.sourceImage.getRaster(),
Local.sourceImage.isAlphaPremultiplied(),
javacast("null", "")
);
</cfscript>

<cfreturn ImageNew( Local.newBufferedImage )>

</cffunction>

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep