Reading Images With Fallback Approaches In ColdFusion
One of the things that I love about ColdFusion is how freaking easy it is to read, write, and manipulate images without any 3rd party software. You get all that magic in ColdFusion, right out of the box. Sometimes, however, your users upload an image that ColdFusion doesn't really like, and you have to start taking a different approach. Or rather, you have to start taking a variety of different approaches. You could go with something like ImageMagick. But, as much as possible, I like to stick to the ColdFusion internals - less can go wrong, less to maintain.
When reading in a ColdFusion image, I try to go with the basics first; and then, if necessary, fallback to using more complex approaches. In the following code, I'm using a combination of solutions that I found on the web. From Zac Spitzer, I borrowed reading images as binary. From Joey Krabacher, I borrowed reading images using the Java Advanced Imaging (JAI) library. And, as far as the JAI goes, I don't really understand what's going on there - I just copied what Joey had.
In the following code, I use each technique for demonstration purposes. In reality, however, I would only use the fallbacks if the previous attempt(s) failed to read-in the image.
<cfscript>
// Full path to image files.
path = expandPath( "./images/monkey.png" );
//path = expandPath( "./images/wrong-extension.jpg" );
//path = expandPath( "./images/cmyk.jpg" );
// ------------------------------------------------------ //
// ------------------------------------------------------ //
try {
image = imageRead( path );
writeOutput( "PASS: imageRead()<br />" );
} catch ( any error ) {
writeOutput( "FAIL: imageRead()<br />" );
writeOutput( "--- #error.message#<br />" );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
try {
image = imageNew( path );
writeOutput( "PASS: imageNew( path )<br />" );
} catch ( any error ) {
writeOutput( "FAIL: imageNew( path )<br />" );
writeOutput( "--- #error.message#<br />" );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
try {
image = imageNew( fileReadBinary( path ) );
writeOutput( "PASS: imageNew( binary )<br />" );
} catch ( any error ) {
writeOutput( "FAIL: imageNew( binary )<br />" );
writeOutput( "--- #error.message#<br />" );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
try {
imageFile = createObject( "java", "java.io.File" ).init(
javaCast( "string", path )
);
fileSeekableStream = createObject( "java", "com.sun.media.jai.codec.FileSeekableStream" ).init( imageFile );
parameterBlock = createObject( "java", "java.awt.image.renderable.ParameterBlock" ).init();
parameterBlock.add( fileSeekableStream );
// Use the Java Advanced Imaging library to read in the JPEG
// file (will throw error if NOT jpeg) as a buffered image.
// This will properly handle the different EXIF data types.
bufferedImage = createObject( "java", "javax.media.jai.JAI" )
.create(
javaCast( "string", "jpeg" ),
parameterBlock
)
.getAsBufferedImage()
;
image = imageNew( bufferedImage );
// imageNegative( image ); // CAN HELP A LITTLE BIT.
writeOutput( "PASS: Java Advanced Imaging.<br />" );
} catch ( any error ) {
writeOutput( "FAIL: Java Advanced Imaging.<br />" );
writeOutput( "--- #error.message#<br />" );
} finally {
// Clean up the file stream, pass OR fail.
fileSeekableStream.close();
}
</cfscript>
<!--- If the file managed to be processed, then output it. --->
<cfif ! isNull( image )>
<p>
<cfimage
action="writeToBrowser"
source="#image#"
/>
</p>
</cfif>
I'm using both imageRead() and imageNew() in this exploration. Both of these can take a file path or URL as the source of the image input. I'm using both functions to demonstrate that imageNew() can take a larger variety of arguments.
There's not too much explanation to offer here. And, as far as the Java Advanced Imaging, I don't want to offer up any information that's not accurate. I will say that if you try to read in a PNG that is labelled (incorrectly) as a JPG, the JAI will raise this exception:
Not a JPEG file: starts with 0x89 0x50
Anyway, just a quick post for Friday afternoon. Have a great weekend!
Want to use code from this post? Check out the license.
Reader Comments
Thanks Ben, great info as usual. I've been doing more work with images lately myself and found a lot of photos that people were uploading were rotated in odd ways and that the EXIF information would often contain rotation information to "correct" them before processing. I've also had some images that just won't load or had color issues so I will try some of these techniques on them as well. Thanks for sharing.
An incompatible palette is one issue, another is performance and compression. We were experiencing the CPU going to 100% when processing JPG images from digital cameras. For optimizing large images, we switched to CFX_OpenImage (C++ tag that uses GraphicsMagick). Thumbnail generation was much faster and the filesizes were smaller.
http://www.kolumbus.fi/jukka.manner/cfx_openimage/
Check out the documentation. There's a lot of features crammed into the library and many sample scripts.
[NOTE: I use NoScript 2.6.5.8 for Firefox and it alerted me of an XSS attempted when posting this response. It may be a false positive, but I wanted to report it. I switched to Google Chrome so I could post this.]
@Justin,
From my own personal experience, I can say that rotation is super annoying :D I'll take photos with my phone. They look fine. I'll email them to myself. They look fine... then I open them in a different email and BLAM, they are sideways :( What?! That's good to know that the EXIF data can contain that information.
@James,
Yeah, we are toying around with trying to use something like ImageMagick on the command line to get some better performance and file size. That's one thing that CF seems bad about is filesize. Also, once we are on the command line, there's all kinds of opportunities for "smushing" the images (or whatever they call it when you losslessly remove data from image).
Also, thanks for the XSS tip. I don't see anything in the page that is the obvious cause. I'll try to install that as well and see what I can find. Thanks!!!
@James,
NoScript is pretty cool. I see a number of scripts loading, but it all looks like the GitHub / Video / Facebook / Google Analytics / AdSense stuff. It's possible that one of the comments in the sidebar had something funky in it at the time. I'll try to track it down. Again, thanks for the heads-up.
Hi Ben,
With regard to CMYK images, it seems CF9 supports some of the operations in cfimage.
Check this page:
http://help.adobe.com/en_US/ColdFusion/9.0/CFMLRef/WSc3ff6d0ea77859461172e0811cbec22c24-7945.html
The quote from the docs:
CMYK support
The cfimage tag supports reading and writing CMYK images, but does not support actions that require converting the images. For example, you can use CMYK images with the read, write, writeToBrowser, resize, rotate, and info actions. You cannot use CMYK images with the convert, captcha, and border actions. The same rule applies to image functions. For example, the ImageNew, ImageRead, and ImageWrite functions support CMYK images, but the ImageAddBorder function does not.
What are your thoughts on this? Seems to have slipped past a few people.
@Michael,
Hmm, very interesting. In my current app, we really only resize images. I don't think we do anything that requires an actual type conversion. That said, I've noticed the error in Reading images with CMYK; although, even after reading them with the JAI toolkit, the colors are definitely messed up.