7 Apr 2011

SharePoint Workflows: Custom Actions

by Dave

Although I have been aware of custom actions in SharePoint workflows, and I have used them before, I haven't had a chance to actually develop one.  That is, unitl now...

I recently had the need to convert documents in SharePoint to PDF and digitally sign them, as part of a workflow; in comes the custom action.  The action that I will describe below uses the Word Interop to save the document as PDF (using the Word 2007 and 2010 save to PDF add-in), and then signs the document with a certificate (and scanned signature image) using the open-source iText PDF library.

Below is the code to convert a Word document to PDF, using the Word Interop:

object paramMissing = Type.Missing;
object strInputFileName = strFileName;
ApplicationClass wordApplication = null;
Document wordDocument = null;
string paramExportFilePath = strFileName.Substring(0, strFileName.LastIndexOf('.')) + ".pdf";
WdExportFormat paramExportFormat = WdExportFormat.wdExportFormatPDF;
bool paramOpenAfterExport = false;
WdExportOptimizeFor paramExportOptimizeFor = WdExportOptimizeFor.wdExportOptimizeForPrint;
WdExportRange paramExportRange = WdExportRange.wdExportAllDocument;
int paramStartPage = 0;
int paramEndPage = 0;
WdExportItem paramExportItem = WdExportItem.wdExportDocumentContent;
bool paramIncludeDocProps = true;
bool paramKeepIRM = true;
WdExportCreateBookmarks paramCreateBookmarks = WdExportCreateBookmarks.wdExportCreateWordBookmarks;
bool paramDocStructureTags = true;
bool paramBitmapMissingFonts = true;
bool paramUseISO19005_1 = false;

try
{
	wordApplication = new ApplicationClass();
	wordDocument = wordApplication.Documents.Open(
		ref strInputFileName, ref paramMissing, ref paramMissing,
		ref paramMissing, ref paramMissing, ref paramMissing,
		ref paramMissing, ref paramMissing, ref paramMissing,
		ref paramMissing, ref paramMissing, ref paramMissing,
		ref paramMissing, ref paramMissing, ref paramMissing,
		ref paramMissing);

	// Export it in the specified format.
	if (wordDocument != null)
	{
		wordDocument.ExportAsFixedFormat(paramExportFilePath,
			paramExportFormat, paramOpenAfterExport,
			paramExportOptimizeFor, paramExportRange, paramStartPage,
			paramEndPage, paramExportItem, paramIncludeDocProps,
			paramKeepIRM, paramCreateBookmarks, paramDocStructureTags,
			paramBitmapMissingFonts, paramUseISO19005_1,
			ref paramMissing);
	}                
}
catch (Exception ex)
{
	throw new ApplicationException(string.Format("Could not convert document to PDF: {0}", strFileName), ex);
}
finally
{
	// Close and release the Document object.
	if (wordDocument != null)
	{
		wordDocument.Close(ref paramMissing, ref paramMissing, ref paramMissing);
		wordDocument = null;
	}

	// Quit Word and release the ApplicationClass object.
	if (wordApplication != null)
	{
		wordApplication.Quit(ref paramMissing, ref paramMissing, ref paramMissing);
		wordApplication = null;
	}

	GC.Collect();
	GC.WaitForPendingFinalizers();
	GC.Collect();
	GC.WaitForPendingFinalizers();                
}

In a nutshell, the first bit sets up all of the required variables and parameters .  The second part opens the word document specified, and the next saves the document as a PDF.   Job done.

Now that we have our PDF (I wrote mine to a MemoryStream, and then converted it to a byte[]), we can use iText to sign it and add our signature image:

public byte[] Sign(byte[] bPdfContent)
{
	PdfReader reader = new PdfReader(bPdfContent);

	byte[] bSignedContent = null;

	using (EventFiringMemoryStream memStream = new EventFiringMemoryStream())
	{
		memStream.Closing += new EventHandler(delegate(object sender, EventArgs e) { bSignedContent = (sender as MemoryStream).ToArray(); });

		PdfStamper stamper = PdfStamper.CreateSignature(reader, memStream, '\0', null, true);
		stamper.MoreInfo = MetaData.Info;
		stamper.XmpMetadata = MetaData.GetStreamedMetaData();

		PdfSignatureAppearance sap = stamper.SignatureAppearance;
		sap.SetCrypto(Certificate.Akp, Certificate.Chain, null, PdfSignatureAppearance.WINCER_SIGNED);
		sap.Reason = Certificate.Reason;
		sap.Location = Certificate.Location;
		sap.Contact = Certificate.Contact;
		if (!string.IsNullOrEmpty(Certificate.ImagePath))
		{
			Uri uri = new Uri(Certificate.ImagePath);
			Stream sImageStream = null;
			try
			{
				//Image is not on local machine, so we need to make a web request to get it
				if (!uri.IsFile)
				{
					WebRequest wrRequest = WebRequest.Create(uri);
					wrRequest.Credentials = CredentialCache.DefaultNetworkCredentials;

					WebResponse response = wrRequest.GetResponse();
					sImageStream = response.GetResponseStream();
				}
				else
				{
					sImageStream = File.Open(uri.LocalPath, FileMode.Open);
				}

				if (sImageStream != null)
				{
					iTextSharp.text.Image iSigImage = iTextSharp.text.Image.GetInstance(sImageStream);
					int iLeftPos = Certificate.PositionFromLeft - ((int)iSigImage.Width / 2);
					int iBottomPos = Certificate.PositionFromBottom + ((int)iSigImage.Height / 2);
					sap.SetVisibleSignature(new iTextSharp.text.Rectangle(iLeftPos, iBottomPos, iLeftPos + iSigImage.Width, iBottomPos + iSigImage.Height), reader.NumberOfPages, "Workflow Signature");
					sap.Image = iSigImage;
					sap.Layer2Text = "";
					sap.Layer4Text = "";
				}
			}
			catch (Exception ex)
			{
				throw new ApplicationException(string.Format("Could not load signature: {0} ({1})", Certificate.ImagePath, ex.Message), ex);
			}
			finally
			{
				if (sImageStream != null)
				{
					sImageStream.Close();
					sImageStream.Dispose();
					sImageStream = null;
				}
			}
		}

		stamper.Close();
	}

	return bSignedContent;
}

Now, there is quite a lot going on here, so I'll break it down.

First, we open our PDF document using iText's PdfReader, and passing in the byte[]:

public byte[] Sign(byte[] bPdfContent)
{
	PdfReader reader = new PdfReader(bPdfContent);

Then, we open an extended version of MemoryStream (which I have called EventFiringMemoryStream).  The reason for this is due to the way that the iText library signs PDFs.  It expects something like a FileStream, which it can just close itself;this is fine if you are outputting to a file, but not if you want to get at the data, as in the case of a MemoryStream.  I got around this problem by overriding the Close method of MemoryStream, and added an event to output the data before the stream is completely closed.

public class EventFiringMemoryStream : MemoryStream
{
	public event EventHandler Closing;

	public EventFiringMemoryStream() : base() { }
	public EventFiringMemoryStream(byte[] buffer) : base(buffer) { }
	public EventFiringMemoryStream(int iCapacity) : base(iCapacity) { }
	public EventFiringMemoryStream(byte[] buffer, bool writable) : base(buffer, writable) { }
	public EventFiringMemoryStream(byte[] buffer, int index, int count) : base(buffer, index, count) { }
	public EventFiringMemoryStream(byte[] buffer, int index, int count, bool writable) : base(buffer, index, count, writable) { }
	public EventFiringMemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible) : base(buffer, index, count, writable, publiclyVisible) { }        

	public override void Close()
	{
		if (Closing != null)
		{
			Closing(this, EventArgs.Empty);
		}

		base.Close();
	}
}

Simples.

With this done, I can then attach to this event to catch the data when the stream is closed by iText:

byte[] bSignedContent = null;

	using (EventFiringMemoryStream memStream = new EventFiringMemoryStream())
	{
		memStream.Closing += new EventHandler(delegate(object sender, EventArgs e) { bSignedContent = (sender as MemoryStream).ToArray(); });

 

Now down to the certificate signing: 

PdfStamper stamper = PdfStamper.CreateSignature(reader, memStream, '\0', null, true);
stamper.MoreInfo = MetaData.Info;
stamper.XmpMetadata = MetaData.GetStreamedMetaData();

PdfSignatureAppearance sap = stamper.SignatureAppearance;
sap.SetCrypto(Certificate.Akp, Certificate.Chain, null, PdfSignatureAppearance.WINCER_SIGNED);
sap.Reason = Certificate.Reason;
sap.Location = Certificate.Location;
sap.Contact = Certificate.Contact;

Adapting code from the iSafePdf library, I signed the PDF using a supplied certificate.  I then added my signature image (pulled form a URL or local file) and inserted that into the document:

if (!string.IsNullOrEmpty(Certificate.ImagePath))
{
	Uri uri = new Uri(Certificate.ImagePath);
	Stream sImageStream = null;
	try
	{
		//Image is not on local machine, so we need to make a web request to get it
		if (!uri.IsFile)
		{
			WebRequest wrRequest = WebRequest.Create(uri);
			wrRequest.Credentials = CredentialCache.DefaultNetworkCredentials;

			WebResponse response = wrRequest.GetResponse();
			sImageStream = response.GetResponseStream();
		}
		else
		{
			sImageStream = File.Open(uri.LocalPath, FileMode.Open);
		}

		if (sImageStream != null)
		{
			iTextSharp.text.Image iSigImage = iTextSharp.text.Image.GetInstance(sImageStream);
			int iLeftPos = Certificate.PositionFromLeft - ((int)iSigImage.Width / 2);
			int iBottomPos = Certificate.PositionFromBottom + ((int)iSigImage.Height / 2);
			sap.SetVisibleSignature(new iTextSharp.text.Rectangle(iLeftPos, iBottomPos, iLeftPos + iSigImage.Width, iBottomPos + iSigImage.Height), reader.NumberOfPages, "Workflow Signature");
			sap.Image = iSigImage;
			sap.Layer2Text = "";
			sap.Layer4Text = "";
		}
	}
	catch (Exception ex)
	{
		throw new ApplicationException(string.Format("Could not load signature: {0} ({1})", Certificate.ImagePath, ex.Message), ex);
	}
	finally
	{
		if (sImageStream != null)
		{
			sImageStream.Close();
			sImageStream.Dispose();
			sImageStream = null;
		}
	}
}

Finally, I closed the PdfStamper (which updated the document and closed the stream, thus the need for my extended memory stream) and returned the resulting byte[]:

stamper.Close();
	}

	return bSignedContent;
}

This was then saved into an appropriate place in SharePoint.

All in all, it was an interesting foray into the world of custom workflow actions, PDF generation and document signing. Not too bad really.

About me

I am a software developer in the UK, and specialise in Microsoft technologies.

Starting as a web developer working on the British Airways website at Leighton, I progressed to .net development at Aspire Technology Solutions.  A lot of my work has involved SharePoint, and I have implemented numerous solutions using the platform.  Recently I have been involved in a large-scale enterprise application utilising WCF services, and also a large BI databse using SQL Server technologies.

Please download my CV.

MCTS

Month List