The scenario addressed in this blog post is one where you have multiple PDF documents that you would like to combine into a single PDF document. This is not to be confused with the script step to append to an existing PDF.
This post examines how to take two existing PDFs and combine them instead of printing a layout or report and appending it to an existing file.
This example takes multiple PDFs stored in container fields, combines them into a single document and then stores the resulting file in another container field.
I believe this is a good example, in part because it combines several techniques, each of which has value. Some of the techniques used in this example:
- Perform Script on Server
- Using server-side plugins
- Pass multiple script parameters
- Performing command line scripts on a server
- Integrating with a 3rd party server-side program
- Encode/decode Base64 to create text files on the fly
- Multi-use safe script
By performing this script on the server, there is no need for third party plugins to be installed on the client, which also makes it compatible across the platform and supports FileMaker Go and WebDirect. It also does not matter if you have external storage enabled for container fields, since FileMaker takes care of that part for you.
Another benefit is the savings in networks latency if this action were to be performed on the client. Normally, you would need to download all PDF files to the client via container fields, export them somewhere where they can be manipulated and then combined and finally re-uploaded the result to another container field. Performing the script on the server avoids hair-pinning and doubling the amount of network traffic required.
Now let’s get to building this thing! Can we build it?
Yes, we can!
- First, this technique uses the Perform Script on Server script step (a.k.a. PSOS) so we already assume that your environment includes FileMaker Server.
- Next, we will be using the now open source Goya BaseElements (thanks Goya!) plugin installed on the server. This means that once we are done, clients won’t need to have the plugin installed, but for development, we will also have it installed on FileMaker Pro. Also recommended is the use of FileMaker Pro Advanced for development, but this is not a requirement.
- Finally, we will be integrating with a freely available and popular server-side program for manipulating PDFs, PDFtk server. This version provides a command line interface which is fine for our server-side implementation. PDFtk is also capable of doing much more than combining PDFs, but for this example, that will be our end result.
PDFtk also supports handling different size pages of PDFs and combines them as a single document.
Also of note, this example uses FileMaker Server installed on Windows Server. All the pieces involved will also work on OS X, but the shell scripting would be different.
For the script that runs on the server, exporting files to the Documents folder that resides in the FileMaker Server Data folder is the safe place to put things and avoid potential permissions issues, so we will use that. To avoid multi-user conflicts that will potentially use the same file names to when performing the scripts, we can create a unique folder on the server that represents the user.
Once that is done, we can work in that directory until done and then clean up after ourselves by removing the folder at the end of the script. The client’s persistent ID can be used as an identifier for each users device that is connected to the server.
Since the script running on the server needs to know about the current context we are working in, we need to pass it a couple of script parameters. We pass in the Persistent ID of the computer or device where the script is running, and the current ID of the parent record we are on.
Note: there are several ways to pass multiple script parameters to a subscript. At Soliant, we have several custom functions to aid in making this easier, but you may substitute whatever method you prefer.
The objective is the same, to hand off multiple parameters to our script running on the server. Once we have those, we can unpack them in our server-side script and use them.
In our example, we use a custom function to handle quoting and to make it easier to write. It ends up looking like this:
Perform Script On Server [ Wait for Completion ; "server cat ( persistentID ; appID )" ; Parameter: #t ( Get ( PersistentID ) ) & #t ( CON__Contracts::ID ) ]
Working Directory in a Multi-User Environment
FileMaker Server can write to the Documents directory in the FileMaker Server folder on the server. By creating a separate folder for each user, using their Persistent ID, each user’s folder will only contain their documents.
To create a folder in the Documents folder, we can use some standard functions to calculate the correct path:
Right ( Get ( DocumentsPath ) ; Length ( Get ( DocumentsPath ) ) - 1 ) & $_persistentID
Remember that we passed in the Persistent ID by using “Get ( PersistentID )” from the client. We use it here to get the proper path that we want to create in the Documents folder on the server. If the preceding line is set to a variable named “$this.docspath” then we can set another variable, named “$cmd” to the command line used to create that folder on the server.
"cmd.exe /c mkdir " & Quote ( $this.docspath )
Now we can use the function “BE_ExecuteSystemCommand” from the BaseElements plugin to run this command on the server like so:
BE_ExecuteSystemCommand ( $cmd ; "-1" )
The “-1” parameter tells the script to “wait forever” for the system script to complete. Once we have our working directory, we similarly create a sub folder called “output” that will eventually hold the combined PDF.
Eventually we will output a batch file (or .bat file) to concatenate (cat) all PDFs in our working directory into a single file. That command line looks like the following and references the default installed location of PDFtk server like this:
"C:Program Files (x86)PDFtk Serverbinpdftk.exe" *.pdf cat output output/combined.pdf
This command line will run the application specified, telling it to match anything ending with “.pdf” and concatenate it to a file named “combined.pdf” in a relative folder called “output” located in our working directory.
Since this file is generic enough, we can create batch file, named “cat.bat” and store it in a container field> We will show a technique to create simple text files like this in container fields on the fly.
Base64 Encode and Decode
It is possible to populate a container field with a text file that contains any text we want using the Base64 functions. This technique requires FileMaker (Pro or Server) 14 or higher. With the “cat.bat” content set in a text field “cat_bat”, we can use the Set Field script step to set the container field like so:
Base64Decode ( Base64Encode ( Resources::cat_bat ) ; "cat.bat" )
Since the Export Field Contents script step is not compatible with server-side scripts, we can again lean on the Base Elements plugin by using the BE_ExportFieldContents function to get our file to the server.
With all this in place, we are ready to loop through all the PDFs we want to combine and save them to the server by using the BE_ExportFieldContents function.
Once that is done, it is time to call our cat.bat script using the following command:
"cmd.exe /c cd " & Quote ( $this.docspath ) & " & " & Quote ( "cat.bat" )
When we execute the batch file, again with the BE_ExecuteSystemCommand function, all the PDFs we exported to our working directory are combined. I find it best to add a slight pause at this point to make sure the script completes, even though we specify (with “-1”) to wait to completion for the command to run.
Import the File
Once again, the Base Elements plugin allows us to overcome a server side scripting limitation by importing a file into a container field with the function, BE_ImportFile. Our complete file will be in the “/output/combined.pdf” folder relative to our working folder.
Clean Up Time
We wouldn’t want to arbitrarily leave lots of PDF files littering our server, especially since this is not consider temporary space that regularly gets emptied. Fortunately this is fairly easy to do with BE_ExecuteSystemCommand. This command looks like this:
"cmd.exe /c rmdir /s /q " & Quote ( $this.docspath )
This command tells the system to remove the specified directory, along with all files and sub-directories container within.
Now we need a script that will run on the client, sets up the variables to pass as parameters, and call the Perform Script on Server script step. You should enable the option to “wait for completion” when using this script step, so the resulting PDF will be available in our client when the server is done combining and saving it in a container field.
In the client’s user interface, the button to run the script calls the “parent” script, which in turn runs the PSOS script step.
Ensure Interactive Content
This optional last step is to ensure that the resulting PDF is available to be displayed interactively. Normally, you would be required to have inserted the file in an acceptable way for PDFs to use this feature, but if we use our Base64 functions to encode and decode, the file gets saved in the field correctly. Use the Set Filed script step to set the container field like this:
Base64Decode ( Base64Encode ( CON__Contracts::pdf_combined_r ) ; GetAsText (CON__Contracts::pdf_combined_r ))
By using the GetAsText function, we tell the resulting container field content what the file name is, dynamically based on whatever it was to begin with.
We hope this example has demonstrated several takeaway techniques that can be used independently. Taken as a whole, it also provides a free and easy way to address a common request. Even if you do not have the specific need to combine PDFs, the techniques listed at the top of this post can be cherry picked as needed to solve your own project requirements as needed.
Download the Sample File
After installing both PDFtk and the BaseElements plugin on the server, you can get the sample file available here:
Special thanks to my colleague, Mislav Kos for reviewing and contributing feedback on this post.
For another technique that uses Applescript, check out Makah’s post.