“Stamping” PDF Files Downloaded from SharePoint 2010

Posted on Posted in Article, SharePoint 2010

First off I want to clarify that the subject of this post is not my idea as it is something that my friend Roman Kobzarev put together for his company and I merely assisted with getting the code to work. The problem that Roman was trying to solve was that his company provided numerous PDF files that registered/paying members could download and, unfortunately, they were finding some of those files being posted to various other sites without their permission; so in an attempt to at least discourage this they wanted to stamp the PDF files with some information about the user who downloaded the file.

There are various ways in which this problem can be solved but perhaps one of the simpler approaches, and the approach outlined here, was to create an HTTP Handler which intercepted requests for any PDF files and then simply retrieve the file from SharePoint, modify it, and then send it on its way. The cool thing about this approach and the pattern shown here is that it can easily be applied to any file type which requires some user or request specific modifications applied to it.

Before I get to the actual code I want to point out one third party dependency that we used: iTextSharp. This is an open source .NET library for PDF generation and manipulation. There are many options available when looking to manipulate PDF files and in this case this one was chosen due to its cost (free). So let’s get to the code.

In terms of code there really isn’t that much which is what makes this such a nice solution. The first piece is the actual implementation of the IHttpHandler interface:

using System.IO;
using System.Web;
using Microsoft.SharePoint;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace Aptillon.SharePoint.PDFWatermark
{
    public class PDFWatermarkHttpHandler : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {

            SPFile file = SPContext.Current.Web.GetFile(context.Request.Url.ToString());
            byte[] content = file.OpenBinary();
            SPUser currentUser = SPContext.Current.Web.CurrentUser;

            string watermark = null;
            if (currentUser != null)
                watermark = "This download was specially prepared for " + currentUser.Name;

            if (watermark != null)
            {
                PdfReader pdfReader = new PdfReader(content);
                using (MemoryStream outputStream = new MemoryStream())
                using (PdfStamper pdfStamper = new PdfStamper(pdfReader, outputStream))
                {
                    for (int pageIndex = 1; pageIndex <= pdfReader.NumberOfPages; pageIndex++)
                    {
                        //Rectangle class in iText represent geometric representation... 
                        //in this case, rectangle object would contain page geometry
                        Rectangle pageRectangle = pdfReader.GetPageSizeWithRotation(pageIndex);
                        //PdfContentByte object contains graphics and text content of page returned by PdfStamper
                        PdfContentByte pdfData = pdfStamper.GetUnderContent(pageIndex);
                        //create font size for watermark
                        pdfData.SetFontAndSize(BaseFont.CreateFont(BaseFont.HELVETICA_BOLD, BaseFont.CP1252, BaseFont.NOT_EMBEDDED), 8);
                        //create new graphics state and assign opacity
                        PdfGState graphicsState = new PdfGState();
                        graphicsState.FillOpacity = 0.4F;
                        //set graphics state to PdfContentByte
                        pdfData.SetGState(graphicsState);
                        //indicates start of writing of text
                        pdfData.BeginText();
                        //show text as per position and rotation
                        pdfData.ShowTextAligned(Element.ALIGN_CENTER, watermark, pageRectangle.Width / 4, pageRectangle.Height / 44, 0);
                        //call endText to invalid font set
                        pdfData.EndText();
                    }
                    pdfStamper.Close();
                    content = outputStream.ToArray();
                }
            }
            context.Response.ContentType = "application/pdf";
            context.Response.BinaryWrite(content);
            context.Response.End();
        }

        public bool IsReusable
        {
            get { return false; }
        }
    }
}

 

As you can see from the previous code listing, the bulk of the code involves the actual processing of the PDF file but the core SharePoint specific piece is in the beginning of the ProcessRequest() method where we use the SPContext.Current.Web.GetFile() method to retrieve the actual file requested and then, if we can get an actual SPUser object, we create a simple message that will be added to the bottom of the PDF. I’m not going to cover what’s happening with the iTextSharp objects as the point of this article is to demonstrate the pattern which can easily be applied to other file types and not how to use iTextSharp.

To deploy this class I created an empty SharePoint 2010 project using Visual Studio 2010 and added the file to the project. I then created a new Web Application scoped Feature which I use to add the appropriate web.config settings which will register the HTTP Handler. The following screenshot shows the final project structure:

SNAGHTML8af78243

Note that I also added the iTextSharp.dll to the project and added it as an additional assembly to the package by double clicking the Package.package element and then, in the package designer, click Advanced to add additional assemblies:

image

Before I show the code for the Web Application Feature I first want to show what settings I set for the Feature after adding it:

image

When you add a new Feature to a project it’s going to name it Feature1 and set the default scope to Web. The first thing I do is rename the Feature to something meaningful – in this case, because I know I’ll only have the one Feature I go ahead and name it the same as the project name: Aptillon.SharePoint.PDFWatermark (I always follow the same naming convention for my projects, which equate to WSP file names and Features: <Company>.SharePoint.<Something Appropriate for the Contained Functionality>). The next thing I do is change the Deployment Path property for the Feature so that it only uses the Feature name and does not prepend the project name; and finally I set the scope and title of the Feature. Now I’m ready to add my Feature activation event receiver.

The code that I want to include in the event receiver will handle the addition and removal of the web.config handler elements. I do this using the SPWebConfigModification class. Now there’s debate on whether this should be used or not; this is one of those classes where you might say (as my friend Spence Harbar puts it), “Just because you should use it doesn’t mean you can.” The simple explanation for this is that ideally you should be using this class to make web.config modifications but the reality is that this guy is fraught with issues and usually doesn’t work. That said, what I usually do is, where it makes sense, use this class to add and remove my entries but work under the premise that it will probably not work and plan on making these changes manually (or write a timer job which will do what this guy is attempting to do, but that’s out of scope of this article). So here’s the FeatureActivated() and FeatureDeactivating() methods that I use to add and remove the appropriate web.config entries which register the previously defined HTTP Handler:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    string asmDetails = typeof(PDFWatermarkHttpHandler).AssemblyQualifiedName;

    SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
    if (webApp == null) return;

    SPWebConfigModification modification = new SPWebConfigModification("add[@name=\"PDFWatermark\"]", "configuration/system.webServer/handlers");
    modification.Value = string.Format("<add name=\"PDFWatermark\" verb=\"*\" path=\"*.pdf\" type=\"{0}\" preCondition=\"integratedMode\" />", asmDetails);

    modification.Sequence = 1;
    modification.Owner = asmDetails;
    modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
    webApp.WebConfigModifications.Add(modification);

    webApp.Update();
    webApp.WebService.ApplyWebConfigModifications();
}


public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    string asmDetails = typeof(PDFWatermarkHttpHandler).AssemblyQualifiedName;

    SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
    if (webApp == null) return;

    List<SPWebConfigModification> configModsFound = new List<SPWebConfigModification>();
    Collection<SPWebConfigModification> modsCollection = webApp.WebConfigModifications;
    for (int i = 0; i < modsCollection.Count; i++)
    {
        if (modsCollection[i].Owner == asmDetails)
        {
            configModsFound.Add(modsCollection[i]);
        }
    }

    if (configModsFound.Count > 0)
    {
        foreach (SPWebConfigModification mod in configModsFound)
            modsCollection.Remove(mod);

        webApp.Update();
        webApp.WebService.ApplyWebConfigModifications();
    }
}

 

With all the code in place I can deploy to my Web Application and now any time a PDF is downloaded I’ll have a nice little message displayed on the bottom of each page. Again, the intent here is to show the simplicity of the pattern and approach – with a little imagination you can easily come up with lots of other uses for this (applying security or password protection to the PDF, adding an image watermark, removing pages based on registration status thus providing “sample” versions, prefilling form fields with user data, adding a version history page, etc.). And of course all this can also be applied to other file types such as the Office files or images (though image handling would take a little more logic to ignore images not coming from document libraries).

16 thoughts on ““Stamping” PDF Files Downloaded from SharePoint 2010

  1. ITextSharp is great for working with PDF files. I used in years ago on a consulting engagement to allow users to select multiple “reports” from a SharePoint doc lib, bundle them together into one PDF, and then download them. Surprised how smooth the development and deployment ended up. Thanks for sharing this code and walkthrough.

  2. Hi,
    Thanks for sharing this valuable code. I have implemented the above functionality and its working on case of downloading the PDF file from document title link but, it not firing the Httphandler on the case of downloading PDF file from Ribbon “Dowload a Copy” or from context menu -> send to -> download a copy. Please help me on this.

    1. That’s because when you use those buttons to open the file it uses the /_layouts/download.aspx file to stream the file to the client so the http hander is never hit. To accomodate this you’d have to create a custom version of the download.aspx file.

  3. Did you run into any issues with copying PDFs to an “open in explorer” window? We work a lot in bulk uploads of PDFs to libraries and the Explorer View has been the preferred method.

  4. Hey Gary, Nice Share.

    I need some advice,

    The strategy is:
    I want to fill in pdf form with custom webpart, and the pdf form placed in SharePoint Doc Library named Certificate, but i have problem that my output stream is not incorrect PDF format.

    below my code:

    protected void downloadCertificate(object sender, EventArgs e)
    {

    SPFile file = SPContext.Current.Web.GetFile(SPContext.Current.Web.Url + (“/Certificate/TemplateCertificate.pdf”));
    byte[] content = file.OpenBinary();

    PdfReader r = new PdfReader(content);
    using (MemoryStream outputStream = new MemoryStream())
    using (PdfStamper ps = new PdfStamper(r, outputStream))
    {
    AcroFields af = ps.AcroFields;
    af.SetField(“tbxName”, uName.ToUpper());
    af.SetField(“tbxDate”, dateAch);
    ps.FormFlattening = true;

    content = outputStream.ToArray();
    ps.Close();
    }

    Response.ContentType = “application/pdf”;
    Response.AppendHeader(
    “Content-Disposition”,
    “attachment;filename=Certificate.pdf”
    );
    Response.BinaryWrite(content);
    r.Close();
    Response.End();
    }

    Thanks In Advance

  5. Hai Gary,

    The problem was solved by me.

    This final code:

    protected void downloadCertificate(object sender, EventArgs e)
    {

    try
    {
    SPFile file = SPContext.Current.Web.GetFile(SPContext.Current.Web.Url + (“/Certificate/TemplateCertificate.pdf”));
    byte[] content = file.OpenBinary();

    PdfReader r = new PdfReader(content);
    using (PdfStamper ps = new PdfStamper(r, Response.OutputStream))
    {
    AcroFields af = ps.AcroFields;
    af.SetField(“tbxName”, uName.ToUpper());
    af.SetField(“tbxDate”, dateAch);
    ps.FormFlattening = true;
    }

    string contentType = “application/x-pdf”;
    string fileName = “Certificate_” + uName.ToUpper();
    string fileType = “pdf”;
    Response.ContentType = contentType;
    Response.AddHeader(“content-disposition”,
    string.Format(“attachment; filename={0}.{1}”,
    fileName, fileType));

    HttpContext.Current.ApplicationInstance.CompleteRequest();
    }
    catch (Exception ex)
    {
    throw ex;
    }
    }

    Thanks

  6. Thanks for the great solution Gary. This is exactly what I was looking for.

    Let me start by saying that I am not a developer and have very limited experience with creating SharePoint solutions, but this seemed pretty straight forward so I wanted to give it a shot. I was able to deploy the solution after adding a couple references to the project and using statements in the event receiver, but a watermark doesn’t get added when a PDF is downloaded.

    Are there any basic steps that were skipped assuming a developer would know them? I checked web.config and can see the pdfwatermark handler was added. Also, I see the folder for the feature, but it only contains 1 file: feature.xml. Should the dll and/or cs file also be in this folder? I kept all the files names the same as your example and the project structure is the same as yours. I’m not sure how to troubleshoot this. Any suggestions?

    Thanks

    1. Never mind. It’s working perfectly. I misread the previous comment. I was downloading the file from the context menu and thought just the download button in the ribbon wasn’t supported. Thanks again for the walkthrough.

  7. I cannot understand the second part of this tutorial, specifically the featureactivated and featuredeactivated.
    Can you provide a detailed version of the second part, especially the references used.

    Thanks,
    Ms. SP

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA

*