ExpressJS和PDFKit - 在内存中生成PDF并发送给客户端下载

7
在我的 api 路由器中,有一个名为generatePDF的函数,它旨在使用 PDFKit 模块在内存中生成 PDF 文件,并将其发送给客户端以供下载,而不仅仅是显示。
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;

这个不起作用。错误消息是(node:11444) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

我该如何修复这个问题并让它正常工作?

另一个相关的问题是如何将PDF生成的业务逻辑与路由分离并将它们链接起来?

4个回答

15

完整的解决方案。

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;

谢谢。body 是什么引用?是 mydoc 吗? - alextc
mydoc看起来像一个对象,因此找出如何从中获取实际数据。 - user10046520
添加了完整的解决方案。 - user10046520
感谢您的回答。这很有效!唯一的问题是我仍然对将 end 事件绑定到 myDoc 感到困惑。这在 PDFKit 文档中没有提到,但文档确实提到使用 doc.pipe(res) 将 pdf doc 写入 http 响应中。但在您的答案中没有使用它。 - alextc
1
另一个问题在我原来的问题结尾处:如何将 PDF 生成的业务逻辑与路由分离并将它们链接起来? 我想从 req.query 中获取一个参数并将其传递给 PDF 生成逻辑。 这值得单独提出一个问题吗? - alextc

7

首先,我建议创建一个PDF kit的服务。然后创建一个控制器来路由您想要的内容。

我使用了get-stream来使这更容易。

这也回答了您对被接受的答案提出的问题:

如何将PDF生成的业务逻辑与路由分离并将它们链接起来?

这是我的专业解决方案:

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

接下来是控制器(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);

 
  }

最后是路由:

routes.get('/pdf', FileController.show);

4

对于那些不想将内存用于缓冲PDF并立即向客户端发送块的人:

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

-1

您可以像这样使用Blob流。

参考: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;
});

将所有的PDF数据传输到Blob,然后将其写入文件或URL。 或者您可以直接将PDF存储到云存储中,如Firebase存储,并向客户端发送下载链接。

如果您想动态生成PDF,则还可以尝试在Node中使用html-pdf库,它允许您从HTML模板创建PDF并添加动态数据。此外,它比pdfkit更可靠。 https://www.npmjs.com/package/html-pdf 还请参考此链接 使用pdfkit生成PDF文件并在nodejs-expressjs中将其发送到浏览器


谢谢!原始问题是关于Node服务器端而不是浏览器端的。我会查看html-pdf库。 - alextc
是的,解决方案是在浏览器端,但一旦你创建了 Blob,你可以在文件系统中创建写入流并直接写入文件并存储在本地存储中,或者直接将写入流写入云存储。 - Karan Hotwani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接