I'm working on a project where I'm trying to create thumbnails for documents the user uploads. Since CF8 has introduced the <cfpdf /> tag, I thought it would be pretty straightforward to turn page 1 of a PDF into a thumbnail image—turns out I was wrong.
- The <cfpdf /> only allows you to create the images based on a scaled percentage. This is pretty pointless if you ask me—since I suspect different PDFs might generate different image sizes. I wanted my thumbnails scaled to fit a specific dimension.
- You can't specify the exact name of the file to be generated. You specify a "prefix" which is attached to each image and then it automatically appends the string "_page_N" (where "N" is the current page number.) This perfectly logically when you're exporting multiple pages, but in my case I only want the first page and I need to specify the exact file name.
- The <cfpdf /> tag holds a lock on the images created. I believe this because it never closes the java.io.File objects it creates for the new images. Since CF holds a lock on the file, this prevents me from being able to rename or delete the thumbnail image until the lock is released—whenever that might happen.
I was able to work around issues #1 & 2, but issue #3 was the one causing me the real issues. I mean I could write the files to a temp file and clean them up later, but I already felt like I was hacking too many things to get this to all work.
So, I thought I'd play around with the native Java objects to see if it wouldn't be easy to just write a ColdFusion UDF that would allow me to do exactly what I wanted with the image. It turns out it's pretty straightforward.
The power all lies in the PdfDecoder class. There's a lot a methods in that class (including some text extracting methods which I didn't get around to playing with.) Converting a page to an BufferedImage object is as easy as invoking either the getPageAsImage() or getPageAsTransparentImage() methods.
Since the PdfDecoder class returns a BufferedImage object, that made it really easy to manipulate further with CF8's built-in image handling functions. You just need to pass the BufferedImage to the imageNew() function.
So, within just a few minutes I was able to put together this little UDF:
<cffunction name="convertPdfToImage" access="public" returntype="string" output="false" hint="Attempts to create a thumbnail from a file.">
<!---// define the arguments //--->
<cfargument name="source" type="string" required="true" hint="Full filepath to source file" />
<cfargument name="destination" type="string" required="true" hint="Destination folder for image" />
<cfargument name="page" type="numeric" default="1" hint="Page number in the PDF to convert to an image" />
<cfargument name="type" type="string" default="png" hint="Type of image to create (i.e. PNG, JPG, etc)" />
<cfargument name="width" type="numeric" default="-1" hint="Width of image (specifying both a width and height for the image to scale-to-fit, otherwise the image is fullsize)" />
<cfargument name="height" type="numeric" default="-1" hint="Height of image (specifying both a width and height for the image to scale-to-fit, otherwise the image is fullsize)" />
<cfargument name="highResolution" type="boolean" default="true" hint="Indicates whether or not to use high quality rendering" />
<cfargument name="transparent" type="boolean" default="false" hint="Indicates whether or not the page should be retrieved as a transparent image" />
<cfargument name="interpolation" type="string" default="highestQuality" hint="Interpolation method used for resampling" />
<cfargument name="quality" type="numeric" default="0.8" hint="Defines the JPEG quality used to encode the image" />
<!---// declare variables //--->
<cfset var pdfDecode = "" />
<cfset var pdfImage = "" />
<cfset var imageToSave = "" />
<cfset var newFile = arguments.destination & reReplaceNoCase(getFileFromPath(arguments.source), "\.pdf$", "." & arguments.type) />
pdfDecode = createObject("java", "org.jpedal.PdfDecoder").init(javaCast("boolean", true));
// the version of PdfDecoder in cf8 supports showing annotations
if( structKeyExists(pdfDecode, "showAnnotations") ) pdfDecode.showAnnotations = javaCast("boolean", false);
// if a password has been supplied, use the password
if( structKeyExists(arguments, "password") )
imageToSave = createObject("java", "java.awt.image.BufferedImage");
// if creating a transparent image, do so now
imageToSave = pdfDecode.getPageAsTransparentImage(javaCast("int", page));
// otherwise, get the standard image
imageToSave = pdfDecode.getPageAsImage(javaCast("int", page));
// close the PDF file
* go back to native CF functions
// create a native CF image from the BufferedImage
pdfImage = imageNew(imageToSave);
// if we've specified a width/height, scale to those dimensions
if( (arguments.width gt 0) and (arguments.height gt 0) )
imageScaleToFit(pdfImage, width, height, interpolation);
// write the image to disk
imageWrite(pdfImage, newFile, arguments.quality);
<!---// if an error has occured, just return an empty string to indicate we couldn't process the PDF //--->
<cfreturn "" />
<cfreturn newFile />
I've updated the code to work with ColdFusion 9. CF9 does not support the "showAnnotations" option, so we just need to exclude it.
The file will be saved with the same name as the original PDF, but it will have whatever you specified for the file "type" as the extension. For example:
<cfset imgPath = convertPdfToImage(
expandPath(".") & "\attachments\" & "my.pdf"
, expandPath(".") & "\attachments\thumbnails\"
<img src="./attachments/thumbnails/#getFileFromPath(imgPath)#" />
<h1>Could not process PDF</h1>
The code above would create a file in the "thumbnails" folder titled "my.png" that is scaled to fit the dimensions 64 x 64. The UDF returns the path to the file it wrote, unless it was not able to write an image from the PDF in which case it returns an empty string.
There's actually a lot of interesting looking things in the PdfDecoder class. When I have more time, I'll have to go back and play with some of the other methods.