如何用代码生成PDF文档
PDF格式是一个很常见的格式,有时候我们需要把一些内容生成PDF文件供用户使用。
如果是现成的Word等文档,我们可以直接用Wor的“另存为”功能来将其转换成PDF,或者使用一些其他类似Adobe Acrobat的工具来转换。这个我们不在这里多说。
如果是用代码来生成PDF文档,有一些办法。但是自己研究PDF格式的思路就不提了,重复造轮子,太慢,我们只考虑能重用开源代码的几种方式。
根据需求,我们先找找有没有质量可靠,功能够用的NPM包。搜了一下,发现了这个页面:https://www.npmtrends.com/html-pdf-vs-jspdf-vs-pdf-vs-pdf-lib-vs-pdfkit-vs-pdfmake
根据星星数量能看出,备选的库有:jspdf、pdfmake、pdfkit、pdf-lib和html-pdf。
上面的这些库,可以让我们用两种方式来生成PDF:
方法1. 按照库要求的格式,把数据准备好,然后生成PDF。
jspdf:
import { jsPDF } from "jspdf";
// Default export is a4 paper, portrait, using millimeters for units
const doc = new jsPDF();
doc.text("Hello world!", 10, 10);
doc.save("a4.pdf");
pdfmake:
var fonts = {
Roboto: {
normal: 'fonts/Roboto-Regular.ttf',
bold: 'fonts/Roboto-Medium.ttf',
italics: 'fonts/Roboto-Italic.ttf',
bolditalics: 'fonts/Roboto-MediumItalic.ttf'
}
};
var PdfPrinter = require('../src/printer');
var printer = new PdfPrinter(fonts);
var fs = require('fs');
var docDefinition = {
content: [
'First paragraph',
'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
]
};
var pdfDoc = printer.createPdfKitDocument(docDefinition);
pdfDoc.pipe(fs.createWriteStream('pdfs/basics.pdf'));
pdfDoc.end();
另外pdfkit和pdf-lib均是类似的方式。
优点:
- PDF里的文字都是真实的问题,文档清晰。
- 就是可以在node.js端直接生成生成PDF文档,不用依赖浏览器等方式,比较直接。
缺点:
- 需要熟悉这个库的API接口,上手门槛较高;
- 可能对中文字体支持不够友好。看这里。
- 布局复杂的内容,排版不太容易。
方法2. 把内容生成HTML页面,然后再用库来转换成PDF。
HTML是一种直观、成熟、普遍使用的技术,如果我们先把内容生成HTML,然后用一个库一下子就能转换成PDF,是不是更容易一些呢?这样,不用再学习PDF库的那么多API,只要会HTML,CSS即可。
html-pdf即是这样的一个库,我们先看一下官方的例子:
var fs = require('fs');
var pdf = require('html-pdf');
var html = fs.readFileSync('./test/businesscard.html', 'utf8');
var options = { format: 'Letter' };
pdf.create(html, options).toFile('./businesscard.pdf', function(err, res) {
if (err) return console.log(err);
console.log(res); // { filename: '/app/businesscard.pdf' }
});
但是呢,理想很丰满,现实很骨感,这种方式也还是有明显的优缺点。
优点:
- 将html一步转成PDF
缺点:
- 用了phantomjs这样一个四五年前已经停止继续开发的、用JavaScript来实现的一个无头浏览器库,让人很担心他对最新的HTML标准的支持和遗留的bug能否被修复;
- 使用的生成方式是HTML -> Canvas -> 图片 ->PDF,因为PDF的内容是从图片转换过来的,因此PDF内容的清晰度让人不敢恭维。
因为缺点如此明显,我就没有再深入尝试和研究,浅尝辄止了。
从上面的两种方式可以看出,每种方式都存在一些比较明显的缺点,所以需要找一个更好的方式,因此后来又找到下面两种办法。
方法三. 利用浏览器的PDF打印功能。
细心的你一定会发现,浏览器菜单里的"打印"功能,是可以将完整的网页或者网页的一部分打印成pdf的,那么在我们要生成PDF的网页里,加上这么一行,就可以调用浏览器的打印功能来让用户自己选择是否打印出PDF了。
window.print();
优点 :
- 简单。
缺点 :
- 需要用户后续面对一堆打印选项,知道如何打印当前的页面,对于不熟悉浏览器这个功能的用户来说,是很困难的一件事情。
方法四. 把内容生成HTML页面,使用puppeteer来控制浏览器打开HTML并导出PDF。
Puppeteer是谷歌提供的一个操作Chrome 或者 Chromium浏览器的一个Node.js库,可以用来:
- 生成页面的屏幕截图和PDF。
- 对 SPA(单页应用程序)进行爬取并生成预呈现的内容(即“SSR”(服务器端呈现))。
- 自动提交表单、UI 测试、键盘输入等。
- 创建最新的自动化测试环境。使用最新的 JavaScript 和浏览器功能,直接在最新版本的 Chrome 中运行测试。
从功能上来看,用puppeteer来生成PDF也是一个很自然的选项了。
优点 :
- 简单;
- 可以很好地复用已有的HTML/CSS/JS代码生成的网页来生成PDF;
- 因为puppeteer调用的是Chrome无头浏览器,因此不用担心兼容性问题。
缺点 :
- 需要后台启动一个Chrome浏览器进程,启动需要花费额外的几秒钟时间,另外生成PDF前要打开页面也可能会占用较多内存。.
这里给一段示例代码:
const puppeteer = require('puppeteer');
const express = require('express')
const app = express();
let browser;
const start = async () => {
browser = await puppeteer.launch({
headless: true,
defaultViewport: {
width: 1280,
height: 800
},
args: [
"--no-sandbox",
'--ignore-certificate-errors'
]
});
await app.listen(3000);
}
async function makePdf(url) {
const page = await browser.newPage();
await page.setCacheEnabled(false);
console.time('open');
await page.goto(url);
console.timeEnd('open');
console.time('print');
const data = await page.pdf({
format: 'A4'
});
console.timeEnd('print');
console.time('close');
await page.close();
console.timeEnd('close');
return data;
}
app.get('/print', async function (req, res) {
const data = await makePdf('https://www.lema.fun/');
res.setHeader( "Content-Type", "application/pdf")
res.send(data);
})
start();
这段代码例子是启了一个Express的服务器,然后同时让puppeteer打开了一个Chrome浏览器供后面使用,这样可以避免每次生成pdf都要反复启动浏览器花费额外的2~3秒时间。
考虑到这种方式比较耗费CPU和内存,并且是一个很独立的功能,比较合适作为一个单独的服务启动,而不是和别的业务代码一起在放在一个进程里。另外,同时有多个请求并发生成PDF的时候,要考虑到这些请求打开页面的时候,不要因为共享了一些数据导致意外的后果,比如Cookie等。方法呢,可以尝试一下使用隐身模式,来给每个页面创建一个隔离的环境:
(async () => {
const browser = await puppeteer.launch();
// Create a new incognito browser context.
const context = await browser.createIncognitoBrowserContext();
// Create a new page in a pristine context.
const page = await context.newPage();
// Do stuff
await page.goto('https://example.com');
})();
如果你有什么更好的办法,或者发现别的问题,欢迎在乐码范留言讨论。