alextc
alextc

Reputation: 3515

ExpressJS and PDFKit - generate a PDF in memory and send to client for download

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?

Upvotes: 7

Views: 21168

Answers (4)

Oleg Khalidov
Oleg Khalidov

Reputation: 5268

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();

Upvotes: 4

Frederiko Ribeiro
Frederiko Ribeiro

Reputation: 2358

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);

Upvotes: 7

Karan Hotwani
Karan Hotwani

Reputation: 24

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

Upvotes: -1

user10046520
user10046520

Reputation:

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;

Upvotes: 18

Related Questions