While working on a Dynamics CRM implementation for Valx —whose website was made by Jan, Zlatan and me with Umbraco— with my dear friend and “colleague” Jan Hoefnagels, we stumbled upon a technological hurdle.
We promised our client quotes, orders and invoices —as generated by SQL Server Reporting Services, which is neatly integrated into Dynamics CRM— with and without their own letter background, so they could use the same reports for printing on pre-printed paper (no background), as well as send the PDF through email to their clients (with background).
Sounds like a logical, real-world proposition, right? So the only thing we needed to do is create a report that has a background that can be enabled/disabled. SSRS supports background-images, but as we soon found out, the actual DPI (or PPI since it’s a bitmap) of the image is neglected. Image rendering in SSRS is hard-coded at 72 96DPI. Not only that, it will assume the original image is 96DPI, so a 300DPI image will end up being rendered appr. 3.1 times larger than expected!
And even more so, why would I want to include a bitmap image of something I already have laying around as a vector file (namely a PDF)? It will only add up to the file size and decrease it’s quality.
So the search begins, for “something” that would allow us to add a PDF background “on the fly” to a report exported as PDF from the report viewer in Dynamics CRM. As it turned out, lot’s of people searching for it, but no conclusive answer or solution (hence this post).
We wanted it to be as integrated and reusable as possible for ourselves, as well as transparent to the end-user. Enter the world of Rendering Extensions. It’s a typical Microsoft world where the documentation on MSDN lists all classes and -members (the “what”), but fails to explain the “how”. I’m sure it will improve over time (as has been the case with the Dynamics CRM 4.0 documentation), but when you need it NOW, well, look further. By the way, this is what MSDN says about implementing your own rendering extension:
Writing a custom rendering extension is difficult. A rendering extension must typically support all possible combinations of report elements and requires that you implement hundreds of classes, interfaces, methods, and properties.
Gee, thanks for the warning! But I don’t want that! I just want to add a PDF background… I still want to use Microsoft’s PDF rendering extension and then add the background.
Jan is not a guy to say: hey, let’s just leave it at that. He’s more the kind of guy that says: I can do that. If some Microsoft engineer can do it, I can do it. So he did the plumbing to create a rendering extension (implementing IRenderingExtension), registering it in SSRS… and then ground to a halt. He got it so far as to output the Report parameters to a plain text result. And it did show up as an export option in the Dynamics CRM report viewer!
So we sat together and step-by-step came to the following result (I’ll explain afterwards) that will actually… do exactly the same thing as the built-in PDF renderer! But as you will soon see, it allows you to do “something” with the result from the built-in PDF renderer, before sending it to the end-user. Which is kind-of exactly what we want to do.
using System.Collections; using System.Collections.Specialized; using System.IO; using System.Text; using Microsoft.ReportingServices.OnDemandReportRendering; using ImageRenderer = Microsoft.ReportingServices.Rendering.ImageRenderer; using Interfaces = Microsoft.ReportingServices.Interfaces; namespace Broes.ReportRendering.PDF { 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; } 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 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 the streams are copying, think of the // possibilities: this is the place where you would // be able to fire up that external PDF library // and add the PDF background! // But don't think too long, cause' computers are pretty fast // these days and will probably have finished copying this // stream by the time you even got to start reading. 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(); // 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) { // Had there been configuration options, // we would do something with them here. // Like, for instance, the path to the PDF file // you want to use as a background. } 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! } } }
What more is there to explain? If you would use a component like ABCpdf you could use something similar to Read(Stream stream)
to open the intermediateStream
and add a background/watermark.
After you’ve done that, get the component to return the edited PDF as a Stream and copy the contents of this Stream to the outputStream
.
When we’re done implementing this extension for the client, I’ll post a Part 2! How about that for a cliffhanger!
Hi,
I tired using your example to render report in its original format, that too does not work.
The output stream is completely blank.
would like to throw some light on it.
as I am too trying to implement the same thing.
I am currently using sql server 2008 r2 and find that I cannot use this since PDFRendererer is an internal sealed class. I used reflector and found that to be the case. You said you used this code in sql 2008 - I am wondering whether msft sealed that class in a subsequent release. Could you shed some light into it. I am wondering if you ran into that.
thanks
@LRH Well, I don't have a SQL Server 2008 R2 at my disposal, so I can't check that version, but I'm using the DLL named "Microsoft.ReportingServices.ImageRendering.dll" (found in "C:\Program Files\Microsoft SQL Server\MSRS10.MSSQLSERVER\Reporting Services\ReportServer\bin" in my case). The Microsoft.ReportingServices.Rendering.ImageRenderer.PDFRenderer class is "public sealed", but definitely not internal.
I can only confirm it works on a SQL Server 2008 SP2 Reporting Services Server (version 10.0.4000).
Man, would it suck if Microsoft has actually decided to internalize that class in R2.
yes this is the problem,
For sql server 2008 r2
Microsoft.ReportingServices.Rendering.ImageRenderer -
all the class in this namespace has been internalized
and are sealed or abstract classes. 🙂
Is there any other way to address this application?
Still working on the same application and trying to find any other possible solution.
can you please provide working example with test page?
you can email me on <snip>... thanks
Hi dhananjay, the working example can be found under https://www.broes.nl/2011/02/pdf-watermark-background-rendering-extension-for-ssrs-part-2/
I'm sure you'll be able to get it working and produce a test page yourself!
What [event or another method] may call the method ?
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; }
Result = Main Report (fail) + Attachments (many fail)
@andrii, I would recommend looking in the Application Log of the Event Viewer to see what the error messages are. To be honest, I haven't got a clue what would call the RenderStream method, but you could try to just pass it on onto the PdfRenderer. So the body of the method would be something like
Also, don't forget to take a look at the second part of this post for an actual implementation.
And finally, Microsoft has a lovely bit of information on the RenderStream method.
Good luck!
OK !
A result is attained !
Thank You !
Dim Key As Boolean
Key = CsvCustom.Render(Report, ReportServerParameters, DeviceInfo, ClientCapabilities, RenderProperties, New Microsoft.ReportingServices.Interfaces.CreateAndRegisterStream(AddressOf MainCreateAndRegisterStream))
Dim Save As System.IO.Stream = CreateAndRegisterStream(MainName, MainExtension, MainEncoding, MainMimeType, MainWillSeek, MainOperation)
How to divide streams (after copying on the side of client) ?
You Will Specify Implements
Result of actions
https://www.broes.nl/2011/02/pdf-watermark-background-rendering-extension-for-ssrs-part-2/
One file or much file ? (on the side of client)
@andrii: one file. Just like the default PDF renderer.
Task - unload a basic report + image (binary) fields - separate files
Please Your Email – I will specify
My EMail = <snip>
Thank You !
@andrii I'm sorry mate, but I won't be fixing your problem. I've shared all the info I have on this topic, from here you're on your own! And if you solve your problem, might be a good idea to share your solution...
OK !
Hi,
thankyou for the article. very helpful. i have a question, not related to this article, but it may be related to extensions.
i need to call a webservice to format the data in a tablix. i achived this by writting a custom assembly . but the issue is that the tablix will have more than 100K records and using custom assembly, the WS is called 100K times for one column(1 call per data). the call increase if the formatting needs to be done for more than 1 column.
am trying to see if there is a way to group all the column data and make one WS call instead of making one call per data.
anyway i can use extenstions to achieve this? that is after report server renders the data and just before zipping the file, can you change the data in excel?
Any insights will be very helpful.
Thanks in advance