Friday 29 November 2013

PDFs from ASP.Net pages, exactly as they look in a browser, with forms auth.

So, I'm writing a web report that has a number of different charts and grids.
The customer wants to print straight from the browser and then save as a PDF.
You spend quite a bit of time getting the page just right, and then a bit more getting the css for the printing just so.
After all that, the prospect of writing it all again, using a server side PDF library is a bit unpalatable.

What we need is a tool to run on the server that does what a browser does, and serves the rendered page as a PDF to the browser.

Enter phantomjs.

This is a headless implementation of a web-kit browser that you can manipulate using JavaScript.
There are quite a few good tutorials on getting phantomjs to serve up a PDF, but I needed to use it on a ASP.Net MVC site using Forms Auth.

When the webkit browser goes to load the pages, its not authenticated and gets bounced to the mvc login page.

To get phantomjs  logged in I needed to pass it the authentication cookie from the request.
Here's how it works

////////////////////////////////////////////
// pdfAuth.js
////////////////////////////////////////////
var page = require('webpage').create(),
    system = require('system'),
    address, output, size;

address = system.args[1];
output = system.args[2];
domain = system.args[3];
auth = system.args[4];

page.viewportSize = { width: 600, height: 600 };

page.paperSize =  { format: "A4", orientation: 'portrait', margin: '1cm' };

phantom.addCookie({
 'name': '.ASPXAUTH',
 'value': auth,
 'domain': domain
});

page.open(address, function (status) {
 if (status !== 'success') {
  console.log('Unable to load the address!');
  phantom.exit();
 } else {
  window.setTimeout(function () {
   page.render(output);
   phantom.exit();
  }, 200);
 }
});



The Js fIle accepts three parameters:
  1. The URL of the page to render
  2. The output filename
  3. The aspxauth cookie
This dumps a file on disk for the mvc code to pick up.

The following code is a sample action that pokes phantomjs with the right parameters to send the PDF to the client

        public ActionResult Print(string url)
        {

            string serverPath = HttpContext.Server.MapPath("~/Phantomjs/");
            string filename = DateTime.Now.ToString("ddMMyyyy_hhmmss") + ".pdf";

            url = "http://" + Request.Url.Authority + "#" + url;

            new Thread(new ParameterizedThreadStart(x =>
            {
                ExecuteCommand("cd " + serverPath + @" & phantomjs pdfAuth.js " + url + " " + filename + " " + Request.Url.Host + " " + Request.Cookies[".ASPXAUTH"].Value);
            })).Start();

            var filePath = Path.Combine(serverPath, filename);

            var stream = new MemoryStream();
            byte[] bytes = DoWhile(filePath);

            return File(bytes, "application/pdf", filename);
        }

        private void ExecuteCommand(string Command)
        {
            try
            {
                ProcessStartInfo ProcessInfo;
                Process Process;

                ProcessInfo = new ProcessStartInfo("cmd.exe", "/K " + Command);
                ProcessInfo.CreateNoWindow = true;
                ProcessInfo.UseShellExecute = false;

                Process = Process.Start(ProcessInfo);
            }
            catch { }
        }

        private byte[] DoWhile(string filePath)
        {
            byte[] bytes = new byte[0];
            bool fail = true;

            while (fail)
            {
                try
                {
                    using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                    {
                        bytes = new byte[file.Length];
                        file.Read(bytes, 0, (int)file.Length);
                    }

                    fail = false;
                }
                catch
                {
                    Thread.Sleep(500);
                }
            }

            System.IO.File.Delete(filePath);
            return bytes;
        }