Send PDF from server to client

I need to generate a zip file filled with a PDF report and images that are stored on S3 for a user to download. I want to generate the PDF report from a template so it is looking like I’ll need to generate the PDF on the server. I planned to have an event that called a createPDF method, which returns a PDF, and then passes the PDF to JSZip. I am now having trouble figuring out how to (1) generate a PDF and (2) get that PDF from the server to the client. For number one, I tried using Webshot on the server with PhantomJS but couldn’t get it to take a picture of a dynamically filled template… I used a technique from another post in which I used a server side route to stream PDF data to the client. That didn’t work for me :frowning: The PDF it returned was always blank. I’ve also looked at html-pdf (npm module), but I still have the problem of getting the PDF from the server to the client. I assume that you can’t simply return the generated PDF to be sent to the client. (I’ve tried and it doesn’t work). I haven’t done much in the realm of PDF generation so I’m a bit of a noob in this area. If anyone could provide me with some help I’d appreciate it very much.

2 Likes

I did something like this, except the PDF was served from Meteor (the reports are transient and don’t need to be kept). I created a server method that generates a PDF, saves it to the file system and returns a URL.

The client call the method and does a redirect to the returned URL:

Meteor.call('generatePDF', opts, function (error, result) {
  if(error) {
    console.log(error.reason);
  } else {
    window.location = result.url;
  }
});

There is also a server-side route (Iron Router) that serves the PDF:

Router.map( function() {
  this.route('pdf', {
    where: 'server',
    path: '/pdf/:path/:file',
    action: function() {
      var fn = this.params.file.replace(/\.pdf$/, '-' + this.params.path + '.pdf');
      var data;
      try {
        data = fs.readFileSync('/opt/downloads/' + fn);
      } catch(err) {
        this.response.writeHead(404);
        this.response.end('Error 404 - Not found.');
        return;
      }

      var headers = {
        'Content-type': 'application/octet-stream',
        'Content-Disposition': 'attachment; filename=' + this.params.file
      };
    this.response.writeHead(200, headers);
    this.response.end(data);
  }
});

I’m setting the content type to octet stream because this causes the file to download instead of open in a window in Chrome.

@jeffm That’s interesting. My reports don’t need to be persisted either, however, I’ve been thinking that maybe I could store a generated PDF in a mongo collection - publish the collection to the client - grab the PDF on the client - insert the PDF into the zip file - and delete the file from mongo. My problem is that I can’t just trigger a download with a link, I need to serve up the PDF so that I can add it to a zip file along with other documents. Does the database approach sound reasonable? I’ve never stored a PDF in mongo, but I know that you can. Seems like it might work.

I’ve been playing with PDF generation but needed something more simple than what you need, I created this a few days ago, might help you to some extent, its using the JSPDF package and its super simple, you can see in action here: http://jspdf-example.meteor.com/

Source: https://github.com/surfer77/JSPDF-Meteor

1 Like

@hodaraadam It would be nice if I could generate a PDF on the client like you are doing. It looks like your approach works pretty well… Have you tried styling the document at all? Thanks for sharing!

I did customize it a bit, here is the official page of the script so you see more options, you can even draw on it https://parall.ax/products/jspdf

I haven’t had a reason to try that, but if you’re going to create the zip file client side, why not create pdf there as well?

@jeffm That’s what I am thinking too. I’m looking into JSPDF to see if it will do what I need it to do.

@hodaraadam Did you add images to the html that you turned into a PDF? I’m trying to add an image to the pdf (by adding it to the html) and style it with inline styles but the styles don’t seem to be working. Did you try styling your html?

I did add images but also having a hard time with the styling, as of now it prints all the data with images but working also on the styling :frowning:, let me know if you come up with something

we wrapped and included JSPDF in the meteor:pdf package, but have discovered that styling is tough. You could try using meteorhacks:ssr as another approach. I’ve had some success with that. I hope that someone comes up with a foolproof package though - because most of the existing solutions are only partially viable.

1 Like

@ongoworks Yes, that would be very nice. I’ve built a huge app over the last month and the most difficult (and only unsolved) problem has been generating PDF reports and then storing them in a zip with photos from S3 for someone to download on the client… I’m getting pretty frustrated with it haha

1 Like

I too have built a hug application, part of which PDF form fill and generation in addition to S3 documents where to be apart. Alas, I could only get the PDF form fill and generation (server to client) to work. The saving to and distributing from S3 still alludes me (I can save the PDF to the local server, but choose to delete after I’ve rendered the PDF to the client for now).

Of course in my case, I use an existing PDF ‘template’, which I use as a ‘vessel’ (with fields, images, text, styling, etc. already in place) to form fill with data I gather from client side html forms (aka views).

If anyone knows how to save a PDF to S3 and then render said saved PDF from S3 to the client, with/in Meteor, I’d love to see the code.

@hodaraadam Alright, I figured out a solution that seems to be working really well. I have a private directory in the top level of my app with an html file called ‘case-report.html’. That file is an html page that I’m using to generate a PDF via webshot. It is styled with styles in that file. I actually just copied all the css from my main app into a style tag in the head section of that html page. I then created a route with the following code:

I guess I should also mention that I had to install "webshot": "0.16.0" via meteorhacks:npm, dfischer:phantomjs, and meteorhacks:ssr

Router.route('/cases/:_id/report/generatePDF', {
name: 'generatePDF',
where: 'server',
action: function() {
    var webshot = Meteor.npmRequire('webshot');
    var fs      = Npm.require('fs');
    var Future = Npm.require('fibers/future');
    var fut = new Future();
    var fileName = "case-report.pdf";
    var curCase = Cases.findOne(this.params._id);

    // Render the template on the server
    SSR.compileTemplate('caseReport', Assets.getText('case-report.html'));

    var html = SSR.render("caseReport", curCase);

    // Setup Webshot options
    var options = {
        "paperSize": {
            "format": "Letter",
            "orientation": "portrait",
            "margin": "1cm"
        },
        siteType: 'html'
    };



    webshot(html, fileName, options, function(err) {
        fs.readFile(fileName, function (err, data) {
            if (err) {
                return console.log(err);
            }

            fs.unlinkSync(fileName);
            fut.return(data);
        });
    });

    this.response.writeHead(200, {'Content-Type': 'application/pdf',"Content-Disposition": "attachment; filename=generated.pdf"});
    this.response.end(fut.wait());

}
});

I then have a link on the page that I want to generate the PDF from:

<a href="{{pathFor 'generatePDF'}}" class="waves-effect waves-light btn download">Download Case</a>

Because I am doing it this way, I can actually have another route with the exact same code as the case-report.html file and show users a preview of what the PDF will look like.

3 Likes

Just a few quick notes: On a non-Meteor project about half a year ago when I needed server-side PDF generation and was researching and implementing it I found that the most straightforward way would be to use webkit (wkhtmltopdf in my case, to be specific) to render an HTML page served up with all the regular styles (and some additional ones specific to displaying as PDF) and then just let it render that to PDF. Sending that to the client, or using it for emailing, is not a big deal after that, just a matter of understanding how Meteor/NodeJS allow you to do that.

I’m fairly sure most of that still applies. Webkit == phantomjs. Just wanted to state this, to let you know that this is a good path to pursue and that it can work really well. I don’t have time right now to write things up and port the solution to Meteor, knee-deep in work right now, but maybe if PDF generation still feels like a not super straightforward thing to accomplish with Meteor until I get around to it, I’ll write a short article and publish a package with a nice API for it. Even thinking of a SaaS for this, though I’m not sure if that wouldn’t be overkill. But could be nice, not having to install anything like (the right version of) phantomjs (or wkhtmltopdf) on your server and having it just work, be secure (noone should be able to call your routes that generate PDFs, other than the PDF webkit)…

And it does seem like Ryan figured it out, maybe there’s going to be a nice package for this until the time that I next need this functionality and would get to it. :smile:

1 Like

@seeekr I definitely think that generating a PDF server side is the way to go. I’ve done a bit more work on my project and now have a route that renders a PDF report, adds the file to a zip folder, grabs some images from Amazon S3, adds the images to the zip, and then sends the zip to the client as a download. I may look into wrapping the code up into a package… I think it would be super useful to have this functionality packaged up in the future. If I don’t get around to it, I’d love to see what you come up with!

1 Like

I have a project where I generate a PDF on the server, and send it to the client via a meteor method (as a data url). I describe it here https://github.com/pascoual/meteor-pdfkit/issues/11#issuecomment-74018064, but I’ll copy and paste what I wrote to make things easier:

I return a base64 encoded PDF from a meteor method for my own Meteor application. I started out using this package、but ended up using the npm package pdfkit directly, because I wanted access to the pipe() function. Maybe there is some way to get access to that in this Meteor package?

The advantage to doing this through a meteor method for myself is because I get access to authentication information (this.userId) within the server-side meteor method’s context.

//Code that I wrote as a custom meteor package 
var base64 = Npm.require('base64-stream');
var Future = Npm.require('fibers/future');
var PDFDocument = Npm.require('pdfkit');

somePDFGeneratingFunction(){
var doc = new PDFDocument({…});
//create the pdf with various commands (doc.text, doc.rect, etc)

var future = new Future();
var finalString = “”;
var stream = doc.pipe(base64.encode());
doc.end();
stream.on(‘data’, function(chunk){
finalString += chunk;
});

stream.on(‘end’, Meteor.bindEnvironment(function(){
future.return(finalString);
}));

return future.wait();
}

```javascript
//inside a meteor method
Meteor.methods({
  'reports.generatePDF': function(){
    return somePDFGeneratingFunction();
  }
});
//on the web browser
Meteor.call('reports.generatePDF', function(err, res){
  window.open("data:application/pdf;base64, " + res);
});

It was easier than creating a special route for the client to go to to download the PDF, and plus since it’s a meteor method I get this.userId so I know if they’re logged in or not, and what their access level is.

7 Likes

@wallslide That’s cool. I like that. I’ll play around with it and see if I can get it working in my app. Thanks for sharing!

Interesting. I’ve was needing something very alike. I’ve created a package that basically take a collection and draw PDF out of its records. It can take screenshot from displayed HTML fragments or insert images from CollectionFS or the public folder. Here’s my blog post on the reason why I did it this way and a basic example.

I love the CFS packages for this: https://atmospherejs.com/cfs