PDF watermark/background Rendering Extension for SSRS – Part 2

Updated 2011-02-22: the underlying Stream is closed when PdfWriter.Close() is called, so the code for the PdfHandler.AddBackgroundPdf() method is updated (line 79 and further).

Well, I didn’t think this follow-up to Part 1 would come this quick, but here it is! Again I would like to emphasize that Jan Hoefnagels is the one who should take credit for this solution, but he isn’t the blog-writing type of guy. He’s not even a “Getting Things Done” guy. He’s a “Making Things Work” guy. And he’s pretty good at that.

Anyway, Part 1 showed you a general solution for an implementation of a Rendering Extension for SQL Server Reporting Services that was able to utilize the built-in (Microsoft provided) PDF Renderer, while enabling you to get the rendered PDF and do “something” with it, before sending it back to the SSRS server, which would subsequently send it to the end-user as a downloadable PDF file. That was a long sentence by English standards, but by Dutch standards, that’s kind of like a normal length. In German however, you would just be getting started. This whole article could be one sentence in German and still leave room for more.

Stay focussed. Now this “something” would be: adding a so-called watermark or background to the PDF of the rendered result. And this background would be another PDF file, like the corporate letter design. This would retain e.g. embedded fonts and vector-based images, like the company logo. Yes, we’re those guys that are anal about quality, and bitmap just doesn’t cut it for us.

What has changed since the last post is the need to make the solution as dependency-free as possible. I don’t dare to use the words “Inversion of Control”, since I haven’t got the slightest idea what they mean, but I just did. It was more like: how can we create the Rendering Extension in such a way that we can set the PDF used as a background without rebuilding the extension? So it’s more about configurability. Maybe some IoC as well. What do I know…

We cooked up two points of entry for configuration and I’m sure there are more to find:

OK, you might ask, so where do I put the PDF with the background design? Well, just add it to the Report Server using the Report Manager (hint: http://<reportserver:port>/Reports )!
Because the Report class has a helper function public bool GetResource(string resourcePath, out byte[] resource, out string mimeType), you are able to retrieve a resource on the report server, using a resourcePath that can either be relative to the report, or absolute to the report server. When you’re using the Report Manager, you can see the path to the current folder or file in the URL, it’s the itemPath parameter. It’s UrlEncoded, by the way, so those %2F are actually forward slashes /.

I could go on for hours, but let me just give you the updated code. It’s using ITextSharp, based on IText, which is an open-source PDF library, but not necessarily free. So much for IoC by the way. Ah well, this was merely a “proof of concept”, right?

using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.ReportingServices.OnDemandReportRendering;
using ImageRenderer = Microsoft.ReportingServices.Rendering.ImageRenderer;
using Interfaces = Microsoft.ReportingServices.Interfaces;

namespace Broes.ReportRendering.PDF
{
    
    // The "PDF with background PDF renderer" for SQL Server Reporting Services.
    public class PdfWithBackgroundRenderer : IRenderingExtension
    {
        // The built-in PDF renderer we're gonna use later.
        private ImageRenderer.PDFRenderer pdfRenderer;

        // Stream to capture the result of the PDF renderer later.
        private Stream intermediateStream;

        // Fields for maintaining state.
        private string _name;
        private string _extension;
        private Encoding _encoding;
        private string _mimeType;
        private bool _willSeek;
        private Interfaces.StreamOper _operation;

        // Default constructor
        public PdfWithBackgroundRenderer()
        {
            // Initialize the PDF renderer.
            pdfRenderer = new ImageRenderer.PDFRenderer();
        }

        // Intermediate CreateAndRegisterStream method that matches the delegate
        // Microsoft.ReportingServices.Interfaces.CreateAndRegisterStream
        // It will return a reference to a new MemoryStream, so we can get to
        // the results of the intermediate render-step later.
        public Stream IntermediateCreateAndRegisterStream(
            string name,
            string extension,
            Encoding encoding,
            string mimeType,
            bool willSeek,
            Interfaces.StreamOper operation)
        {
            _name = name;
            _encoding = encoding;
            _extension = extension;
            _mimeType = mimeType;
            _operation = operation;
            _willSeek = willSeek;

            // Create and return a new MemoryStream,
            // which will contain the results of the PDF renderer.
            intermediateStream = new MemoryStream();
            return intermediateStream;
        }

        // This method will be called by Reporting Services
        // when it executes a request to render a report
        // using this renderer.
        public bool Render(Report report,
            NameValueCollection reportServerParameters,
            NameValueCollection deviceInfo,
            NameValueCollection clientCapabilities,
            ref Hashtable renderProperties,
            Interfaces.CreateAndRegisterStream createAndRegisterStream)
        {
            // Let the built-in PDF renderer do the hard work!
            // After this call, the rendered PDF will be in the intermediateStream.
            // We're just passing-through the Render parameters.
            pdfRenderer.Render(
                report,
                reportServerParameters,
                deviceInfo,
                clientCapabilities,
                ref renderProperties,
                // This is the tricky part: get a delegate method to send a stream to the
                // PDF renderer, while keeping a reference to the same stream.
                // (See the IntermediateCreateAndRegisterStream method above).
                new Interfaces.CreateAndRegisterStream(IntermediateCreateAndRegisterStream)
            );

            // This is the actual Stream which Reporting Services uses
            // to send the result to the end-user.
            Stream outputStream =
                createAndRegisterStream(_name, _extension, _encoding, _mimeType, _willSeek, _operation);

            // It took us some time to figure out why the intermediateStream,
            // while having a length, always returned an empty result upon
            // reading from it. Well, after writing to the MemoryStream,
            // the PDF renderer doesn't reset the stream's position, so
            // we have to.            
            intermediateStream.Position = 0;

            // A byte buffer for the background PDF.
            byte[] backgroundContent;
            // This will contain the mime-type of the background PDF.
            string backgroundType;

            // To be able to configure which background PDF to
            // use from the report designer, you could use a report parameter
            // like 'BackgroundPdfPath'. You can then store the PDF on the
            // Report Server itself!
            // The path to the PDF should be relative to the report,
            // e.g. "../Background.pdf" or absolute to the server,
            // e.g. "/Sales/Quote/Background.pdf".
            ReportParameter pdfPath = null;
            try
            {
                pdfPath = report.Parameters["BackgroundPdfPath"];
            }
            catch
            { }

            // Check if there actually was a path to the background PDF.
            if ( 
                pdfPath != null // ReportParameter is not null
                && pdfPath.Instance != null // ... and its instance is not null
                && !string.IsNullOrEmpty((string)pdfPath.Instance.Value) // ... and its value is not null
                // The GetResource method of a Report is able to get any resource
                // available on the Report Server a relative or absolute path.
                // The contents of the resource are in the 'backgroundContent' byte buffer,
                // while the mime-type is in the 'backgroundType' string.
                && report.GetResource((string)pdfPath.Instance.Value, out backgroundContent, out backgroundType))
            {
                // Pass the contents of the resource to the AddBackgroundPdf helper
                // method.
                // Notice that we just assume it is a PDF. Wanna be more certain?
                // Check the mime-type in the 'backgroundType' string.
                intermediateStream = PdfHandler.AddBackgroundPdf(intermediateStream, new MemoryStream(backgroundContent));
            }
            // There was no path to a background PDF in the report
            // parameters, so we fall back to the deviceInfo.
            // DeviceInfo is configurable in the RSReportServer.config file, see
            // http://msdn.microsoft.com/en-us/library/ms156281.aspx
            // This way, you could set a default background PDF if it isn't
            // specified in the report parameters. Configurability FTW!
            else if (
                deviceInfo.AllKeys.Contains("BackgroundPdfResourcePath")
                && !string.IsNullOrEmpty(deviceInfo["BackgroundPdfResourcePath"])
                && report.GetResource(deviceInfo["BackgroundPdfResourcePath"], out backgroundContent, out backgroundType))
            {
                intermediateStream = PdfHandler.AddBackgroundPdf(intermediateStream, new MemoryStream(backgroundContent));
            }

            // A buffer for copying the intermediateStream to the outputStream
            // http://stackoverflow.com/questions/230128/best-way-to-copy-between-two-stream-instances-c
            byte[] buffer = new byte[32768];

            // Do the actual copying.
            while (true)
            {
                int read = intermediateStream.Read(buffer, 0, buffer.Length);
                if (read <= 0) break;
                outputStream.Write(buffer, 0, read);
            }

            // Be nice and release some hard-needed resources.
            intermediateStream.Close();
            intermediateStream = null;

            // Return false, because:
            // "A return value of true indicates that any properties added
            // to the report object model are saved into the intermediate format."
            // http://msdn.microsoft.com/en-us/library/microsoft.reportingservices.reportrendering.irenderingextension.render(SQL.90).aspx
            // ... and we're obviously not doing that, are we? Are we? ARE WE?
            return false;
        }

        public bool RenderStream(
            string streamName,
            Report report,
            NameValueCollection reportServerParameters,
            NameValueCollection deviceInfo,
            NameValueCollection clientCapabilities,
            ref Hashtable renderProperties,
            Interfaces.CreateAndRegisterStream createAndRegisterStream)
        {
            // We'll implement this "later". No, seriously.
            return false;
        }

        public string LocalizedName
        {
            // Just say what it is. In English. That's one localization...
            get { return "PDF with background"; }
        }

        public void SetConfiguration(string configuration)
        {
            // Did I tell you this is another place
            // where you could receive configuration options?
            // Check out:
            // http://msdn.microsoft.com/en-us/library/microsoft.reportingservices.interfaces.iextension.setconfiguration.aspx
        }

        public void GetRenderingResource(
            Interfaces.CreateAndRegisterStream createAndRegisterStreamCallback,
            NameValueCollection deviceInfo)
        {
            // We'll implement this "later" as well. Still don't believe me?
            // Then you've got some serious trust issues!
        }
    }
}

And of course the PdfHandler class referenced above.

using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace Broes.ReportRendering.PDF
{
    // Utility class to handle PDF's, obviously.
    // Uses the open source component iTextSharp:
    // http://itextsharp.com/
    public static class PdfHandler
    {
        // One method to rule them all. Since it's the only one, that is.
        public static Stream AddBackgroundPdf(Stream existingDocumentStream, Stream backgroundStream)
        {
            // This stream will hold the new (merged) document
            MemoryStream documentWithBackground = new MemoryStream();

            // Initialize a new PDF document
            Document newDocument = new Document();

            // Initialize a new PDF stream writer
            PdfWriter writer = PdfWriter.GetInstance(newDocument, documentWithBackground);

            // Initialize the background PDF stream reader
            PdfReader backgroundReader = new PdfReader(backgroundStream);

            // Initialize the existing PDF stream reader
            PdfReader reader = new PdfReader(existingDocumentStream);

            // Set the page size of the merged document to the
            // size of the first page of the existing document.
            newDocument.SetPageSize(reader.GetPageSize(1));

            // Opening the document allows you to start writing content to it,
            // but you will no longer be able to add header- or meta-information.
            newDocument.Open();

            // Get access to the contents of the writer
            PdfContentByte newContent = writer.DirectContent;

            // Get the y-offset of the background PDF with regard to the
            // existing PDF, considering its page size.
            // PDF always starts at the bottom left, and we want the 
            // background to always start at the top left.
            float yDistance = 0;
            if (backgroundReader.GetPageSize(1).Height > reader.GetPageSize(1).Height)
            {
                yDistance = reader.GetPageSize(1).Height - backgroundReader.GetPageSize(1).Height;
            }

            // Clean up some resources.
            backgroundReader.Close();

            // Only import the first page from the background PDF
            PdfImportedPage backgroundPage = writer.GetImportedPage(backgroundReader, 1);

            // Merge each page of the existing PDF with the background PDF
            for (int i = 1; i <= reader.NumberOfPages; i++)
            {
                // Create the page
                newDocument.NewPage();
                // Add the contents of the first page of the background PDF to
                // the current page, using the y-offset.
                newContent.AddTemplate(backgroundPage, 0, yDistance);
                // Import the current page of the existing PDF.
                PdfImportedPage existingPage = writer.GetImportedPage(reader, i);
                // Add the contents of the existingPage to the current page.
                newContent.AddTemplate(existingPage, 0, 0);
            }

            // Clean up some resources.
            newDocument.Close();
            reader.Close();

            // Close the writer to properly finish the PDF document.
            // http://api.itextpdf.com/com/itextpdf/text/pdf/PdfWriter.html#close()
            writer.Close();

            // UPDATE 2011-02-22:
            // The following doesn't work, as the underlying Stream also
            // closes when you call PdfWriter.Close().
            // -- Reset the position of the MemoryStream,
            // so the next consumer will find it ready to go.
            // documentWithBackground.Position = 0; --

            // UPDATE 2011-02-22:
            // After closing the MemoryStream, it is still
            // possible to copy the contents of its underlying
            // buffer using ToArray() to a new Stream.
            return new MemoryStream(documentWithBackground.ToArray());
        }
    }
}

And that kind of wraps it up. When you want to roll this into your own Rendering Extension, don’t forget to “Strong Name” your assembly.
You’ll need the assembly’s “Public Key Blob”, which you can find by running (from the Visual Studio Command Prompt) sn -Tp [pathToAssembly]\[assemblyFileName]. This post explains how you can roll the command into the VS Tools menu.
After that, you need to update two config files: rssrvpolicy.config and rsreportserver.config. Both are located under (by default) C:\Program Files\Microsoft SQL Server\MSRS10.MSSQLSERVER\Reporting Services\ReportServer

The least hard is editing the rsreportserver.config file, and this is pretty well-documented on this page.

Editing the rssrvpolicy.config file however is a pain in the @$ß. Again, it is “documented” right here (seriously, I feel so dumb reading stuff like that). Let’s just look at an example entry:

<CodeGroup class="UnionCodeGroup" version="1" PermissionSetName="FullTrust" Name="SharePoint_Server_Strong_Name" Description="This code group grants SharePoint Server code full trust. ">
    <IMembershipCondition class="StrongNameMembershipCondition" version="1" PublicKeyBlob="0024000004800000940000000602000000240000525341310004000001000100AFD4A0E7724151D5DD52CB23A30DED7C0091CC01CFE94B2BCD85B3F4EEE3C4D8F6417BFF763763A996D6B2DFC1E7C29BCFB8299779DF8785CDE2C168CEEE480E570725F2468E782A9C2401302CF6DC17E119118ED2011937BAE9698357AD21E8B6DFB40475D16E87EB03C744A5D32899A0DBC596A6B2CFA1E509BE5FBD09FACF" />
</CodeGroup>

I would recommend just mimicking such an entry. Make up a name and description and copy the PublicKeyBlob you got from the sn -Tp command referenced above.

Finally, copy your DLL (and PDB?) as well as iTextPdf’s DLL to the Reporting Services’ Report Server’s bin directory, which by default is located at C:\Program Files\Microsoft SQL Server\MSRS10.MSSQLSERVER\Reporting Services\ReportServer\bin.

Update 2011-02-23: If you want to debug the assembly in Visual Studio 2010, you need to follow these instructions.

Don’t forget to restart the “SQL Server Reporting Services (INSTANCENAME)” under “Services”!

Keep a close eye on the Application Log in the Event Viewer. When something’s wrong, SSRS will start whining right there.

And that’s it! As you can see, sentences got a whole lot shorter while nearing the end of this post. I guess the sentence-length kind of reflects my state of mind, which now screams “exhaustion”! Good night (and yes, I realize that the chances someone else is reading this right before going to bed are rather slim)!

7 thoughts on “PDF watermark/background Rendering Extension for SSRS – Part 2”

  1. Thank you for sharing this article. I am in the same boat where I have to put out a watermark on my SSRS report. I have a few questions. I really appreciate your help in this regard. 1) Do you know if this will work on SQL 2005 reports server? 2) Did you use the freeware version of ITextSharp? 3) If so, on the rendered PDF, does it contain any matter that is not part of your data? It is it possible for you to share a sample of an output that was generated using this code? - Thanks, Aarthi

  2. Can something similar be done with rendering local reports in a Winforms application? I need to add a watermark when rendering local report to PDF.

  3. Thanks for this excellent two-part series. I wanted to do something similar try and write an extension myself and to write an answer on Stack Overflow on a custom CSV renderer wrapping an existing one. I've found these posts invaluable!

    If anyone's interested my version of such a "wrapper renderer" can be found here:
    http://stackoverflow.com/a/13057837/419956

  4. Thanks for Article. I have similar requirement to render excel and create chart run time. While execute the code I get the error "Cannot access close stream" at line " intermediateStream.Position = 0;" of "Render" method.

    Please help me.

  5. Is there a way of creating a custom rendering extension to convert a report into an SVG image?

Leave a Reply

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