为什么 nodeJs 不从磁盘读取整个二进制文件? 编辑 MCVE:

问题描述

我有一个 PDF 文件,我想使用 NodeJS 将其读入内存。理想情况下,我想使用 base64 对其进行编码以进行传输。但不知何故,read 函数似乎无法读取完整的 PDF 文件,这对我来说毫无意义。原始 PDF 是使用 pdfKit 生成的,可以使用 PDF 阅读器程序查看。

原始文件 test.pdf 在磁盘上有 90kB。但是,如果我读取并将其写回磁盘,则只有 82kB 并且新的 PDF test-out.pdf 不行。 pdf 查看器说:

无法打开文档。 pdf 文件已损坏。

因此 base64 编码也无法正常工作。我使用此 webservice 对其进行了测试。有人知道为什么以及这里发生了什么吗?以及如何解决

我已经找到了 this 个帖子。

fs = require('fs');
let buf = fs.readFileSync('test.pdf'); // returns raw buffer binary data
// buf = fs.readFileSync('test.pdf',{encoding:'base64'}); // for the base64 encoded data
// ...transfer the base64 data...
fs.writeFileSync('test-out.pdf',buf); // should be pdf again

编辑 MCVE:

const fs = require('fs');
const PDFDocument = require('pdfkit');

let filepath = 'output.pdf';

class PDF {
  constructor() {
    this.doc = new PDFDocument();
    this.setupdocument();
    this.doc.pipe(fs.createWriteStream(filepath));
  }

  setupdocument() {
    var pageNumber = 1;
    this.doc.on('pageAdded',() => {
        this.doc.text(++pageNumber,0.5 * (this.doc.page.width - 100),40,{width: 100,align: 'center'});
      }
    );

    this.doc.moveDown();
    // draw some headline text
    this.doc.fontSize(25).text('Some Headline');
    this.doc.fontSize(15).text('Generated: ' + new Date().toUTCString());
    this.doc.moveDown();
    this.doc.font('Times-Roman',11);
  }

  report(object) {

    this.doc.moveDown();
    this.doc
      .text(object.location+' '+object.table+' '+Date.Now())
      .font('Times-Roman',11)
      .moveDown()
      .text(object.name)
      .font('Times-Roman',11);

    this.doc.end();
    let report = fs.readFileSync(filepath);
    return report;
  }
}

let pdf = new PDF();
let buf = pdf.report({location: 'athome',table:'wood',name:'Bob'});
fs.writeFileSync('outfile1.pdf',buf);

解决方法

encodingfs.readFileSync() 选项用于告诉 readFile 函数文件的编码方式,以便读取文件的代码知道如何解释它读取的数据。它不会将其转换为该编码。

在这种情况下,您的 PDF 是二进制文件 - 它不是 base64,因此您告诉它尝试将其从 base64 转换为二进制文件,这会导致数据混乱。

您根本不应该传递 encoding 选项,然后您将获得 RAW 二进制缓冲区(这就是 PDF 文件 - 原始二进制文件)。如果您出于某种原因想将其转换为 base64,则可以对其执行 buf.toString('base64')。但是,这不是它的原生格式,如果您将转换后的数据写回磁盘,它将不是合法的 PDF 文件。

要将同一个文件读取和写入不同的文件名,请完全取消编码选项:

const fs = require('fs');
let buf = fs.readFileSync('test.pdf'); // get raw buffer binary data
fs.writeFileSync('test-out.pdf',buf); // write out raw buffer binary data
,

经过大量搜索,我发现了 this Github 问题。我的问题中的问题似乎是 doc.end() 的调用,它出于某种原因不等待流完成(写流的 finish 事件)。因此,正如 Github 问题中所建议的,以下方法有效:

  • 基于回调:
doc = new PDFDocument();
writeStream = fs.createWriteStream('filename.pdf');
doc.pipe(writeStream);
doc.end()
writeStream.on('finish',function () {
    // do stuff with the PDF file
});
  • 或基于承诺:
const stream = fs.createWriteStream(localFilePath);
doc.pipe(stream);
.....
doc.end();
await new Promise<void>(resolve => {
  stream.on("finish",function() {
    resolve();
  });
});
  • 或者更好,不是直接调用 doc.end(),而是调用下面的函数 savePdfToFile
function savePdfToFile(pdf : PDFKit.PDFDocument,fileName : string) : Promise<void> {
  return new Promise<void>((resolve,reject) => {

    //  To determine when the PDF has finished being written sucessfully 
    //  we need to confirm the following 2 conditions:
    //
    //  1. The write stream has been closed
    //  2. PDFDocument.end() was called syncronously without an error being thrown

    let pendingStepCount = 2;

    const stepFinished = () => {
      if (--pendingStepCount == 0) {
        resolve();
      }
    };

    const writeStream = fs.createWriteStream(fileName);
    writeStream.on('close',stepFinished);
    pdf.pipe(writeStream);

    pdf.end();

    stepFinished();
  }); 
}

这个函数应该正确处理以下情况:

  • PDF 生成成功
  • 在写入流关闭之前在 pdf.end() 内抛出错误
  • 写入流关闭后,在 pdf.end() 内抛出错误