Categories
Uncategorized

Printing SSRS Reports with XPS

Printing in .NET can present some technological challenges. This is particularly true when you are trying to print from the context of a Windows service or an ASP .NET application or service. Now throw SQL Server Reporting Services (SSRS) into the mix and you have a real debacle. Fortunately there is a reliable solution using the XPS Printing API.

I found several helpful examples on the internet but not exactly what I needed for printing SSRS reports via XPS. Thanks to Aspose for their XPS server side printing article and this MSDN post describing how to add images to XPS documents. The information was very helpful in getting started.

There were several challenges I faced in accomplishing my goal. The first was that much of the documentation I found online for XPS presented solutions for creating a physical XPS file on disk. I needed to generate and print the document without creating any physical file. Another challenge was getting the page content from SSRS.

The SSRS ReportExecution2005.Render method can be used to render SSRS reports programmatically. In versions previous to SSRS 2008R2 when rendering to an image, the StreamIds out parameters provided the required IDs to render each page as a stream. Many examples on the internet use this method of rendering the pages of a report programmatically. Unfortunately this does not work in newer versions of SSRS. I came across an article on CodePlex that provided a solution to this issue. Now I will jump into the code.

The prerequisites for this sample are Visual Studio 2010 or later, Visual Studio 2008 SP1 / Business Intelligence Development Studio (BIDS) 2008R2, AdventureWorks 2008R2 database, and AdventureWorks Sample Reports 2008R2. You should use VS 2008 SP1 / BIDS to deploy the datasource and the Product Catalog SQL2008R2 report to your report server. You can also manually deploy the RDL file and create a datasource using the SSRS Report Manager. Note you may need to manually edit the RDL file to provide the appropriate SQL Server connection string which matches your shared datasource. I created three static utility classes to demonstrate the three steps used to retrieve and print the reports. The entire sample project can be downloaded here.

The first step in the process is to get a collection of streams from SSRS that represent the pages in the reports. This is accomplished from the code below from sending an HTTP request to SSRS. There are a couple of key items here. One is the use of the rs:PersistStreams=True / rs:GetNextStream=True query parameter. These parameters tell reporting services to use persistent streams for the first and subsequent pages of the report. Each stream contains the image data for a single page. The second thing to take note of is rc:OutputFormat=png parameter. This parameter tell SSRS to render the pages as png images.

        public static IEnumerable<Stream> GetReportStreams(string reportServerUrl, string reportPath, string snapshotId = null)
        {
            var reportStreams = new List<Stream>();
            var cookies = new CookieContainer();
            string requestUri;
            if (!string.IsNullOrEmpty(snapshotId))
            {
                requestUri =
                    string.Format(
                        "{0}?{1}&rs:SnapshotId={2}&rs:Command=Render&rs:Format=IMAGE&rs:PersistStreams=True&rc:OutputFormat=png",
                        reportServerUrl, reportPath, snapshotId);
            }
            else
            {
                requestUri =
                    string.Format(
                        "{0}?{1}&rs:Command=Render&rs:Format=IMAGE&rs:PersistStreams=True&rc:OutputFormat=png",
                        reportServerUrl, reportPath);
            }
            var request = WebRequest.Create(requestUri);
            ((HttpWebRequest) request).CookieContainer = cookies;

            while (true)
            {
                //Use the currently logged in user
                request.UseDefaultCredentials = true;

                //Get the web response for the request object.
                var response = request.GetResponse();

                //Read the stream.
                var memoryStream = new MemoryStream();
                var byteCount = 0;
                var bytes = new byte[1024];
                var responseStream = response.GetResponseStream();
                do
                {
                    if (responseStream != null)
                        byteCount = responseStream.Read(bytes, 0, bytes.Length);
                    memoryStream.Write(bytes, 0, byteCount);
                } while (byteCount > 0);

                //Reset the memory stream position to the beginning
                memoryStream.Seek(0, SeekOrigin.Begin);

                //An empty stream signals the end of the pages so break out of the while loop
                if (memoryStream.Length == 0)
                {
                    break;
                }

                //Add the current pages to the collection of page streams
                reportStreams.Add(memoryStream);

                //Request the next page.
                if (!string.IsNullOrEmpty(snapshotId))
                {
                    requestUri =
                        string.Format(
                            "{0}?{1}&rs:SnapshotId={2}&rs:Command=Render&rs:Format=IMAGE&rs:GetNextStream=True&rc:OutputFormat=png",
                            reportServerUrl, reportPath, snapshotId);
                }
                else
                {
                    requestUri =
                        string.Format(
                            "{0}?{1}&rs:Command=Render&rs:Format=IMAGE&rs:GetNextStream=True&rc:OutputFormat=png",
                            reportServerUrl, reportPath);
                }
                request = WebRequest.Create(requestUri);
                ((HttpWebRequest) request).CookieContainer = cookies;
            }
            return reportStreams;
        }

 

The second step of the process is to create an XPS document as a memory stream and insert the images as individual pages. A couple of things worth noting here are that the image size is retrieved from the data. This determines the page size. Reports could be designed for 8.5 x 11 or for some other paper size like A4. The document itself is constructed in markup according to the XPS specifications. I found the following code as the minimal amount needed to accomplish my goals. Note the specification defines specific extensible Application Markup Language (XAML) constructs and appropriate attributes required to construct the XPS document.

        internal static void SendToXpsPrinter(IEnumerable<Stream> streams, string printerName, string printJobName)
        {
            using (var memoryStream = new MemoryStream())
            {
                var documentUri = new Uri("pack://document.xps");
                var streamPackage = Package.Open(memoryStream, FileMode.Create, FileAccess.ReadWrite);
                PackageStore.AddPackage(documentUri, streamPackage);

                var xpsDoc = new XpsDocument(streamPackage, CompressionOption.NotCompressed, documentUri.AbsoluteUri);
                var docSeqWriter = xpsDoc.AddFixedDocumentSequence();
                var docWriter = docSeqWriter.AddFixedDocument();
                foreach (var stream in streams)
                {
                    var pageWriter = docWriter.AddFixedPage();
                    var imageUri = AddImage(pageWriter, stream);

                    //Calculate image size for the png to determine page size
                    var imageSizeBuffer = new byte[32];
                    stream.Read(imageSizeBuffer, 0, 32);
                    const int WOffset = 16;
                    const int HOffset = 20;
                    var width =
                        Convert.ToDouble(
                            BitConverter.ToInt32(
                                new[]
                                    {
                                        imageSizeBuffer[WOffset + 3], imageSizeBuffer[WOffset + 2],
                                        imageSizeBuffer[WOffset + 1], imageSizeBuffer[WOffset + 0]
                                    }, 0));
                    var height =
                        Convert.ToDouble(
                            BitConverter.ToInt32(
                                new[]
                                    {
                                        imageSizeBuffer[HOffset + 3], imageSizeBuffer[HOffset + 2],
                                        imageSizeBuffer[HOffset + 1], imageSizeBuffer[HOffset + 0]
                                    }, 0));

                    //Write the XAML
                    var xmlWriter = pageWriter.XmlWriter;
                    xmlWriter.WriteStartElement("FixedPage");
                    xmlWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/xps/2005/06");
                    xmlWriter.WriteAttributeString("xmlns:x",
                                                    "http://schemas.microsoft.com/xps/2005/06/resourcedictionary-key");
                    xmlWriter.WriteAttributeString("xml:lang", "en-US");
                    xmlWriter.WriteAttributeString("Height", height.ToString(CultureInfo.InvariantCulture));
                    xmlWriter.WriteAttributeString("Width", width.ToString(CultureInfo.InvariantCulture));
                    xmlWriter.WriteStartElement("Canvas");
                    xmlWriter.WriteStartElement("Path");
                    xmlWriter.WriteAttributeString("Data",
                        string.Format("M 0,0 L {0},0 {1},{2} 0,{3} z",
                            width.ToString(CultureInfo.InvariantCulture),
                            width.ToString(CultureInfo.InvariantCulture),
                            height.ToString(CultureInfo.InvariantCulture),
                            height.ToString(CultureInfo.InvariantCulture)));
                    xmlWriter.WriteStartElement("Path.Fill");
                    xmlWriter.WriteStartElement("ImageBrush");
                    xmlWriter.WriteAttributeString("ViewboxUnits", "Absolute");
                    xmlWriter.WriteAttributeString("ViewportUnits", "Absolute");
                    xmlWriter.WriteAttributeString("Viewbox",
                        string.Format("0,0,{0},{1}",
                            width.ToString(CultureInfo.InvariantCulture),
                            height.ToString(CultureInfo.InvariantCulture)));
                    xmlWriter.WriteAttributeString("Viewport",
                        string.Format("0,0,{0},{1}",
                            width.ToString(CultureInfo.InvariantCulture),
                            height.ToString(CultureInfo.InvariantCulture)));
                    xmlWriter.WriteAttributeString("ImageSource", imageUri);
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteEndElement();
                    pageWriter.Commit();
                }
                //Add any print tickets to both the page and the document sequence
                var ticket = new PrintTicket {PageOrientation = PageOrientation.Portrait};
                docWriter.PrintTicket = ticket;
                docWriter.Commit();
                docSeqWriter.PrintTicket = ticket;
                docSeqWriter.Commit();
                xpsDoc.Close();

                //Reset the stream to the beginning
                memoryStream.Position = 0;
                XpsPrint.Print(memoryStream, printerName, printJobName, true);
            }
        }

        private static string AddImage(IXpsFixedPageWriter pageWriter, Stream imageStream)
        {
            var image = pageWriter.AddImage(XpsImageType.PngImageType);
            CopyStream(imageStream, image.GetStream());
            image.Commit();
            return image.Uri.ToString();
        }

        private static void CopyStream(Stream sourceStream, Stream destinationStream)
        {
            const int BufferSize = 1024*1024;
            var buffer = new byte[BufferSize]; //1MB
            var bytesRemaining = sourceStream.Length;
            var writeDpi = false;
            while (bytesRemaining > 0)
            {
                var copyLength = (bytesRemaining > BufferSize) ? BufferSize : bytesRemaining;
                sourceStream.Read(buffer, 0, (int) copyLength);
                if (!writeDpi && bytesRemaining > 76)
                {
                    writeDpi = true;
                    buffer[70] = buffer[74] = 0x00;
                    buffer[71] = buffer[75] = 0x00;
                    buffer[72] = buffer[76] = 0x0E;
                    buffer[73] = buffer[77] = 0xC3;
                }
                destinationStream.Write(buffer, 0, (int) copyLength);
                bytesRemaining -= copyLength;
            }
            sourceStream.Position = 0;
        }

 

I replace the Physical pixel dimensions (pHYs chunk) in the CopyStream method. Updating this value to 3779 (0x0EC3 hex) for both the X and Y axis values ensures the image is rendered at 96 dots per inch (DPI) resolution. The unit specifier (byte[78]) of 1 signifies meters. If meters is specified (which it is) then a conversion factor of 0.0254 meters per inch is used to calculate the value. The calculation works out to 96 DPI / 0.0254 = 3779.

                    buffer[70] = buffer[74] = 0x00;
                    buffer[71] = buffer[75] = 0x00;
                    buffer[72] = buffer[76] = 0x0E;
                    buffer[73] = buffer[77] = 0xC3;

 

 

The final step of the process is to print the document through the XPS Print API. A few .NET methods act as calling wrappers for the unmanaged API calls via PInvoke.

        public static void Print(Stream stream, string printerName, string jobName, bool isWait)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }
            if (printerName == null)
            {
                throw new ArgumentNullException("printerName");
            }

            //Create an event that we will wait on until the job is complete.
            var completionEvent = CreateEvent(IntPtr.Zero, true, false, null);
            if (completionEvent == IntPtr.Zero)
            {
                throw new Win32Exception();
            }

            try
            {
                IXpsPrintJob job;
                IXpsPrintJobStream jobStream;
                StartJob(printerName, jobName, completionEvent, out job, out jobStream);

                CopyJob(stream, job, jobStream);

                if (isWait)
                {
                    WaitForJob(completionEvent);
                    CheckJobStatus(job);
                }
            }
            finally
            {
                if (completionEvent != IntPtr.Zero)
                {
                    CloseHandle(completionEvent);
                }
            }
        }

        private static void StartJob(string printerName, string jobName, IntPtr completionEvent, out IXpsPrintJob job,
                                     out IXpsPrintJobStream jobStream)
        {
            var result = StartXpsPrintJob(printerName, jobName, null, IntPtr.Zero, completionEvent,
                                          null, 0, out job, out jobStream, IntPtr.Zero);
            if (result != 0)
            {
                throw new Win32Exception(result);
            }
        }

        private static void CopyJob(Stream stream, IXpsPrintJob job, IXpsPrintJobStream jobStream)
        {
            try
            {
                var buff = new byte[4096];
                while (true)
                {
                    var read = (uint) stream.Read(buff, 0, buff.Length);
                    if (read == 0)
                    {
                        break;
                    }

                    uint written;
                    jobStream.Write(buff, read, out written);

                    if (read != written)
                    {
                        throw new Exception("Failed to copy data to the print job stream.");
                    }
                }

                //Indicate that the entire document has been copied.
                jobStream.Close();
            }
            catch (Exception)
            {
                //Cancel the job if we had any trouble submitting it.
                job.Cancel();
                throw;
            }
        }

        private static void WaitForJob(IntPtr completionEvent)
        {
            const int INFINITE = -1;
            switch (WaitForSingleObject(completionEvent, INFINITE))
            {
                case WAIT_RESULT.WAIT_OBJECT_0:
                    //Expected result, do nothing.
                    break;
                case WAIT_RESULT.WAIT_FAILED:
                    throw new Win32Exception();
                default:
                    throw new Exception("Unexpected result when waiting for the print job.");
            }
        }

        private static void CheckJobStatus(IXpsPrintJob job)
        {
            XPS_JOB_STATUS jobStatus;
            job.GetJobStatus(out jobStatus);
            switch (jobStatus.completion)
            {
                case XPS_JOB_COMPLETION.XPS_JOB_COMPLETED:
                    //Expected result, do nothing.
                    break;
                case XPS_JOB_COMPLETION.XPS_JOB_FAILED:
                    throw new Win32Exception(jobStatus.jobStatus);
                default:
                    throw new Exception("Unexpected print job status.");
            }
        }

        [DllImport("XpsPrint.dll", EntryPoint = "StartXpsPrintJob")]
        private static extern int StartXpsPrintJob(
            [MarshalAs(UnmanagedType.LPWStr)] String printerName,
            [MarshalAs(UnmanagedType.LPWStr)] String jobName,
            [MarshalAs(UnmanagedType.LPWStr)] String outputFileName,
            IntPtr progressEvent, // HANDLE
            IntPtr completionEvent, // HANDLE
            [MarshalAs(UnmanagedType.LPArray)] byte[] printablePagesOn,
            UInt32 printablePagesOnCount,
            out IXpsPrintJob xpsPrintJob,
            out IXpsPrintJobStream documentStream,
            IntPtr printTicketStream);

        // This is actually "out IXpsPrintJobStream", but we don't use it and just want to pass null, hence IntPtr.

        [DllImport("Kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName);

        [DllImport("Kernel32.dll", SetLastError = true, ExactSpelling = true)]
        private static extern WAIT_RESULT WaitForSingleObject(IntPtr handle, Int32 milliseconds);

        [DllImport("Kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr hObject);
    }

    [Guid("0C733A30-2A1C-11CE-ADE5-00AA0044773D")] // This is IID of ISequenatialSteam.
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsPrintJobStream
    {
        // ISequentualStream methods.
        void Read([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbRead);
        void Write([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbWritten);
        // IXpsPrintJobStream methods.
        void Close();
    }

    [Guid("5ab89b06-8194-425f-ab3b-d7a96e350161")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsPrintJob
    {
        void Cancel();
        void GetJobStatus(out XPS_JOB_STATUS jobStatus);
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct XPS_JOB_STATUS
    {
        public UInt32 jobId;
        public Int32 currentDocument;
        public Int32 currentPage;
        public Int32 currentPageTotal;
        public XPS_JOB_COMPLETION completion;
        public Int32 jobStatus; // UInt32
    };

    internal enum XPS_JOB_COMPLETION
    {
        XPS_JOB_IN_PROGRESS = 0,
        XPS_JOB_COMPLETED = 1,
        XPS_JOB_CANCELLED = 2,
        XPS_JOB_FAILED = 3
    }

    internal enum WAIT_RESULT
    {
        WAIT_OBJECT_0 = 0,
        WAIT_ABANDONED = 0x80,
        WAIT_TIMEOUT = 0x102,
        WAIT_FAILED = -1 // 0xFFFFFFFF
    }
        XPS_JOB_COMPLETED = 1,
        XPS_JOB_CANCELLED = 2,
        XPS_JOB_FAILED = 3
    }

    internal enum WAIT_RESULT
    {
        WAIT_OBJECT_0 = 0,
        WAIT_ABANDONED = 0x80,
        WAIT_TIMEOUT = 0x102,
        WAIT_FAILED = -1 // 0xFFFFFFFF
    }

 

Overall this was a great learning experience. XPS printing allows for a unified client / server printing solution when dealing with Windows services and ASP .NET applications and services.