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?

4 Answers 4

18

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; 
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks. What is body referenced by? Is it mydoc?
mydoc looks like an object so find how to get the actual data from it.
Added complete solution
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.
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?
|
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); 

Comments

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

Comments

-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

2 Comments

Thanks! The original question was for the node server side not browser side. I will have a look at the html-pdf library.
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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.