PDF after form fields completed

Generating a PDF document from user entered data in a WFFM Custom Save Action in Habitat

Posted 07/11/2016 by Paul Tiemann

Recently we had a client request for a feature to generate user specific award certificate PDFs that users could download on demand. We could have created a scheduled job that would create these, but it made much more sense just to create the PDF on demand when a user clicked a link on the awards page.

I want to share how we did this using iTextSharp, an open source PDF library. For the client, we retrieved the fill in data from a custom back-end database. Since that wouldn't be very reusable for you to borrow from a blog post, I wanted to create something you can use, so I decided to show two techniques, filing in a PDF with iTextSharp and creating a custom Sitecore Web Forms For Marketers (WFFM) Save Action.

By using a WFFM form for the inputs, we have a generic way of filling in content in a PDF form on demand from user typed input. The method is generic, so new PDF forms can be created and setup by content authors with a WFFM form with no additional development needed.
When you need to create a PDF document with user replaceable values, you start with a base Adobe Acrobat PDF and add form fields and then the code fills in the form fields and outputs this to the user. See note below for how I created this PDF form and what it's requirements are.

Since Sitecore Habitat is what we are using for new development projects, I wanted to do this in the Habitat environment. To save time, I added this to the existing Habitat project Foundation\Forms. It could also have been created a new project.

Standard Disclaimer: The code is not production ready, but if there is enough interest, I can make is so and submit it to the Habitat team for inclusion with the project. Attached below is a Sitecore package with the Sitecore items, and a zipped copy of the project.

How long does it take a Sitecore Developer to create a data template?
Only about an hour but it takes another hour to pick the right icon. ;)

With the custom save action, we need two variables to set by the content author, 

  1. The starter PDF filename which to which we will be adding content.
  2. Where the base file is located on the website. 

Step 1

We create a WFFM dialog defined in an XML file, and a C# class that is called to populate and save the values. To save time, I added this to the existing Habitat project Foundation\Forms. This did require adding additional references to enable the MVC view.

This file is located at: ~/sitecore/shell/Applications/Dialogs/Forms/CreatePdfEditor.xml

Here's the dialog definition:

  
<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"  xmlns:content="http://www.sitecore.net/content">
    <CreatePdfForm.Editor>
        <FormDialog ID="Dialog" Icon="Applications/32x32/document_add.png"  >
            <Stylesheet Src="FormBuilder.css" DeviceDependant="true"/>
            <CodeBeside Type="Sitecore.Foundation.Forms.ActionEditors.CreatePdfEditor,Sitecore.Foundation.Forms"/>
            <DataContext ID="ItemDataContext" DataViewName="Master" Database="master" ShowRoot="true" Root="{062A1E69-0BF6-4D6D-AC4F-C11D0F7DC1E1}" />
            <GridPanel Columns="1" CellPadding="4" Width="100%" Height="100%" Style="table-layout:fixed">
                <Border Width="100%" Height="40%">
                    <Literal ID="SelectBasePDFLiteral" Text="Enter the base PDF File's name" />
                    <Edit ID="EbFileName" GridPanel.Width="80%" Width="100%"/>
                </Border>
                <Border Width="100%" Height="40%">
                    <Literal id="SelectRelativePathLiteral" Text="Enter the Relative Path of Base PDF Location  ensuring that it starts with /"/>
                    <Edit ID="EbRelativePath" GridPanel.Width="80%" Width="100%"/>
                </Border>
            </GridPanel>
        </FormDialog>
    </CreatePdfForm.Editor>
</control>

This dialog definition has highlights for what you would need to change to create a new dialog for a different save action. To add more variables, add more rows via the Border tag. 

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"  xmlns:content="http://www.sitecore.net/content">
    <CreatePdfForm.Editor>
        <FormDialog ID="Dialog" Icon="Applications/32x32/document_add.png"  >
            <Stylesheet Src="FormBuilder.css" DeviceDependant="true"/>
            <CodeBeside Type="Sitecore.Foundation.Forms.ActionEditors.CreatePdfEditor,Sitecore.Foundation.Forms"/>
            <DataContext ID="ItemDataContext" DataViewName="Master" Database="master" ShowRoot="true" Root="{062A1E69-0BF6-4D6D-AC4F-C11D0F7DC1E1}" />
            <GridPanel Columns="1" CellPadding="4" Width="100%" Height="100%" Style="table-layout:fixed">
                <Border Width="100%" Height="40%">
                    <Literal ID="SelectBasePDFLiteral" Text="Enter the base PDF File's name" />
                    <Edit ID="EbFileName" GridPanel.Width="80%" Width="100%"/>
                </Border>
                <Border Width="100%" Height="40%">
                    <Literal id="SelectRelativePathLiteral" Text="Enter the Relative Path of Base PDF Location  ensuring that it starts with /"/>
                    <Edit ID="EbRelativePath" GridPanel.Width="80%" Width="100%"/>
                </Border>
            </GridPanel>
        </FormDialog>
    </CreatePdfForm.Editor>
</control>

Here is the dialog as displayed to edit the save action parameters in my test form.

Save Action Settings

Step 2

We need to created a CodeBeside class to handle populating and saving the parameters from the dialog xml file above. It goes into the project here:
..\Habitat\src\Foundation\Forms\code\ActionEditors\CreatePdfEditor.cs

namespace Sitecore.Foundation.Forms.ActionEditors
{
    using System;
    using Sitecore.Data;
    using Sitecore.Foundation.SitecoreExtensions.Services;
    using Sitecore.Web.UI.HtmlControls;
    using Sitecore.Web.UI.Sheer;

    public class CreatePdfEditor : BaseActionEditor
    {
        public CreatePdfEditor(ISheerService sheerService) : base(sheerService)
        {
        }

        public CreatePdfEditor() : this(new SheerService())
        {
        }

        public DataContext ItemDataContext { get; set; }
        public Edit EbFileName { get; set; }
        public Edit EbRelativePath { get; set; }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
            {
                return;
            }
            var nameValueCollection = this.Parameters;
            if (nameValueCollection == null)
            {
                return;
            }
            this.EbFileName.Value = nameValueCollection[Constants.PdfFileParameter];
            this.EbRelativePath.Value = nameValueCollection[Constants.PdfRelativePathParameter];
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            if (this.EbFileName.Value.Length < 4)
            {
                SheerResponse.Alert("Please Enter a PDF filename");
            }
            else
            {
                this.Parameters.Set(Constants.PdfFileParameter, this.EbFileName.Value);
            }
            if (this.EbRelativePath.Value.Length < 1 || !this.EbRelativePath.Value.StartsWith("/"))
            {
                SheerResponse.Alert("Please Enter a Relative Path starting with /");
            }
            else
            {
                this.Parameters.Set(Constants.PdfRelativePathParameter, this.EbRelativePath.Value);
            }
            base.OnOK(sender, args);
        }
    }
}

It handles saving and repopulating the two parameters needed as well as some input validation. See notes below about possible additional enhancements

Step 3

Add references to needed MVC and weblibraries to the project file using nuget. 

To add iTextSharp to a project, use nuget, details here: https://www.nuget.org/packages/iTextSharp/ 

Step 4

Now we need to define the Custom Save action class. It goes into the project here: 
.\Habitat\src\Foundation\Forms\code\SaveActions\CreatePdfSaveAction.cs

You can see below that iTextSharp exposes a collection of form fields accessible by key. For purposes of this, I have a requirement that the form author use no spaces in the form field title. More about creating forms here.

namespace Sitecore.Foundation.Forms.SaveActions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Sitecore.Diagnostics;
    using Sitecore.WFFM.Abstractions.Actions;
    using Sitecore.WFFM.Actions.Base;
    using iTextSharp.text.pdf;
    using System.IO;
    using Sitecore.Data;
    using Web;
    public class CreatePdfSaveAction : WffmSaveAction
    {
        private const string PdfExtension = ".pdf";
        public string BasePdfFile { get; set; }
        public string PdfRelativePath { get; set; }


        public override void Execute(ID formId, AdaptedResultList adaptedFields, ActionCallContext actionCallContext = null, params object[] data)
        {
            if (string.IsNullOrEmpty(this.BasePdfFile) || string.IsNullOrEmpty(this.PdfRelativePath))
            {
                Log.Warn("Can't create a PDF. BasePDF or relative path isn't set", this);
            }
            Dictionary substitutions = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
            foreach (AdaptedControlResult adaptedField in adaptedFields)
            {
                substitutions.Add(RemoveWhitespace(adaptedField.FieldName), adaptedField.Value);
            }
            string pdfPath = this.CreatePdfWithUpdatedFields(substitutions);
            HttpContext.Current.Session[WebUtil.GetSessionID() + "_CertificateLink"] = StringUtil.EnsurePrefix('/', pdfPath);
        }

        private string CreatePdfWithUpdatedFields(Dictionary substitutions)
        {
            string appRootDir = HttpContext.Current.Server.MapPath("~");

            string startFileName = appRootDir + this.PdfRelativePath + this.BasePdfFile;
            string finalFileRelativeFileName = this.PdfRelativePath + Guid.NewGuid().ToString() + PdfExtension; ;
            string finalFileName = appRootDir + finalFileRelativeFileName; 

                using (FileStream existingFileStream = new FileStream(startFileName, FileMode.Open))
                {
                    using (FileStream newFileStream = new FileStream(finalFileName, FileMode.Create))
                    {
                        PdfReader pdfReader = new PdfReader(existingFileStream);

                        PdfStamper stamper = new PdfStamper(pdfReader, newFileStream);
                        var form = stamper.AcroFields;
 
                        //case insensitive comparison
                        //using actual key from the pdf form field
                        foreach (string key in form.Fields.Keys)
                        {
                            if (substitutions.ContainsKey(key))
                            {
                                form.SetField(key, substitutions[key]);
                            }
                        }

                        stamper.FormFlattening = true;

                        stamper.Close();
                        pdfReader.Close();
                    }
            }
            return finalFileRelativeFileName;
        }

        public static string RemoveWhitespace(string input)
        {
            return new string(input.ToCharArray()
                .Where(c => !char.IsWhiteSpace(c))
                .ToArray());
        }
    }
}

Step 5

Now we need to tell Sitecore about our Save Action, it's code class, the dialog, and it's CodeBeside class. 

Screenshot of Save Action

Step 6

Here's the save action added to a form. See above for the dialog and values.

Save Action Selector Dialog

Step 7

We need to get the PDF to the user. I wanted to figure out how to force a download of the PDF document after the user clicked OK but couldn't find a reliable way in the time I had. In our original implementation, we did this via an MVC Route and ResultAction but as mentioned earlier, we got our data from database, not WFFM. In order to get the unique generated file link I took the easy way, by stuffing the URL into session in the save action, then retrieve and display it below the form.  (I'm very open to comments for anyone with a "Better Idea!" on doing this.) I could have used Mike Reynolds excellent storage class but decided it was overkill for one value.  

Here's the result view. I put it in presentation details just below the form. It only shows the result link after the form posts back.

@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Foundation.Forms
@using Sitecore.Web
@model Sitecore.Mvc.Presentation.RenderingModel

@{
    var sessionId = WebUtil.GetSessionID();
    var url = HttpContext.Current.Session[sessionId + "_CertificateLink"];
    HttpContext.Current.Session.Remove(sessionId + "_CertificateLink");
}
@if (url != null) { Good Job, Here is Your Certficate! }

Finally here is an example of the PDF certificate that is output.

Additional Notes

Files:

It my earlier years as a developer I had to create a custom PDF from code which was a royal pain. I had to set xy coordinates, font type, color, and a bunch of other stuff. Just finding the correct coordinates was a tricky requiring trial and error. If you needed your text centered you had to do a bunch of calculations based on font size to know where to start writing.

How to create a PDF form for free

iTextSharp has a much easier method to modify a PDF document. You create your base PDF with a space for the fields that need to be completed, then using either Adobe Acrobat $$$ or free tool such as the online PDFEscape site add form fields to the document. Here's how to do that.

  1. Create a PDF in Word or some other document
  2. Open the document on PDFEscape by uploading or drag and drop
  3. Click "Form Field" on the Insert tab, (left side of page)
  4. Choose Text 
  5. Click and drag where the field needs to go
  6. Right click and choose object properties
  7. Change the field name.  Notes:
    1. The field names must match the field names in the WFFM form except the form can have spaces in the name. See note. 
    2. The PDF form fields should not have spaces in them. 
    3. I made mine all caps but that's not required. The match in the save action is case insensitive. 
  8. Add any additional fields.

Other nice to have enhancements

  • The WFFM save action form dialog CodeBeside could open the selected PDF and validate that the PDF form field names match the WFFM form fields.
  • The PDF could be stored in media library instead of on the file system.
  • As mentioned earlier, a method of automatically sending the PDF to the browser on form submit would be nice. 

Share:

Add your comment

 
 

 

Archive

Syndication