Add a signature field (not a signature) with PdfSharp

Recently at work we got a request to add a signature field to a generated PDF. Note this isn't a signature, just the field to let the end user do the signing with Acrobat or some other program. Apparently this isn't a very common use case and it took a lot of work to get it working, so this post aims to walk you through the steps taken.

Permalink to “The Search”

I started my search with the query pdfsharp add signature field, which led to many solutions for signing a PDF with a key on the server but only one about adding just the visible signature field to the document: https://stackoverflow.com/questions/57654662/add-field-for-a-digital-signature-to-pdf

While this answer in itself didn't work, the comments were the key as they linked to a PR (through an older PR with the same goal) that strived to add up-to-date signature support to PdfSharp natively.

Permalink to “The PR”

https://github.com/empira/PDFsharp/pull/48

This PR is the key to getting this done. Deep in the comment thread, someone else asked how to do what we want! The PR creator's response is basically to copy their code which adds the visible signature field and leave out the parts that calculate and assign the signature.

At the time of writing the PR hasn't been merged and even though we're copying the implementation we're still using some of the constants and helpers added in the PR so we will need some version of PdfSharp with those changes merged. It appears that the PR's author has created such a library as pdfsharp-extended. I had to use the 1.2.0 preview version to get around a font bug.

Permalink to “The Journey”

With the library installed I began to dig into the file mentioned by the PR author in their response to the comment asking how to do this. That place is PdfSignatureHandler.AddSignatureComponent. This function does some signature padding (which we'll ignore), puts that padding into a dictionary (which we'll ignore), creates a signature field, and adds that field to the document. We only care about creating the field and adding it, so we'll take those bits.

Permalink to “Creating the field”

This is done mostly in PdfSignatureHandler.GetSingatureField. We're gonna ignore the line that adds the passed-in signature data to the /V (value) field and copy the rest:

var signatureField = new PdfSignatureField(Document);

signatureField.Elements.Add(PdfSignatureField.Keys.FT, new PdfName("/Sig"));
signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1")); // TODO? if already exists, will it cause error? implement a name choser if yes
signatureField.Elements.Add(PdfSignatureField.Keys.Ff, new PdfInteger(132));
signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary());
signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot"));
signatureField.Elements.Add("/Subtype", new PdfName("/Widget"));
signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]);

signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle));

signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler()
{
    Location = Options.Location,
    Reason = Options.Reason,
    Signer = signer.GetName()
};
signatureField.PrepareForSave(); // TODO: for some reason, PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically

Document.Internals.AddObject(signatureField);

Since we aren't doing a signature and I already had the rectangle to put the signature block in, I removed the signatureField.CustomAppearanceHandler and signatureField.PrepareForSave() lines. I also just hardcoded the PageIndex to 0 for now.
Now here's where this starts being a pain: PdfSignatureField only has internal constructors, and it's sealed. So we have to use reflection to call that constructor:

//var signatureField = new PdfSignatureField(document);
PdfSignatureField signatureField = (PdfSignatureField)
    typeof(PdfSignatureField)
    .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [typeof(PdfDocument)])
    .Invoke([document]);
Permalink to “Adding the field to the document”

Now, the second half of AddSignatureComponents! This bit of code adds the created field to the document in all the different places it needs, creating the containing data structures along the way if needed.

var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots);
if (annotations == null)
    Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField));
else
    annotations.Elements.Add(signatureField);


// acroform

var catalog = Document.Catalog;

if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null)
    catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document));

if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags))
    catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3));
else
{
    var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags);
    if (sigFlagVersion < 3)
        catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3);
}

if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null)
    catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray()));
catalog.AcroForm.Fields.Elements.Add(signatureField);

Once again we run into things being internal, notably the PdfAcroForm and PdfAcroField.PdfAcroFieldCollection constructors and the PdfAcroForm.Keys` constants. I replaced the constants with their values and used reflection similar to above to call the constructors.
Once this was all complete and combined, I ended up with a helper that could add a signature field anywhere on the first page of the document given that document and a rectangle. If you improve this to handle putting it on other pages, please let me know.

Permalink to “The Implementation”

Here's the final product!

/// <summary>
/// Add an unsigned signature field to the document that fills the given rectangle.
/// </summary>
public static void AddSignatureField(PdfDocument document, PdfRectangle rect)
{
    //This is based upon the code from https://github.com/empira/PDFsharp/pull/48, a PR that adds better signature support to PDFSharp.
    //At the time of writing, this PR is unmerged. We have temporarily switched to using PDFSharp-extended which has this change merged.
    //In the comment chain, a user asks how to add an unsigned signature box: https://github.com/empira/PDFsharp/pull/48#issuecomment-1873730594
    //They are told to do something similar to how the field is added in the PR, just leaving out the actual signature part.
    //The code in the PR closely intermingles the signing of the document and the placement of the signature block on the page.

    //This method is the result of copying that PR's code, detangling the signing and placement,
    //  and working around that most parts of the code are `internal` to PDFSharp. (👻 reflection 👻)

    #region Create the field
    //https://github.com/empira/PDFsharp/pull/48/files#diff-36f814394f9be8a6ab338de355c256dc1ac8288253b9031139fae1a1f21b65e1R156
    //https://github.com/empira/PDFsharp/pull/48/files#diff-36f814394f9be8a6ab338de355c256dc1ac8288253b9031139fae1a1f21b65e1R186

    //var signatureField = new PdfSignatureField(document);
    PdfSignatureField signatureField = (PdfSignatureField)
        typeof(PdfSignatureField)
        .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [typeof(PdfDocument)])
        .Invoke([document]);

    signatureField.Elements.Add(PdfSignatureField.Keys.FT, new PdfName("/Sig"));
    signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1"));
    signatureField.Elements.Add(PdfSignatureField.Keys.Ff, new PdfInteger(132));
    signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary());
    signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot"));
    signatureField.Elements.Add("/Subtype", new PdfName("/Widget"));
    signatureField.Elements.Add("/P", document.Pages[0]); //hardcoding page 0 here

    signatureField.Elements.Add("/Rect", rect);

    document.Internals.AddObject(signatureField);
    #endregion

    #region Add the field to the document
    //PdfPage.Keys references in this section have been replaced with their constants due to it being `internal`.

    //https://github.com/empira/PDFsharp/pull/48/files#diff-36f814394f9be8a6ab338de355c256dc1ac8288253b9031139fae1a1f21b65e1R158
    var annotations = document.Pages[0].Elements.GetArray("/Annots");
    if (annotations == null)
        document.Pages[0].Elements.Add("/Annots", new PdfArray(document, signatureField));
    else
        annotations.Elements.Add(signatureField);

    //https://github.com/empira/PDFsharp/pull/48/files#diff-36f814394f9be8a6ab338de355c256dc1ac8288253b9031139fae1a1f21b65e1R165
    var catalog = document.Internals.Catalog;

    if (catalog.Elements.GetObject("/AcroForm") == null)
        catalog.Elements.Add("/AcroForm",
            //new PdfAcroForm(document)
            (PdfAcroForm)typeof(PdfAcroForm)
                .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [typeof(PdfDocument)])
                .Invoke([document])
            );

    if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags))
        catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3));
    else
    {
        var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags);
        if (sigFlagVersion < 3)
            catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3);
    }

    if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null)
        catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields,
            //new PdfAcroField.PdfAcroFieldCollection(new PdfArray())
            (PdfAcroField.PdfAcroFieldCollection)typeof(PdfAcroField.PdfAcroFieldCollection)
                .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [typeof(PdfArray)])
                .Invoke([new PdfArray()])
            );
    catalog.AcroForm.Fields.Elements.Add(signatureField);
    #endregion
}

This can be used like this:

//digital signature block
var pdfPosition = gfx.Transformer.WorldToDefaultPage(new XPoint(144, 600));
var signatureRect = new PdfRectangle(new XRect(pdfPosition.X, pdfPosition.Y, 200, 50));

AddSignatureField(document, signatureRect);

Thank you for reading