Josh K. sent in an interesting problem today. He was generating 10 PDFs in a request, all running via cfthread, and he was trying to create one main PDF by merging the 10 he created. This turned out to be a bit difficult at first, but here is the code I ended up with that worked. Let's break it down line by line.
First, let's create a list to store the names of our threads. Because our final goal is to merge the PDFs, we know that at some point we have to join the threads. To join threads, you have to know their names. So our list variable will remember each thread name.
<cfset threadList = "">
Now let's loop. I set my loop to 5 instead of 10 just to make things a bit quicker:
<!--- loop pages and create cfdocuments --->
<cfloop index="x" from="1" to="5">
<cfset name = "thread_number_#x#">
<cfset threadList = listAppend(threadList,name)>
For each loop iteration, we create a name for the thread and append it to the list. Now for the actual thread.
<cfthread name="#name#">
<cfdocument name="thread.pdf" format="pdf">
<!--- output data --->
<cfoutput>#randRange(1,10000)#pdf data boo yah #thread.name#</cfoutput>
</cfdocument>
</cfthread>
You can see our dynamic thread name being used there. The cfdocument tag creates the PDF and stores it in a variable called thread.pdf. Normally variables you create in a thread exist in a local scope not available outside the thread. The Thread scope doesn't have this problem. Later on when I join my threads I can reuse this data. By naming my PDF variable thread.pdf, I'm simply storing the data in the thread scope ColdFusion created for me.
Now for the next block:
</cfloop>
Yeah, that ends the loop. Real complex block. ;)
Ok, so now that we've created our threads that generate PDFs, we have to wait for them to complete:
<cfthread action="join" name="#threadlist#" />
That's it. Nice and simple. While my 5 threads operate in parallel, this tag will force ColdFusion to wait for them to finish. Now for the merge:
<cfpdf action="merge" name="final">
<cfloop list="#threadlist#" index="i">
<cfset tempCopy = cfthread[i].pdf>
<cfpdfparam source="tempCopy">
</cfloop>
</cfpdf>
<cfheader name="Content-Disposition" value="inline; filename=test.pdf">
<cfcontent type="application/pdf" variable="#tobinary(final)#" reset="No" >
I begin with a cfpdf tag, merge action. This will perform the actual merge. The tag allows you to use the child tag, cfpdfparam, to specify PDFs to merge. You can use either a file path or the name of a PDF variable. I tried specifiying source="cfthread[i].pdf", but this confused the tag. It thought it was a file path. The tempCopy variable simply copies out the data and makes cfpdfparam nice and happy. (Let me state - I really dislike it when ColdFusion uses the names of variables as arguments. cfoutput query="name" is a good example of this. I really wish it would allow for cfoutput query="#qry#". This is a perfect example. Adobe had to build code to say - is this string a path or a variable? It would have been far easier to say, if a string, assume a path, otherwise assume it's binary data.)
One done with the merge, a quick cfheader/cfcontent combo will actually serve the PDF up to the user.
As always, hope this is helpful! Complete source code is below.
<cfset threadList = "">
<!--- loop pages and create cfdocuments --->
<cfloop index="x" from="1" to="5">
<cfset name = "thread_number_#x#">
<cfset threadList = listAppend(threadList,name)>
<cfthread name="#name#" >
<cfdocument name="thread.pdf" format="pdf">
<!--- output data --->
<cfoutput>#randRange(1,10000)#pdf data boo yah #thread.name#</cfoutput>
</cfdocument>
</cfthread>
</cfloop>
<cfthread action="join" name="#threadlist#" />
<cfpdf action="merge" name="final">
<cfloop list="#threadlist#" index="i">
<cfset tempCopy = cfthread[i].pdf>
<cfpdfparam source="tempCopy">
</cfloop>
</cfpdf>
<cfheader name="Content-Disposition" value="inline; filename=test.pdf">
<cfcontent type="application/pdf" variable="#tobinary(final)#" reset="No">
Archived Comments
I was surprised at how close I was to getting this right, but their were a few hiccups in it. This will come in handy when having to generate large pdf documents. Thanks again Ray for some of the pointers.
excellent walk through, thank you for outlining this, it really helped me correct a problem where we need to dynamically generate a pdf and merge it with an existing pdf on the server.
i have 1 question though, have u seen any performance issues with the merge functionality slowing down the server? just curious.
No, but I have not done anything intensive with it. The closest "intense" PDF thing I've done is generating PDFs at ColdFusionCookBook, and I ensured I cached that as much as possible so I rarely actually ever generated it.
Ray,
How can i modify this to use for a cfpdfform tag ? I have a bunch of pdf's with form in side then and based on condition i have to fill those forms[pdf] and merge all of them together.
Well, it would be the same principal. Put the code in the <cfthread> block that you want threaded. Have you tried writing it yourself yet?
@ Ray,
Yeah, I tried the code writing in myself. But am facing a weird problem. I was trying to place the 'populated' pdfform inside the document tag to generate a 'merged' pdf.this whole thing is threaded.
I followed the help docs and placed the documentsection also at the same level as pdfform. But the resultant pdf is empty!!
<cfdocument format="pdf">
<cfpdfform source="mypdf.pdf" action="populate">
<cfpdfsubform name="myform">
<cfpdfformparam name="feild1" value="value1">
<cfpdfformparam name="field2" value="value2">
</cfpdfsubform>
</cfpdfform>
<cfdocumentsection/>
</cfdocument>
Just beating the head. I think I may be doing something basic wrong. But as of now.. I'm in the darkness still.
cfdocument, w/o a name, tries to render to the screen. If you want to _save_ the PDF, you need to add the name attribute to cfdocument, take that result and save it to the file system.
@Ray,
I did that. But all i see is a empty pdf. Just for me the driving Question is how do I save a populated pdf form into a variable, so that i can use it later.
Also Ray,
Is there a way to find what is the 'form name' present inside the interactive pdf? I tried DDX DocumentInformation Tag and found it to be a 'AcroForm' formtype, but could not see any promising result info about form name. Any thoughts?
Well to save it you do what I said, use name="...", which stores the binary into a variable, and then fileWrite it.
As to your part 2 - check out the docs on cfpdfform. It may be your answer. I've not played with that so I can't help you there.
Thanks Ray. :)
Ray,
I tried this code sample and it gives me an error.
Element PDF is undefined in a Java object of type class coldfusion.thread.ThreadScope.
on line <cfset tempCopy = cfthread[i].pdf>
Any idea?
Thanks,
It means that the variable, PDF, wasn't created inside the thread. Do a cfdump on the cfthread scope to see what is there.
Thanks, there was a logical error in my code.
Ray...have you ever done something like this with cfimage? I have a client who is a photographer and I am running into performance issues. I have never used cfthread before so I thought it might help. I would appreciate your thoughts. Here is my code...
<!--- Read in the Stark Images watermark. --->
<cfset objWatermark = ImageNew( "./images/logoMain.png" ) />
<!---Resize watermark image--->
<cfimage action = "resize" height = "75%" source = "#objWatermark#" width = "75%" overwrite = "no" name = "objWatermarkresize">
<!---Rotate watermark--->
<cfimage action = "rotate" angle = "-35" source="#objWatermarkresize#" overwrite="no" name="objWatermarkrotate">
<cfoutput query="rsProofs" startrow="#StartRow_rsProofs#" maxrows="#MaxRows_rsProofs#">
<!--- Read in the original image. --->
<cfset objImage = ImageRead( "./proof/#rsProofs.FILENAME#" ) />
<!---Resize main image--->
<cfimage action = "resize" height = "10%" source = "#objImage#" width = "10%" overwrite = "no" name = "objImageresize">
<!---
Turn on antialiasing on the existing image
for the pasting to render nicely.
--->
<cfset ImageSetAntialiasing( objImageresize, "on" ) />
<!---
When we paste the watermark onto the photo, we don't
want it to be fully visible. Therefore, let's set the
drawing transparency to 50% before we paste.
--->
<cfset ImageSetDrawingTransparency( objImageresize, 60 ) />
<!---
Paste the watermark on to the image. We are going
to paste this into the bottom, right corner.
--->
<cfset ImagePaste( objImageresize, objWatermarkrotate, 30, 0 ) />
<!--- Write it to the browser. --->
<p><cfimage action="writetobrowser" source="#objImageresize#" quality="0.10" /></p>
<hr />
</cfoutput>
<a href="<cfoutput>#CurrentPage#?PageNum_rsProofs=#Min(IncrementValue(PageNum_rsProofs),TotalPages_rsProofs)##QueryString_rsProofs#</cfoutput>">Next</a>
Sure, it could help. Also, if you are shrinking/rotating that watermark all the time, I'd consider just saving the shrunk/rotated one to the file system and just using that.
I know this is an old post - but this doesn't work with images in the document...
The text changes for each PDF enerated and merged - but if you add an image(s) -
Such as cycling thru 5 images, o 1 each for the PDF - the image never changes on the resulting merged PDF. - if you do it outside the thread - it behaves as you'd expect...
Thanks. I haven't used CF's PDF stuff in nearly a decade.