Friday, February 24, 2012

Rotativa, how to print PDF in Asp.Net MVC

Creating PDF docs in Asp.Net MVC is a fairly common functionality requested by LOB applications. Usually either for reporting or, more often, for having printable friendly documents (invoices, receipts etc).
I used to design a Crystal report document and then evaluate it and convert it to PDF in the web application. The desing part was ok but what really bothered me was the problems that often came up with deploying it. Version mismatches could really turn it into a nightmare.

Lately I came up using the excellent wkhtmltopdf tool to convert html content to PDF. It uses the WebKit engine (used by Chrome and Safari) to render html. The nice thing about it is that I can leverage my knowledge of html and css to obtain a good looking PDF and also it's quite fast.

It's a exe file not a dll library (a dll actually exists but it's not managed code and it has some problems related to usage in a multi-threaded app). It has to be executed from the Asp.net app spawning a process, so it requires some unusual coding from a web development perspective. Another downside is that it requires some work to set it up: copying exe and dll files in the web app solution, make sure they get copied over when building and publishing and some config stuff needed to make it access authenticated actions.

It seemed to me a perfect candidate to become a Nuget package. It was fairly easy to build it, except some quirks when trying to modify package properties.

I've named it Rotativa /rota'tiva/, which is Italian for rotary printing press.

With Rotativa all it takes to print a PDF is:
1. Install using the Nuget package manager (search for Rotativa) or, more conveniently, with the package manager console, typing:

Install-Package Rotativa
2. Writing a controller action to return a view representing the desired PDF output. This means just coding as usual to serve the data as regular MVC action. Such as:
public ActionResult Invoice(int invoiceId)
{
    var invoiceViewModel;
    // code to retrieve data from a database
    return View(invoiceViewModel);
}
Nothing special here, just an action. Perhaps the view used will have a different master page (or layout page) then the other views of the web app (or you’ll define a separete css for the print media, see http://www.w3.org/TR/CSS2/media.html). You can test and “preview” the results just using the browser, since we are working with a regular action, returning a html result.

3. When the html is ok you can just setup a special action returning a custom ActionResult: ActionAsPdf. The code will look like this:
public ActionResult PrintInvoice(int invoiceId)
{
  return new ActionAsPdf(
                 "Invoice", 
                 new { invoiceId= invoiceId }) 
                 { FileName = "Invoice.pdf" };
}
ActionAsPdf requires a string parameter with the name of the action to be converted to PDF. It can accept a parameter with other route data, in this case we are defining the invoiceId parameter. You can specify the name of the file being returned using the FIleName property.

To link to the PDF file in other views you just have to insert a regular link to the PrintInvoice action. Something like

@Html.ActionLink(“PrintInvoice”)
if you’re working with razor views.
And voila, that’s all and it just works, even if the printed action is protected with forms authentication.

Source code is on GitHub https://github.com/webgio/Rotativa.

67 comments:

  1. Great! ASAP i will try your solution!

    ReplyDelete
  2. Thank you Boy! ;) Let me know if you have problems/ideas...

    ReplyDelete
  3. Nice tool Giorgio, however the IT department upgraded Adobe Reader to the newest version.

    I now get a message from Adobe Reader when I open the PDF file, that says that my PDF file is corrupted.

    Any solution for this?

    ReplyDelete
    Replies
    1. Hi Julian, just tried the latest version of the Acrobat Reader with Rotativa and it seems to work... Are you able to open other pdf files? Does it work from computers with previous versions of Acrobat reader? Could you send me a copy of a sample pdf you are not able to open? Thanks, Giorgio

      Delete
  4. Hi, i get this error from Nuget "Install-Package : Could not install package 'Rotativa 1.2.1'. You are trying to install this package into a project that targets '.NETFramework,Version=v3.5', but the package does not contain any assembly references that are compatible with that framework. For more information, contact the package author."

    ReplyDelete
    Replies
    1. I downloaded the git version and manually changed target to 3.5, now it works, thanks

      Delete
  5. Wow! It installed and worked right out of the box, without a hassle. Thanks for this excellent contribution. Now I need to figure out how to force some page breaks for a cleaner looking PDF. Any ideas on where to look for that solution?

    Thanks,
    -dqj (Ithaca, NY USA)

    ReplyDelete
    Replies
    1. Hi dqj! Thanks for using Rotativa.

      You can force page breaks with css using page-break-after and page-break-before http://www.w3schools.com/cssref/pr_print_pageba.asp

      Giorgio

      Delete
    2. I added a CSS class to some elements:

      .break { page-break-after: always; }

      but I get no page breaks in the resulting pdf?

      Thanks

      Delete
    3. .breakBefore { page-break-before: always; } --> does the trick!

      Thanks.

      Delete
  6. I don't understand how to use requests that require authentication. When I'm creating a PDF from an action that requires authentication, it just prints out the login screen. Ideally, I would like to pass the credentials of the user that is currently logged in. Where can I specify this?

    ReplyDelete
    Replies
    1. As long as the user requesting the pdf is already authenticated, it should work without doing anything. Only, if you use a different cookie from the standard web forms auth cookie, then you can define a different cookie name as an optional property of the ActionAsPdfResult. The property name is Cookie, you can set it as you set the Filename prop.

      Hope this helps.

      Delete
    2. Thanks! I'm using an active directory authentication method, which sets a different cookie. I passed that cookie name into your code and it's working perfect. Thank you!

      Delete
    3. One other quick question, is it possible to use an array as a parameter? For example, in the action I'm passing "ActionAsPdf", one of the parameters is a string[]. If I set my route value to the string array, System.String[] is received on the other end, instead of the actual value. From the request, my query string could be "my-url?state=TN&state=TX&", which would get fed into my string[] state parameter. When ActionAsPdf is called, the array values are not passed along.

      Delete
    4. I think I found the answer to this, the UrlHelper that gets called in GetUrl() is the cause, and some custom code may be required for array types. See: http://stackoverflow.com/questions/1752721/asp-net-mvc-routedata-and-arrays

      Delete
    5. Brosto (or Giorgio)--I have the same issue as my deployment is for an intranet that uses AD authentication. Now that I have built to the server, I am getting "Error: Authentication Required" when I attempt to execute an `ActionAsPdf`.

      I tried setting `FormsAuthenticationCookieName = ".ADAuthCookie"` but it didn't work. Any thoughts?

      Thanks,

      Matt

      Delete
  7. How can we have the generated pdf saved to file or send straight up in an email as attachment without having the user download the file

    ReplyDelete
    Replies
    1. If you mean saved on the server, it's not possible. In order to do that you should create pdf in code and save it on the server. As for the sending email directly, I think i's not possible to do that on the web.

      Delete
  8. Giorgio, does it work on asp.net mvc 2?

    ReplyDelete
    Replies
    1. Diego, in fact I didn't test it on asp.net mvc 2. BTW it makes sense it should work, it shouldn't use any feature specific to mvc 3. Have you tried it?

      Delete
  9. Hi thanks for this, Prove in a normal mvc application worked. But try to prove in an application with azure and I created a PDF that says Bad Request - Invalid Hostname
    HTTP Error 400. The request is invalid hostname. Any idea why this?

    ReplyDelete
    Replies
    1. Hi Irving,
      you could have some problem with Dev Fabric load balancer. Have you tried deploying to Azure?

      Delete
    2. Hi Giorgio,

      I encountered this issue as well, and you are correct, generating a PDF when running in Azure proper seems to work. Do you have any idea how to configure the emulator to allow it to work locally? It would be a bummer to have to deploy just to test views out.

      BTW - awesome tool!

      Thanks

      Delete
  10. Greate work, You should protect your self from commerical packages owners.

    ReplyDelete
    Replies
    1. Thanks!
      What do you mean by " protect your self from commerical packages owners."? What would they do?

      Delete
  11. Great work Giorgio. Definitely a step in the right direction. Many steps in fact.

    Do you have any advice on getting style="page-break-inside: avoid" to work? My divs are still being broken across pages.

    ReplyDelete
  12. Hi Giorgio. Great work. Thanks. But one issue. It works fine for me locally, but when deployed to IIS7 the file saves with 0 bytes. I m assuming the exe is not executing on server. I have given full permissions to the Rotativa folder along with .Net Trust set to full. Any help would be appriciated. Thanks

    ReplyDelete
    Replies
    1. I got the detailed error. Its due to Windows Authentication enabled. What should be done to bypass/fix this?

      Delete
    2. Rotativa doesn't work with Windows authentication, Just with Web forms authentication or a cookie based one. Your only alternative is turn it off for that page.

      Delete
    3. This comment has been removed by the author.

      Delete
  13. Great PDF solution. I have a project with multiple solutions. Is there a simple way to use this so I only have 1 install that is accessible from all projects?

    ReplyDelete
    Replies
    1. That should read solution with multiple projects.

      Delete
  14. Hi giorgio,

    Is there a way to print a PDF that contains plots generate via JQPLOT, (Jquery Plot). Is there a way to make it work client side?

    ReplyDelete
  15. Hi Giorgio,

    I'm having a strange issue with rotativa, once I moved from my dev machine to production server.

    Now it generates empty pdf, with acrobat sayng they're broken or tehy have unknown file format.

    The action used to render the view that is then converted to pdf functions correctly.

    Have you ever experienced this problem? any suggestion?

    Thank you in advance,

    Alberto

    ReplyDelete
    Replies
    1. Nooo... it was my fault.. I'v been kinda stupid :)

      the website is currently running on a temporary url, which I have specified to point to my prod. server in file hosts. But the server itself didn't actually "know" where to look for the site url, so wkhtmltopdf.exe couldn't find the page i was trying to render as a pdf.

      That solved, now it actually returns a pdf, but it now "print" the login form, even if I am logged and I use the standard asp.net authentication.. strange..

      Any help?

      Delete
  16. Have rotativa any limit of pages to print

    ReplyDelete
    Replies
    1. Not that I know. Did you experience problems?

      Delete
  17. Great tool, I just had one quick question. Why do we need the QT.browser file?

    ReplyDelete
    Replies
    1. Thanks! It's needed to make cookies work. http://stackoverflow.com/a/7502669/212398

      Delete
  18. How can I call Pdf from another controllers action? (not-Authenticated secred action :) )

    ReplyDelete
    Replies
    1. Glikoz, not sure I understood what you are trying to do... You already can call another action, even on another route, and nobody will see the actual Url.

      Delete
  19. Hi, is there any way by which we can combine multiple PDFs containing multiple views?

    ReplyDelete
    Replies
    1. Hi Amit, not that I know, sorry. What about simply concatenate them in a "super" view?

      Delete
  20. Hi Giorgio. Great tool. I'm having a problem when I deploy to production though. I'me getting an error that says "The directory name is invalid". It works great on my development machine. Are there any special folder the component uses that I should give permissions to? Thanks.

    ReplyDelete
    Replies
    1. Hi, ones you install the package no other configuration should be needed. How did you deploy to production? You need the Rotativa folder in the root of the web app, or if different provide the path in web.config.

      Delete
    2. Thanks for yuor quick reply. Then it's totally my bad. I have the Rotativa dll in my bin folder, but nothing else. I'm going to deploy the Rotativa folder as well. Do I need the dll file in the bin folder or juest the Rotative directory in the root? Thanks again.

      Delete
    3. Well, it worked. Thanks. Now I'm getting the issue mentioned a couple of comments above where my login page gets rendered instead of my content page, so as suggested I'm going to check the cookes. Funny thing is that I'm not using a specific cookie name, so I'm guessing it's something about IIS.

      Quick question. Is there a way I can programatically get the cookie name from my app? Thanks Giorgio.

      Delete
    4. Found the way to get the cookie name:

      FormsAuthentication.FormsCookieName

      However, I still get the login page instead of the actual authorized view, both whe I set FormsAuthenticationCookieName to ".ASPXAUTH" (the default name) or to FormsAuthentication.FormsCookieName. Not sure what's wrong there, cause on my development pc works fine.

      Delete
  21. How can we save this pdf to server???pls reply me

    ReplyDelete
    Replies
    1. Hi, just uploaded Rotativa 1.4 to nuget gallery. It has a SaveOnServerPath property that, if set with a valid server file system path, saves the pdf. Sorry for taking more time then expected, hope it helps anyway.

      Delete
  22. This comment has been removed by the author.

    ReplyDelete
  23. I'm having the redirect to login problem mentioned earlier. I've tried setting FormsAuthenticationCookieName property on ActionAsPdf, but that didn't help. I'm using Forms authentication.

    Can you tell me how can I resolve this?

    Another question - can I save PDF on the server?

    Thanks!

    ReplyDelete
    Replies
    1. Hi Jelena,
      It should work without having to set the cookie name. If it doesn't you could think about using ViewAsPdf instead.
      Ciao,
      Giorgio

      Delete
    2. Grazie Gio!

      ViewAsPdf works like a charm - no authentication issues.

      Do you have a suggestion how to use Rotativa to generate pdf on the server, so I can attach it to an email. Like invoice email with PDF attached.

      Great work. Cheers!

      Delete
    3. Grazie Jelena! ;)
      You can use the new property SaveOnServerPath. If it is set with a valid server file system path, it saves the pdf.

      Ciao,
      Giorgio

      Delete
    4. Thank you again Giorgio. SaveOnServerPath works great for saving the pdf on the server. Thanks for adding it to the package. Cheers!

      Delete
    5. It's a feature that was requested from several users and was relatively easy to implement, I'm really happy to be able to help my fellow programmers ;)
      Ciao!

      Delete
  24. if I have multiple views, how do I save it to become a multiple pages pdf?

    ReplyDelete
    Replies
    1. To control page breaks you can use css: http://www.w3schools.com/cssref/pr_print_pageba.asp

      Delete
  25. Hello Giorgio,

    It is working well on my local machine. But it is giving me unhandled exception on server. I am using 'SaveOnServerPath', where I am giving windows temp path to save the document. This is where I am getting the error.
    at Rotativa.WkhtmltopdfDriver.Convert(String wkhtmltopdfPath, String switches, String html)
    at Rotativa.AsPdfResultBase.CallTheDriver(ControllerContext context)
    at Rotativa.AsPdfResultBase.ExecuteResult(ControllerContext context)

    ReplyDelete
  26. Hi, does rotativa support .gif images?

    ReplyDelete
    Replies
    1. Hi, if gif images are part of the html to convert they should be converted. Did you experience issues with this?

      Delete