Sunday, August 3, 2008

Adding an image to a worksheet with POI

I saw an interesting post on the adobe forums about POI. A member named MinOP mentioned having trouble adding an image to an Excel file . Here is the problematic section of their code:


<cfset patriarch = newSheet.createDrawingPatriarch()/>
<cfset patriarch.createPicture(
createObject("java", "org.apache.poi.hssf.usermodel.HSSFClientAnchor").init(),
loadPicture("asset/images/logo_e2eA.png", workSheet)
)/>


The code is similar to one of the java examples in the POI Quick Guide: adding images to a worksheet. Now what is confusing about the java example is the use of a method called loadPicture. I remember having this question myself. But could not recall the cause or solution.
So I searched my jar files for loadPicture but found nothing. Eventually I found a thread which explained that loadPicture is a private convenience method contained in one of the example classes. The example classes are located in the org.apache.poi.hssf.usermodel.examples.* package. I checked my jar files. Sure enough the examples package was missing. But even if it were present, the loadPicture method is private. Meaning it cannot be used outside its parent class anyway. At least not without modifying the source code. So that meant I needed to create a ColdFusion function that mimics the loadPicture method.

If you look at the java source code for loadPicture, it is very simple. It reads in an image file then adds the raw bytes of the image to a workbook. As you can see the code uses a FileInputStream object to read the image file. Then the bytes of image are copied into a ByteArrayOutputStream object. Finally, the image bytes are converted to an array and added to the workbook using the HSSFWorkbook.addPicture method.


private static int loadPicture( String path, HSSFWorkbook wb ) throws IOException
{
int pictureIndex;
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try
{
// read in the image file
fis = new FileInputStream( path);
bos = new ByteArrayOutputStream( );
int c;
// copy the image bytes into the ByteArrayOutputStream
while ( (c = fis.read()) != -1)
bos.write( c );

// add the image bytes to the workbook
pictureIndex = wb.addPicture( bos.toByteArray(), HSSFWorkbook.PICTURE_TYPE_PNG );
}
finally
{
if (fis != null)
fis.close();
if (bos != null)
bos.close();
}
return pictureIndex;
}

So I created a ColdFusion function to do the same thing. It is worth noting the java example assumes the image is a PNG. It should be adapted it to accept other image types as needed.


<cffunction name="loadPicture" access="public" returntype="numeric" output="false">
<cfargument name="path" type="string" required="true">
<cfargument name="workbook" type="any" required="true">
<cfset var Local = structNew()>

<cfscript>
Local.index = 0;
Local.errorMessage = "";

try {
Local.fis = createObject("java", "java.io.FileInputStream").init( arguments.path );
Local.bao = createObject("java", "java.io.ByteArrayOutputStream").init();

Local.c = Local.fis.read();
while ( Local.c neq -1 )
{
Local.bao.write( Local.c );
Local.c = Local.fis.read();
}

// Note: Assumes image is PNG format. Adapt if needed
Local.index = arguments.workbook.addPicture( Local.bao.toByteArray(),
arguments.workbook.PICTURE_TYPE_PNG
);
}
catch (Any e) {
Local.errorMessage = e.Message &" "& e.Detail;
}
// always close the input stream
if ( structKeyExists(Local, "fis") ) {
Local.fis.close();
}
</cfscript>

<!--- if an error occurred, rethrow it --->
<cfif len(Local.errorMessage)>
<cfthrow message="#Local.errorMessage#" type="UnableToLoadPicture">
</cfif>

<cfreturn Local.index >
</cffunction>



Let us try this again ..
Now after examining at the java code again, I realized there is a much simpler method. The bulk of the java code just reads in the bytes of a file. In ColdFusion 8 you could simply use the FileReadBinary() function to do this, or for MX7 use <cffile action="readBinary" ...>. It is possible they may do the same thing behind the scenes. But I like the simplicity of using a single function or tag. So the code for loadPicture was simplified to:

imageBytes = FileReadBinary( pathToImage );
pictureIndex = book.addPicture( imageBytes, book.PICTURE_TYPE_PNG );

Here is the complete example below. For convenience I threw together a function called getPOIPictureType. It maps a file extension like "jpg" or "png" to a POI picture type. It is not heavily tested, so no guarantees it is not missing file extensions. Use it at your own risk ;)

Add image to worksheet example
How to install POI on ColdFusion 8 using JavaLoader.cfc



<h1>Adding an image to a worksheet with POI </h1>
<cfscript>
// get a reference to the javaLoader stored in the server scope
javaLoader = server[application.myJavaLoaderKey];

pathToOutFile = "c:\excelFileWithImage.xls";
pathToImage = expandPath("c:\myImage.JPG");

//create a new workbook and worksheet
book = javaLoader.create( "org.apache.poi.hssf.usermodel.HSSFWorkbook").init( );
sheet = book.createSheet("Image Test");

// extract the image bytes
// FileReadBinary() is ColdFusion 8 specific. For MX 7 use CFFILE action="readBinary"
imageBytes = FileReadBinary( pathToImage );

// add the image bytes to the workbook
fileExtension = listLast( pathToImage, "." );
pictureType = getPOIPictureType( fileExtension, book );
pictureIndex = book.addPicture( imageBytes, pictureType );

// WARNING: This will clear out any existing shapes for the worksheet.
patriarch = sheet.createDrawingPatriarch() ;

// IMAGE1: add the picture at top left
anchor = javaLoader.create( "org.apache.poi.hssf.usermodel.HSSFClientAnchor");
picture = patriarch.createPicture( anchor.init() , pictureIndex );
// set anchor to actual width and height
preferredSize = picture.getPreferredSize();
picture.setAnchor( preferredSize );

// IMAGE2: add the picture at specific location. For details see:
// http://poi.apache.org/apidocs/org/apache/poi/hssf/usermodel/HSSFClientAnchor.html
anchor = anchor.init( 0, 0, 0, 255, 4, 5, 4, 7);
pictureIndex = book.addPicture( imageBytes, pictureType );
picture = patriarch.createPicture( anchor , pictureIndex );
preferredSize = picture.getPreferredSize();
picture.setAnchor( preferredSize );


//write the file to disk
outFile = createObject("java", "java.io.FileOutputStream").init( pathToOutFile );
book.write( outFile );
outFile.close();

WriteOutput("Done!");
</cfscript>

<!---
WARNING: File extension were not heavily tested
--->
<cffunction name="getPOIPictureType" access="public" returntype="numeric" output="false">
<cfargument name="fileExtension" type="string">
<cfargument name="workbook" type="any" required="true">

<cfset var type = "" >
<cfswitch expression="#arguments.fileExtension#">
<!--- PNG format --->
<cfcase value="PNG">
<cfset type = arguments.workbook.PICTURE_TYPE_PNG>
</cfcase>
<!--- JPEG format --->
<cfcase value="JPG,JPEG">
<cfset type = arguments.workbook.PICTURE_TYPE_JPEG>
</cfcase>
<!--- Device independant bitmap --->
<cfcase value="BMP,DIB">
<cfset type = arguments.workbook.PICTURE_TYPE_DIB>
</cfcase>
<!--- Mac PICT format --->
<cfcase value="EMF">
<cfset type = arguments.workbook.PICTURE_TYPE_EMF>
</cfcase>
<!--- Mac PICT format --->
<cfcase value="PICT,PCT">
<cfset type = arguments.workbook.PICTURE_TYPE_PICT>
</cfcase>
<!--- Windows Meta File --->
<cfcase value="WMF">
<cfset type = arguments.workbook.PICTURE_TYPE_WMF>
</cfcase>

<cfdefaultcase>
<cfthrow message="Invalid or unhandled file extension [#arguments.fileExtension#]">
</cfdefaultcase>
</cfswitch>

<cfreturn type>
</cffunction>

...Read More

Friday, August 1, 2008

How to install POI on ColdFusion 8 using JavaLoader.cfc


Update (02/07/2009):

The new POI 3.5 beta also supports ooxml files (xlsx,docx,...). For updated instructions see: How to install the POI 3.5 beta on ColdFusion 8 using JavaLoader.cfc

Update (01/15/2010): Changed server scoped lock to named lock in Application.cfc example


I decided to write up a quick set of instructions for using POI with ColdFusion 8 as a reference. If you are not familiar with the JavaLoader.cfc it is a great tool, written by Mark Mandel, that allows you to dynamically load jar files.


Instructions:

1. Download the latest version of POI from http://poi.apache.org/. Extract the jars into the desired directory.

    Example:
    I created a directory under the webroot named "poi" and placed the jar files there. You can use a different location. So long as ColdFusion has permission to access whatever directory you select. Please note the jar file names tend to be based on version and date. So your file names may differ.

    Jar Locations:
    c:\coldfusion8\wwwroot\poi\poi-3.1-FINAL-20080629.jar
    c:\coldfusion8\wwwroot\poi\poi-contrib-3.1-FINAL-20080629.jar
    c:\coldfusion8\wwwroot\poi\poi-scratchpad-3.1-FINAL-20080629.jar

2. Download and install the JavaLoader.cfc, available at javaloader.riaforge.org

    Example:
    I installed the JavaLoader under the webroot

    JavaLoader Location:
    C:\coldfusion8\wwwroot\javaloader\JavaLoader.cfc
    C:\coldfusion8\wwwroot\javaloader\JavaProxy.cfc
    C:\coldfusion8\wwwroot\javaloader\lib\*.*

3. Instantiate the JavaLoader

Usage
// get a reference to the javaLoader
<cfset javaLoader = server[application.myJavaLoaderKey]>

Application.cfc
<cfcomponent>

<cfset this.name = "POIExamples">
<cfset this.sessionManagement = true>
<cfset this.loginStorage = "session">

<cffunction name="onApplicationStart">
<!--- use a unique hard coded key to store the javaLoader in the server structure ---->
<!--- the xxxx is actually a hardcoded UUID value ---->
<cfset var myJavaLoaderKey = "xxxxxx-xxxxxxxx-xxxxxxxxxx-xxxxxxxxxxx_javaloader">
<cfset var jarPaths = arrayNew(1)>

<!--- if the javaLoader was not created yet --->
<cfif NOT structKeyExists(server, myJavaLoaderKey)>

<!--- these are absolute paths to the POI jar files --->
<cfset arrayAppend( jarPaths, expandPath("./poi-3.1-FINAL-20080629.jar")) >
<cfset arrayAppend( jarPaths, expandPath("./poi-contrib-3.1-FINAL-20080629.jar")) >
<cfset arrayAppend( jarPaths, expandPath("./poi-scratchpad-3.1-FINAL-20080629.jar")) >

<!--- re-verify it was not created yet --->
<cfif NOT structKeyExists(server, myJavaLoaderKey)>
<cflock name="#Hash(myJavaLoaderKey)#" type="exclusive" timeout="10">
<!--- create an instance of the JavaLoader and store it in the server scope --->
<cfset server[myJavaLoaderKey] = createObject("component", "javaloader.JavaLoader").init( jarPaths )>
</cflock>
</cfif>
</cfif>

<!--- store the key in the appliation scope for easy access by other pages --->
<cfset application.myJavaLoaderKey = myJavaLoaderKey>
</cffunction>

</cfcomponent>

...Read More

Friday, July 11, 2008

HTTP is a Built-In ColdFusion Scope?

While using cfhttp yesterday, I noticed something interesting.

I was running a small test page that made a cfhttp calls and then used cfdump to display the results to the screen. Though the script executed without error, the results seemed wrong... and yet very familiar. It took me a second before I realized why. The results looked exactly like a dump of the CGI scope. So I went back and checked my code and found that I had accidentally dumped HTTP instead of CFHTTP:


<cfdump var="#HTTP#">


Based on the results, HTTP seems to be a built-in scope. I was a little surprised because I could not recall reading anything about a scope by that name. Curious, I used a few of the underlying java methods to examine the HTTP and CGI objects more closely. Sure enough java reported the two objects were both instances of the class coldfusion.runtime.CgiScope and that the two were equal.


<cfoutput>
HTTP.equals(CGI) = #HTTP.equals(CGI)#<br>
HTTP.getClass() = #HTTP.getClass().getName()#<br>
CGI.getClass() = #CGI.getClass().getName()#
</cfoutput>



So apparently HTTP is another another way of referencing the CGI scope. Now while I strongly suspect that is _not_ new to ColdFusion, it was certainly news to me ;-)

...Read More

Friday, July 4, 2008

What Fonts are Available in myJVM?

A while back I was looking for a quick way to determine what fonts were available in my JVM. I came across the java GraphicsEnvironment class, which returns an array of font families available in the current graphics environment. So I threw together a quick loop, and presto .. I had sample images of the available fonts in my JVM.


<cfset environ = createObject("java", "java.awt.GraphicsEnvironment").getLocalGraphicsEnvironment()>
<cfset allFamilies = environ.getAvailableFontFamilyNames()>

<h3>Display Available Font Families</h3>

<table>
<cfoutput>
<cfset failedFonts = [] >
<cfloop array="#allFamilies#" index="family">
<cftry>
<cfset img = ImageNew("", 200, 35, "argb", "##dcdcdc")>
<cfset prop = { font=family, size="14", style="plain" }>
<cfset ImageSetAntiAliasing( img, "on" )>
<cfset ImageSetDrawingColor( img, "##000000" )>
<cfset ImageDrawText( img, family, 15, 15, prop)>
<cfset wasRendered = true>

<cfcatch>
<cfset arrayAppend( failedFonts, family )>
<cfset wasRendered = false>
</cfcatch>
</cftry>

<cfif wasRendered>
<tr><td><cfimage action="writeToBrowser" source="#img#"></td></tr>
</cfif>
</cfloop>
</cfoutput>
</table>
<br>

<h3>Unable to display fonts:</h3>
<cfdump var="#failedFonts#">




One thing that surprised me was that a few of the fonts could not be displayed. Namely the five (5) logical fonts and a few of the Lucida fonts. If you are not familiar with the term logical fonts, one of the old sun api's describes them as:


" .. not actual font libraries that are installed anywhere on your system. They are merely font-type names recognized by the Java runtime which must be mapped to some physical font that is installed on your system."


While I understand the difference between logical and physical fonts, I was still surprised logical font names were not accepted by the ImageDrawText function. When I created a java Font object directly, it worked fine. So I guess I was expecting ImageDrawText to behave the same way.

Using Java Font Object

<cfscript>
img = ImageNew("", 100, 50, "argb");
Color = createObject("java", "java.awt.Color");
Font = createObject("java", "java.awt.Font");
textFont = Font.init( "Dialog", Font.PLAIN, javacast("int", 12));
graphics = ImageGetBufferedImage( img ).getGraphics();
graphics.setColor( Color.BLACK );
graphics.setFont( textFont );
graphics.drawString( textFont.getFamily(), javacast("int", 10), javacast("int", 10) );
</cfscript>

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


As the documentation on ImageDrawText does not say much about the "font" attribute, that may be a misunderstanding on my part. If anyone can shed some light on this, I am eager to be enlightened ;-)

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep