« Enter to Win a Free iPod | Main | Site Statistics - An Informal Poll »

June 10, 2003

Using ColdFusion to Write Out Binary Data

I have been trying to get ColdFusion to write binary data (image bytes) out to my browser on and off for the last couple of weeks using several different techniques. Thanks to some internal help, this morning, I finally got it.

The idea is that I want to take a byte array and write it directly to the output stream between the server and client and have the image render properly in the browser. That means the data and the output stream can't be at all corrupted by whitespace, new lines or line returns like it can be if you are only sending back text. The following code illustrates how to get this to work:

<cffile action="readbinary" file="/home/cantrell/Pictures/Corrs2.jpg" variable="pic"/>

<cfscript>
    context = getPageContext();
    context.setFlushOutput(false);
    response = context.getResponse().getResponse();
    out = response.getOutputStream();
    response.setContentType("image/jpeg");
    response.setContentLength(arrayLen(pic));
    out.write(pic);
    out.flush();
    out.close();
</cfscript>

It wasn't the process of writing a byte array to the output stream that had me stumped for so long. My problem was that I wasn't getting my hands on the right output stream, or more specifically, the right response. Notice this line:

response = context.getResponse().getResponse();

I was only calling getResponse() once, which returned a response object with an output stream that would always corrupt binary data because it was always expecting character data. By calling getResponse() twice, I am able to get to the underlying output stream which I can use to write bytes with no problems.

So why do something like this? If all I wanted to do is essentially move a binary file from disk to the client, I would use the cfcontent tag, but what if the file doesn't exist on disk? What if the image is in the database, or you got the bytes from a URL? The only way (as far as I know) to send the bytes to a client without creating a temporary file is using the technique above.

I should point out that this is not an officially supported technique. Use it at your own risk, and encapsulate it well so that if it doesn't work in future versions of CFMX, you can fix it easily.

Posted by cantrell at June 10, 2003 11:47 AM

Comments

pretty cool. i wish this was out several years ago so we could have said "in YOUR face asp programmers" ;-) thanks, great tip.

Posted by: PaulH at June 10, 2003 12:17 PM

CFHTTP returns a byte array - it might be cool if it returned the inputstream so that you could write the content out as you receive it.

I had tried this before (with the two getResponse calls) but it didn't work within my Fusebox application.

Would Fusebox's use of <cfsavecontent/> prevent this technique from working properly?

Posted by: Andrew Crump at June 10, 2003 5:25 PM

Am I missing something here? To me your 12 lines of code can be done in one:

I guess if there's a requirement for some manipulation to take place between the file read and the data output, then yeah, but otherwise...?

Adam

Posted by: Adam Cameron at June 24, 2003 6:29 PM

Oops. This time I'll escape my < / > and you might see what I meant!

<cfcontent type="image/jpeg" file="#webDir#/home/cantrell/Pictures/Corrs2.jpg">

Adam

Posted by: Adam Cameron at June 24, 2003 6:32 PM

That will work if you want to server an image from disk, but what if you get the bytes of the image from a URL or from your database? In the example, I grab they bytes off of disk just for conveninece.

Christian

Posted by: Christian Cantrell at June 24, 2003 6:35 PM

We tried the simple solution, however since CFCONTENT can be restricted due to security concerns, then you can use the above as a work around as well.

Posted by: Adam at August 4, 2003 3:03 PM

This code doesn't seem to work in MX 6.1. Is anyone else having this problem, and do you have a solution for it?

Dave

Posted by: David at August 15, 2003 4:51 PM

Never mind, if you comment out the out.close line, it works. It also works on MX with this line commented out.

Posted by: David at August 18, 2003 1:53 PM

I'm slightly confused...what does this do when you store images in a DB that the following will not do?:

<cfcontent type="#getImage.ContentType#/#getImage.ContentSubType#">
<cfoutput>#ToString(GetImage.Store)#</cfoutput><cfabort>

Posted by: Tyler Silcox at September 5, 2003 12:39 PM

This is fantastic! I'm pumped to go try this with the Apache FOP classes. I was always stuck with taking a pdf that was dynamically generated out of the fop java class and storing it to disk, then using cfcontent to get it to stream to the browser. It always bugged me because the save to disk just seemed like a complete waste of time and resources. They have great servlet examples on the apache fop site but they always relied on the byte array. THANK YOU (in advance because I haven't tried this yet but I'm encouraged for the first time in a long time).

Posted by: Neil at March 30, 2004 5:47 PM

Can this be done with PDF and Word Docs?

Posted by: Larry Gutman at April 28, 2004 5:18 PM

I'm getting an error (java.lang.IllegalStateException
) when I try to use this script. This error is the result of using the getOutputStream method on the response structure because getWriter has already been used (I'm assuming by a subclass). How did everybody else avoid this error, and how do I get past it? Thanks, Ryan.

Posted by: Ryan at June 3, 2004 11:53 AM

Thanks. I have tried this and it works great.

I'm using it with cfhttp to get a byte array of a pdf then used the writeTo method to write to the out stream using this getResponse() twice.

Once again thanks a lot.

Posted by: Jorge Pereira at August 19, 2004 12:02 PM

I am pulling binary out of a DB so I am trying this:

context = getPageContext();
context.setFlushOutput(false);
response = context.getResponse().getResponse();
out = response.getOutputStream();
response.setContentType("#FileInformation.FileType#");
response.setContentLength(arrayLen(#FileInformation.FileContent#));
out.write(#FileInformation.FileContent#);
out.flush();
out.close();

#FileInformation.FileContent# is the field from the db. I figure since it is being pulled as binary already no need to read it or convert it right? Then why cant I get this to work? Any ideas? Any help would be greatly appreciated. Thanks.

Posted by: J.P. at October 4, 2004 7:44 PM

Never mind...the same problem as the first post on this page...it didnt work because it was in the Fusebox framework...Thanks!

Posted by: J.P. at October 4, 2004 8:31 PM

Works great, I use FOP to generate PDFs in Memory and serve them up.

As for the close statement, I imagine the compiled servlet (from the cfm in mx) would like to close the stream itself and when you close it first you mess things up for the remainder of the server (hence the bitching).

Posted by: Amir at October 14, 2004 4:50 AM

The timing is impecible that I just found this post. However, I've not had luck in getting to where I need to be. So far a CF test page I created will display a graphic file stored on a remote server properly. The problem is simply that is all I can see on the page. No text, no other HTML markup, etc.

I need to have a standard HTML page appear with an image I retrieved from a database or a remote folder share. Any ideas? I've erased the out.close(); call as he mentions.

Please help! =)

Posted by: J.S. at October 15, 2004 3:24 AM

Well, your post just helped me figure out a long time nagging problem. I have been working on a CF wrapper for JFreeChart to generate on-the-fly charts, and have been stumped with corrupted output, until I saw your fix about the double getResponse() method calls. With that change, all works great! For those who don't understand WHY you might need to do this, here is sampe code that creates a bar chart on-the-fly (no caching on the file system) and outputs directly to the browser a JPEG image:

barDataset = CreateObject("java", "org.jfree.data.category.DefaultCategoryDataset");

Randomize(numberformat(timeformat(now(),"ss")));
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2001", "Q1");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2001", "Q2");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2001", "Q3");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2001", "Q4");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2002", "Q1");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2002", "Q2");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2002", "Q3");
rval = int(randrange(1,500));
barDataset.setValue(numberformat(rval), "2002", "Q4");

chart = CreateObject("java", "org.jfree.chart.JFreeChart");
chartfactory = CreateObject("java", "org.jfree.chart.ChartFactory");
chartorient = CreateObject("java", "org.jfree.chart.plot.PlotOrientation");
chartutil = CreateObject("java", "org.jfree.chart.ChartUtilities");
chart = chartfactory.createBarChart3D
("Sample Bar Chart",
"Category",
"Value",
barDataset,
chartorient.VERTICAL,
true,
true,
true);

response = getPageContext().getResponse().getResponse();
response.setContentType("image/jpeg");
chartutil.writeChartAsJPEG(response.getOutputStream(),
100, // jpeg quality 0-100
chart, // chart object
500, // width
300); // height

response.flush();
response.close();

If anybody else needs help with the above code, email me.

cheers

Posted by: Andrew S at December 16, 2004 5:35 PM

Andrew, what's your e-mail address? I've got some questions about jFreeChart. That code works perfecly.

Posted by: Adrian J. Moreno at December 29, 2004 3:20 PM

This looks great, but I can't get it to work. I'm running a straight install of CF MX 6.1 developer version on XPpro. I'm using ABCpdf, a com object which converts web pages to pdf's. It seems to work well.
Here's my code:

<cfset theDoc.Save("C:\Inetpub\wwwroot\howard\sbc\html2pdfr\abcpdf\pagedhtml.pdf")>
<cffile action="readbinary" file="C:\Inetpub\wwwroot\howard\sbc\html2pdfr\abcpdf\pagedhtml.pdf" variable="theData"/>
<!---<cfset theData = theDoc.GetData()>--->
<cfscript>
context = getPageContext();
context.setFlushOutput(false);
response = context.getResponse().getResponse();
out = response.getOutputStream();
response.setContentType("application/x-pdf");
response.setContentLength(arrayLen(theData));
out.write(theData);
out.flush();
out.close();
</cfscript>

Everything preceding the above code is communicating with the com object to create the pdf. There is nothing after the above code.
I've tried populating theData from the com object (the commented out line), and from the pdf produced by the com object (the not-commented out line). I've tried removing the x- from the mime type. I've tried passing the call to the com object directly into write(). I've tried commenting out the last two lines, singly and in combination. No matter what, I get a screen full of what I assume is the binary treated as text.
The pdf file opens fine in acrobat, but something is obviously going wrong between reading it in, and writing it out.
One thing: the browser sees the file as having a .cfm extension? Does that matter? If so, how do I fix it?
Any pointers would be much appreciated.

Posted by: Sid Maskit at January 6, 2005 12:05 AM

Oops, my bad. It works. Apparently while trying something earlier, I trashed IE. It looked like it was refreshing the page, but just kept loading the same old mess, even with refresh every time selected. So it looked like nothing worked. Nothing like closing and reopening IE as part of one's testing process.

Thanks for a very cool solution!

Posted by: Sid Maskit at January 6, 2005 2:38 AM

This is awesome, Christian. Thank you very much.

I have a follow-up question. In your example, you knew the mime-type of the binary data you were going to display. If you didn't know, is there a way to determine this from the binary data itself? I have a database storing documents in different formats - could be ppt, pdf, doc, etc. When I pull back the binary data, is there any way to dynamically determine the mimetype? Thanks in advance to anyone who can help.

Posted by: Jeff at January 7, 2005 1:24 PM

Thx for the code. Now no more writing to temporary file.

Posted by: Hairie at January 23, 2005 1:12 AM

Can someone please show an example of querying the database and getting the filed out of the database in order to display the image? I cannot seem to get it to work properly. My filed uses an Access database with field type of OLE Object (stores images in database). Your help would be greatly appreciated!

Posted by: Christopher Rentz at June 7, 2005 12:13 PM

I know this is a really old post, but the following may be useful to others (especially on CFMX 7):

I've found that you need to reset the response before closing the OutputStream otherwise you leave the thread in an unusable state for future requests (which leads to the "The server encountered an internal error and was unable to complete your request." error from CF and, more specifically, a "Response has already been committed" error from JRun).

With that, this code cleans up the response correctly:

out.write(byteArray);
out.flush();
response.reset();
out.close();

Posted by: Ryan Emerle at February 1, 2006 7:02 AM

Christian:

I used your code sample in a cfm file that is reading an oracle blob field. the prolbem is when i try to read a file larger than 62K i get an error that the file has been corrupted. do you know a way around this or perhaps is there a cf server setting i need to change? i am running cf 7. any help would be appriciated.

thanks.

Posted by: chris at February 2, 2006 1:53 PM

Found the perfect use for this, and experimenting now.

I've been trying to figure out the equivalent of PHP FLV (flash) streaming but with coldfusion, this seems to be the trick.

Take a look at the PHP code from:
http://www.flashcomguru.com/index.cfm/2005/11/2/Streaming-flv-video-via-PHP-take-two

I will post my code once I get some more testing done.

Steve.

Posted by: Steve Savage at March 24, 2006 8:11 PM

Got the code working, you can now stream .FLV files using coldfusion http://www.realitystorm.com/experiments/flash/streamingFLV/index.cfm

Posted by: Steve Savage at April 1, 2006 6:54 PM