Grails + Angular + PhantomJS = PDF Reports
TL;DR A brain dump from what I learned trying to generate report PDFs for a single page AngularJS app and Grails API backend. It’s easier now with PhantomJS.
Over the years I’ve used various technologies to “generate reports” for web applications. I’ve done this with mixed results using:
2. Various Grails plugins (ie rendering plugin)
3. iText (No longer open source)
4. Then [redacted to protect the innocent] mentioned PhantomJS as way they did it recently and then abruptly left the project for greener pastures. They left no code or examples but just the earworm of “Use PhantomJS it will be easy!”
Problem: Generate a PDF from a HTML page (Bootstrap styles and all) which include JS rendered charts and graphs. D3.js/C3.js
Solution: Use PhantomJS (a headless scriptable WebKit browser) to take a “screen capture” of the HTML page and generate a PDF (a.k.a. hackery). You could just as easily create a image (jpg or png).
Step 1: Install PhantomJS - [PhantomJS doc link]
> brew update && brew install phantomjs
Side note: In theory you could install PhantomJS as a module on a Node server but that’s an exercise left up to the reader. <— I hate when authors do this sort of copout but YOLO. <— Ok that was worse, FTW!
Step 2: In your app remove Bootstrap’s “@media print” CSS attributes or don’t it’s entirely up to you if you like unstyled reports. Phantom’s screen capture functionality uses the same styles as if you were printing from your favorite browser.
The rest of this post is a series of GISTs which do the heavy lifting of generating the PDF reports. I doubt this is the best way to present this material. So I may, in the coming days, create a demo Grails app that has a working version. Vote now and vote often! I also need to learn how to embed GISTs into Tumblr. An exercise left for the author.
File 1: Report Demo - rasterizestdout.js
https://gist.github.com/staticnull/3e0ea8dd9f2d4480a49b
https://gist.github.com/staticnull/3e0ea8dd9f2d4480a49b
Comment: We “improved on” Phantom’s rasterize.js to output directly to standard out. We also added code to allow for a repeatable footer containing an image, pager numbering and HTML.
File 2: Report Demo - Report Controller
https://gist.github.com/staticnull/27a4a1aa472738181faf
https://gist.github.com/staticnull/27a4a1aa472738181faf
Comment: Using Groovy’s String.execute() we’re running rasterizestdout.js script on the full report URL. Setting the response’s content type to ‘application/pdf’ streams the pdf to the browser. FYI, It’s easy enough to have it download directly to the computer by setting a header with a filename.
response.setHeader “Content-disposition”, “attachment; filename=${“${reportName}.pdf”}”
File 3: Report Demo - Config.groovy
https://gist.github.com/staticnull/0189fe2a8faf07262c4a
https://gist.github.com/staticnull/0189fe2a8faf07262c4a
Comment: You’ll mostly likely end up installing PhantomJS differently on your local server versus dev/prod. This is how we configured ours.
File 4: Report Demo - HTML Button + Controller
https://gist.github.com/staticnull/1dd2c1ad11fcb8382a9c
https://gist.github.com/staticnull/1dd2c1ad11fcb8382a9c
Comment: This is where it gets hand wavey. So from the AnuglarJS app pass the report page URL back to the Grails app ReportController. Poof you’ve got a PDF (or not your results may very).
Be careful of JS errors in the HTML report pages your trying to generate as PDF as they can cause PhantomJS to error out. There are some useful debugging ideas in the trouble shooting FAQ. We ran into an issue with C3 causing Phantom to error out and generate nothing. It’s since been resolved but was pretty frustrating at the time.
I more or less wanted to get this post out there before I moved on to other things. Again if there’s interest I’ll create a barebones demo app, maybe even make it Grails only eliminate the Angular variables. Hopefully there’s been enough dots to connect all the line too.
Last but certainly least, I haven’t thought about how production ready and/or scalable this solution may be. First impression is that it isn’t very web scale, so thankfully we’re using MongoDB to offset this problem. </sarcasm>
Feedback and questions are very welcome. Response times may vary.