How do I create a blank PDF signature field using an HTML input document or after exporting to PDF using ABCPDF?

8.6k views Asked by At

I have a source HTML document generated from an ASPX.

I then convert the html to PDF using ABCPDF.

The html has a signature area, which is just a text box with a border.

I need a signature field inside the PDF with a name that I can pass on.

The PDF will be sent to a third-party who will correspond with the client and then digitally sign the PDF and send it back.

Given that I have an html document, or PDF, how do I programmatically add the blank PDF signature field around the original html signature area or somehow relate the two?

Here is a sample application to demonstrate some of the things I'm doing:

namespace ABCPDFHtmlSignatureTest
{
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using WebSupergoo.ABCpdf8;
    using WebSupergoo.ABCpdf8.Objects;

    /// <summary>
    /// The program.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// The file name.
        /// </summary>
        private const string FileName = @"c:\temp\pdftest.pdf";

        /// <summary>
        /// The application entry point.
        /// </summary>
        /// <param name="args">
        /// The args.
        /// </param>
        public static void Main(string[] args)
        {
            var html = GetHtml();

            var pdf = GetPdf(html);

            /* save the PDF to disk */
            File.WriteAllBytes(FileName, pdf);

            /* open the PDF */
            Process.Start(FileName);
        }

        /// <summary>
        /// The get PDF.
        /// </summary>
        /// <param name="html">
        /// The html.
        /// </param>
        /// <returns>
        /// The <see cref="byte"/>.
        /// </returns>
        public static byte[] GetPdf(string html)
        {
            var document = new Doc();

            /* Yes, generate PDF fields for the html form inputs */
            document.HtmlOptions.AddForms = true;

            document.AddImageHtml(html);

            /* We can determine the location of the field */
            var signatureRect = document.Form["Signature"].Rect;

            MakeFieldsReadOnly(document.Form.Fields);

            return document.GetData();
        }

        /// <summary>
        /// The get html.
        /// </summary>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public static string GetHtml()
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ABCPDFHtmlSignatureTest.HTMLPage1.html"))
            {
                using (var streamReader = new StreamReader(stream))
                {
                    return streamReader.ReadToEnd();
                }
            }
        }

        /// <summary>
        /// The make fields read only.
        /// </summary>
        /// <param name="fields">
        /// The fields.
        /// </param>
        private static void MakeFieldsReadOnly(Fields fields)
        {
            foreach (var field in fields)
            {
                if (field.Name == "Signature") continue;

                field.Stamp();
            }
        }
    }
}
1

There are 1 answers

0
Martin Lottering On

Hopefully this can help someone else.

I ended up using both ABCPDF and iTextSharp.

I used ABCPDF to convert the HTML to PDF, and to tell me where the element is, and then I used iTextSharp to put the blank signature field over it.

There were a couple of "gotchas" in this project:

  1. ABCPDF was better at converting HTML to PDF because it is more forgiving with non-standard html, and it was better at reading the physical paths when they contained small mistakes like "/" instead of "\".
  2. When sending your html to the PDF converter (iTextSharp or ABCPDF), the relative paths need to be changed into physical paths because the converter won't know in which web-site you are running or in which virtual directories to find the images, scripts and stylesheets. (See a converter below that can help with that)
  3. ABCPDF was better at interpreting the style sheets, and the end result looked much better with less code.
  4. When trying to figure out where ABCPDF placed the fields or tagged elements, bear in mind that after you add the first page, you still have to go into a loop to "chain" or register the rest of the pages, then only will you be able to resolve the field or the tagged element.

Here is a sample project to demonstrate the solution.

The sample html: (notice the abcpdf-tag-visible: true part in the style of the signature field, this will help us to see where the element is placed in the PDF)

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Test Document</title>
</head>
<body>
    <form method="POST">
        Sample Report Data: <br />
        <table style="border: solid 1px red; margin: 5px" cellpadding="5px">
            <tr>
                <td>Field 1:</td>
                <td><input type="text" id="field1" name="field1" value="FIELD1VALUE" /></td>
            </tr>
            <tr>
                <td>Field 2:</td>
                <td><input type="text" id="Text2" value="FIELD2VALUE" /></td>
            </tr>
            <tr>
                <td>Field 3:</td>
                <td><input type="text" id="Text3" value="FIELD3VALUE" /></td>
            </tr>
            <tr>
                <td>Field 4:</td>
                <td><input type="text" id="Text4" value="FIELD4VALUE" /></td>
            </tr>
            <tr>
                <td>Signature:</td>
                <td><textarea id="ClientSignature" style="background-color:LightCyan;border-color:Gray;border-width:1px;border-style:Solid;height:50px;width:200px;abcpdf-tag-visible: true"
                    rows="2" cols="20"></textarea></td>
            </tr>
        </table>       
    </form>
</body>
</html>

Here is a screen shot of the PDF with a blank signature field, opened with Adobe afterwards.

pdfresultscreenshot

A sample console application to help test out the PDF converters:

namespace ABCPDFHtmlSignatureTest
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;

    using iTextSharp.text;
    using iTextSharp.text.pdf;

    using WebSupergoo.ABCpdf8;

    /// <summary>
    /// The program.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// The file name.
        /// </summary>
        private const string PdfFileName = @"c:\temp\pdftest.pdf";

        /// <summary>
        /// Adds a blank signature field at the specified location.
        /// </summary>
        /// <param name="pdf">The PDF.</param>
        /// <param name="signatureRect">The signature location.</param>
        /// <param name="signaturePage">the page on which the signature appears</param>
        /// <returns>The new PDF.</returns>
        private static byte[] AddBlankSignatureField(byte[] pdf, Rectangle signatureRect, int signaturePage)
        {
            var pdfReader = new PdfReader(pdf);

            using (var ms = new MemoryStream())
            {
                var pdfStamper = new PdfStamper(pdfReader, ms);

                var signatureField = PdfFormField.CreateSignature(pdfStamper.Writer);

                signatureField.SetWidget(signatureRect, null);
                signatureField.Flags = PdfAnnotation.FLAGS_PRINT;
                signatureField.Put(PdfName.DA, new PdfString("/Helv 0 Tf 0 g"));
                signatureField.FieldName = "ClientSignature";
                signatureField.Page = signaturePage;

                pdfStamper.AddAnnotation(signatureField, signaturePage);
                pdfStamper.Close();

                return ms.ToArray();
            }
        }

        /// <summary>
        /// The application entry point.
        /// </summary>
        /// <param name="args">
        /// The args.
        /// </param>
        public static void Main(string[] args)
        {
            var html = GetHtml();

            XRect signatureRect;
            int signaturePage;
            byte[] pdf;

            GetPdfUsingAbc(html, out pdf, out signatureRect, out signaturePage);

            /* convert to type that iTextSharp needs */
            var signatureRect2 = new Rectangle(
                Convert.ToSingle(signatureRect.Left),
                Convert.ToSingle(signatureRect.Top),
                Convert.ToSingle(signatureRect.Right),
                Convert.ToSingle(signatureRect.Bottom));

            pdf = AddBlankSignatureField(pdf, signatureRect2, signaturePage);

            /* save the PDF to disk */
            File.WriteAllBytes(PdfFileName, pdf);

            /* open the PDF */
            Process.Start(PdfFileName);
        }

        /// <summary>
        /// Returns the PDF for the specified html. The conversion is done using ABCPDF.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="pdf">the PDF</param>
        /// <param name="signatureRect">the location of the signature field</param>
        /// <param name="signaturePage">the page of the signature field</param>
        public static void GetPdfUsingAbc(string html, out byte[] pdf, out XRect signatureRect, out int signaturePage)
        {
            var document = new Doc();
            document.MediaBox.String = "A4";
            document.Color.String = "255 255 255";
            document.FontSize = 7;

            /* tag elements marked with "abcpdf-tag-visible: true" */
            document.HtmlOptions.AddTags = true;

            int pageId = document.AddImageHtml(html, true, 950, true);
            int pageNumber = 1;

            signatureRect = null;
            signaturePage = -1;
            TryIdentifySignatureLocationOnCurrentPage(document, pageId, pageNumber, ref signatureRect, ref signaturePage);

            while (document.Chainable(pageId))
            {
                document.Page = document.AddPage();
                pageId = document.AddImageToChain(pageId);

                pageNumber++;
                TryIdentifySignatureLocationOnCurrentPage(document, pageId, pageNumber, ref signatureRect, ref signaturePage);
            }

            pdf = document.GetData();
        }

        /// <summary>
        /// The try identify signature location on current page.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="currentPageId">The current page id.</param>
        /// <param name="currentPageNumber">The current page number.</param>
        /// <param name="signatureRect">The signature location.</param>
        /// <param name="signaturePage">The signature page.</param>
        private static void TryIdentifySignatureLocationOnCurrentPage(Doc document, int currentPageId, int currentPageNumber, ref XRect signatureRect, ref int signaturePage)
        {
            if (null != signatureRect) return;

            var tagIds = document.HtmlOptions.GetTagIDs(currentPageId);

            if (tagIds.Length > 0)
            {
                int index = -1;
                foreach (var tagId in tagIds)
                {
                    index++;
                    if (tagId.Contains("ClientSignature"))
                    {
                        var rects = document.HtmlOptions.GetTagRects(currentPageId);

                        signatureRect = rects[index];
                        signaturePage = currentPageNumber;

                        break;
                    }
                }                
            }
        }

        /// <summary>
        /// The get html.
        /// </summary>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public static string GetHtml()
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ABCPDFHtmlSignatureTest.HTMLPage1.html"))
            {
                if (null == stream)
                {
                    throw new InvalidOperationException("Unable to resolve the html");
                }

                using (var streamReader = new StreamReader(stream))
                {
                    return streamReader.ReadToEnd();
                }
            }
        }
    }
}

When running inside the web server and still generating the HTML, you can use this class to change the relative (virtual) paths to physical (UNC) paths:

namespace YourNameSpace
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Web;

    /// <summary>
    /// Replaces all uris within in an html document to physical paths, making it valid
    /// html outside the context of a web site. This is necessary because outside the
    /// context of a web site root folder, the uris are meaningless, and the html cannot
    /// be interpreted correctly by external components, like ABCPDF or iTextSharp. 
    /// Without this step, the images and other 'SRC' references cannot be resolved.
    /// </summary>
    public sealed class HtmlRelativeToPhysicalPathConverter
    {
        #region FIELDS

        /// <summary>
        /// The _server.
        /// </summary>
        private readonly HttpServerUtility _server;

        /// <summary>
        /// The _html.
        /// </summary>
        private readonly string _html;

        #endregion

        #region CONSTRUCTOR

        /// <summary>
        /// Initialises a new instance of the <see cref="HtmlRelativeToPhysicalPathConverter"/> class.
        /// </summary>
        /// <param name="server">
        /// The server.
        /// </param>
        /// <param name="html">
        /// The html.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// when <paramref name="server"/> or <paramref name="html"/> is null or empty.
        /// </exception>
        public HtmlRelativeToPhysicalPathConverter(HttpServerUtility server, string html)
        {
            if (null == server) throw new ArgumentNullException("server");
            if (string.IsNullOrWhiteSpace(html)) throw new ArgumentNullException("html");

            _server = server;
            _html = html;
        }

        #endregion

        #region Convert Html

        /// <summary>
        /// Convert the html.
        /// </summary>
        /// <param name="leaveUrisIfFileCannotBeFound">an additional validation can be performed before changing the uri to a directory path</param>
        /// <returns>The converted html with physical paths in all uris.</returns>
        public string ConvertHtml(bool leaveUrisIfFileCannotBeFound = false)
        {
            var htmlBuilder = new StringBuilder(_html);

            // Double quotes
            foreach (var relativePath in this.GetRelativePaths(htmlBuilder, '"'))
            {
                this.ReplaceRelativePath(htmlBuilder, relativePath, leaveUrisIfFileCannotBeFound);
            }

            // Single quotes
            foreach (var relativePath in this.GetRelativePaths(htmlBuilder, '\''))
            {
                this.ReplaceRelativePath(htmlBuilder, relativePath, leaveUrisIfFileCannotBeFound);
            }

            return htmlBuilder.ToString();
        }

        #endregion

        #region Replace Relative Path

        /// <summary>
        /// Convert a uri to the physical path.
        /// </summary>
        /// <param name="htmlBuilder">The html builder.</param>
        /// <param name="relativePath">The relative path or uri string.</param>
        /// <param name="leaveUrisIfFileCannotBeFound">an additional validation can be performed before changing the uri to a directory path</param>
        private void ReplaceRelativePath(StringBuilder htmlBuilder, string relativePath, bool leaveUrisIfFileCannotBeFound)
        {
            try
            {
                var parts = relativePath.Split('?');
                var mappedPath = _server.MapPath(parts[0]);
                if ((leaveUrisIfFileCannotBeFound && File.Exists(mappedPath)) || !leaveUrisIfFileCannotBeFound)
                {
                    if (parts.Length > 1)
                    {
                        mappedPath += "?" + parts[1];
                    }
                    htmlBuilder.Replace(relativePath, mappedPath);
                }
                else
                {
                    /* decide what you want to do with these */
                }
            }
            catch (ArgumentException)
            {
                /* ignore these */
            }            
        }
        #endregion

        #region Get Relative Paths
        /// <summary>
        /// They are NOT guaranteed to be valid uris, simply values between quote characters.
        /// </summary>
        /// <param name="html">the html builder</param>
        /// <param name="quoteChar">the quote character to use, e.g. " or '</param>
        /// <returns>each of the relative paths</returns>
        private IEnumerable<string> GetRelativePaths(StringBuilder html, char quoteChar)
        {
            var position = 0;
            var oldPosition = -1;
            var htmlString = html.ToString();
            var previousUriString = string.Empty;

            while (oldPosition != position)
            {
                oldPosition = position;

                position = htmlString.IndexOf(quoteChar, position + 1);

                if (position == -1) break;

                var uriString = htmlString.Substring(oldPosition + 1, (position - oldPosition) - 1);

                if (Uri.IsWellFormedUriString(uriString, UriKind.Relative)
                    && uriString != previousUriString
                    /* as far as I know we never reference a file without an extension, so avoid the IDs this way */
                    && uriString.Contains(".") && !uriString.EndsWith("."))
                {
                    yield return uriString;

                    /* refresh the html string, and reiterate again */
                    htmlString = html.ToString();
                    position = oldPosition;
                    oldPosition = position - 1; /* don't exit yet */

                    previousUriString = uriString;
                }
            }
        }
        #endregion

    }
}

You can use the class like this:

var html = textWriter.ToString();

// change relative paths to be absolute
html = new HtmlRelativeToPhysicalPathConverter(server, html).ConvertHtml();