7

In my api router, there is a function called generatePDF which aims to use PDFKit module to generate a PDF file in memory and send to client for download instead of displaying only.

In api.js:

var express = require('express');
var router = express.Router();

const PDFDocument = require('pdfkit');

router.get('/generatePDF', async function(req, res, next) {
    var myDoc = new PDFDocument({bufferPages: true});
    myDoc.pipe(res);
    myDoc.font('Times-Roman')
         .fontSize(12)
         .text(`this is a test text`);
    myDoc.end();
    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',
        'Content-Length': 1111
    });
    res.send( myDoc.toString('base64'));
});

module.exports = router;

This does not work. The error message is (node:11444) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client.

How can I go about fixing the issue and getting it work?

Also, a relevant question would be how I can separate the business logic of PDF generation from the router and chain them up?

alextc
  • 3,206
  • 10
  • 63
  • 107

4 Answers4

15

Complete solution.

var express = require('express');
var router = express.Router();

const PDFDocument =  require('pdfkit');

router.get('/generatePDF', async function(req, res, next) {
var myDoc = new PDFDocument({bufferPages: true});

let buffers = [];
myDoc.on('data', buffers.push.bind(buffers));
myDoc.on('end', () => {

    let pdfData = Buffer.concat(buffers);
    res.writeHead(200, {
    'Content-Length': Buffer.byteLength(pdfData),
    'Content-Type': 'application/pdf',
    'Content-disposition': 'attachment;filename=test.pdf',})
    .end(pdfData);

});

myDoc.font('Times-Roman')
     .fontSize(12)
     .text(`this is a test text`);
myDoc.end();
});

module.exports = router;
  • Thanks. What is body referenced by? Is it `mydoc`? – alextc Sep 25 '19 at 02:53
  • mydoc looks like an object so find how to get the actual data from it. –  Sep 25 '19 at 03:02
  • Added complete solution –  Sep 25 '19 at 03:26
  • Thanks for your answer. This works! The only thing is I am still confused about binding the `end` event to myDoc. This was not mentioned anywhere in the PDFKit documentation and the documentation does mention using doc.pipe(res) to write pdf doc into a http response. But it was not used in your answer. – alextc Sep 25 '19 at 06:20
  • 1
    Another thing is at the end of my original question. `how I can separate the business logic of PDF generation from the router and chain them up?` I would like to take a param from req.query and pass it to the PDF generation logic. Was it worth a separate question? – alextc Sep 25 '19 at 06:23
7

First I recommend to create a service for the PDF kit. And then a Controller to the route that you want.

I used get-stream to make this easier.

It also answers your question to the accepted answer:

how I can separate the business logic of PDF generation from the router and chain them up?

This is my professional solution:

import PDFDocument from 'pdfkit';
import getStream from 'get-stream';
import fs from 'fs';


export default class PdfKitService {
  /**
   * Generate a PDF of the letter
   *
   * @returns {Buffer}
   */
  async generatePdf() {
    try {
      const doc = new PDFDocument();

      doc.fontSize(25).text('Some text with an embedded font!', 100, 100);

      if (process.env.NODE_ENV === 'development') {
        doc.pipe(fs.createWriteStream(`${__dirname}/../file.pdf`));
      }

      doc.end();

      const pdfStream = await getStream.buffer(doc);

      return pdfStream;
    } catch (error) {
      return null;
    }
  }
}

And then the method of the Controller:

(...) 

  async show(req, res) {
    const pdfKitService = new PdfKitService();
    const pdfStream = await pdfKitService.generatePdf();

    res
      .writeHead(200, {
        'Content-Length': Buffer.byteLength(pdfStream),
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',
      })
      .end(pdfStream);

 
  }

And finally the route:

routes.get('/pdf', FileController.show);
Frederiko Ribeiro
  • 1,844
  • 1
  • 18
  • 30
4

For those how don't want to waste RAM on buffering PDFs and send chunks right away to the client:

    const filename = `Receipt_${invoice.number}.pdf`;
    const doc = new PDFDocument({ bufferPages: true });
    const stream = res.writeHead(200, {
      'Content-Type': 'application/pdf',
      'Content-disposition': `attachment;filename=${filename}.pdf`,
    });
    doc.on('data', (chunk) => stream.write(chunk));
    doc.on('end', () => stream.end());

    doc.font('Times-Roman')
      .fontSize(12)
      .text(`this is a test text`);
    doc.end();
Oleg Khalidov
  • 5,108
  • 1
  • 28
  • 29
-1

You can use blob stream like this.

reference: https://pdfkit.org/index.html

const PDFDocument = require('pdfkit');

const blobStream  = require('blob-stream');

// create a document the same way as above

const doc = new PDFDocument;

// pipe the document to a blob

const stream = doc.pipe(blobStream());

// add your content to the document here, as usual

doc.font('fonts/PalatinoBold.ttf')
   .fontSize(25)
   .text('Some text with an embedded font!', 100, 100);

// get a blob when you're done
doc.end();
stream.on('finish', function() {
  // get a blob you can do whatever you like with
  const blob = stream.toBlob('application/pdf');

  // or get a blob URL for display in the browser
  const url = stream.toBlobURL('application/pdf');
  iframe.src = url;
});

pipe all your pdf data to your blob and then write it to a file or url. or u can store the pdf directly into cloud storage like firebase storage and send download link to client.

If you want to generate pdfs dynamically then you can also try out html-pdf library in node which allows you to create a pdf from html template and add dynamic data in it. Also it is more reliable than pdfkit https://www.npmjs.com/package/html-pdf Also refer this link Generate pdf file using pdfkit and send it to browser in nodejs-expressjs

  • Thanks! The original question was for the node server side not browser side. I will have a look at the html-pdf library. – alextc Sep 25 '19 at 06:31
  • yes the solution is of browser side, but once you create blob you can create write stream in your file system and write directly in file and store in your local storage or directly write stream to cloud storage. – Karan Hotwani Sep 25 '19 at 06:45