LWDW!

Learn the work from doing the work🍺

PDF生成方案比较
Posted on 2023-02-01

PDF生成是一个常见的需求,目的一般是将动态数据插入到特定格式的PDF模版中。

比如签约协议,需要体现出甲方乙方的姓名、时间、签约期限(这些信息可能被维护在CMS后台中)、水印、盖章;又比如生成财务报表,将特定时间区间的收益曲线以图表形式体现在PDF中。

对于这样的诉求,实现方式有多种,本文会大致讨论三种思路:后端生成PDF、前端生成粗糙PDF、前/后端生成精确PDF,比较其中的优劣,给出方案选择的建议。另外,本文会针对前端生成精确PDF进一步展开,并提供示例仓库

方案比较

后端生成PDF

后端生成PDF一直是非常常见的做法,其中主要的区别是采用怎样的PDF绘制方案。

对于简单的文本PDF需求,可以采用itext,通过api快速的进行文字的编写排版;

对于复杂一些的需求,可以考虑puppeteer,这就是一个“运行在node的chrome”,所以它可以在后端虚空绘制html,并将其导出为PDF(前端思想其实很重)。

但后端方案再成熟,它都需要带宽成本、服务运行成本。另外在开发调试过程中,PDF的绘制结果也并不直观。

想空手套白狼的你,可能就想前端搞定,可以咋办?

前端生成粗糙PDF

前端生成粗糙PDF的原理其实和puppeteer很相似,我直接用真实的浏览器渲染html,将其打印为PDF不就完了么!

但问题有几个:

1. PDF尺寸

PDF的尺寸是固定的,而浏览器尺寸是善变的。A4的尺寸在物理世界是210mm×297mm,涉及到电子产业的时候,便是595 × 842 points(PostScript定义)。puppeteer的一大优势就是作为headless chrome,可以方便的指定尺寸,比如指定一个595 × 842 px,凑合也能用,但是浏览器端这么搞就有点麻烦,当然肯定也是能搞得定。

2. 分页、页首、页脚

默认情况下,浏览器打印网页为PDF的时候,分页是很强行的,噶你的图片、表格甚至文字就成两半了。虽然可以通过page-break-after之类的CSS打印属性来进行一定控制(手动分页),但想要流畅的自动分页是很麻烦的,需要结合样式定义和计算(puppeteer同样会遇到该问题)。

3. 客户端差异

客户端的不同很可能导致生成结果的不同,如字体差异。

4. 预览不一致

既然在客户端生成,很可能在下载PDF前,用户是能看到这份PDF的样子的,但下载下来的PDF往往与客户端呈现的是不一致的。毕竟我们是html转的,分页、字体、边距之类的,诸多差异了只能说。

5. 结论

所以说呢,这种方法只建议用在很凑合的场景,否则就请继续往下看!

前/后端生成精确PDF

通过上面的介绍,我们发现PDF生成的挣扎主要集中于绘制,这里是一个两难命题:

  • 采用基于api的方案,绘制并不直观。面对复杂场景可能有些痛苦(尤其是习惯于HTML语言体系的前端)

  • 采用基于html的方案,绘制直观,但是难以匹配PDF的格式。

有没有一种方案,绘制上类似HTML,但又完美匹配了PDF呢?!

隆重介绍:react-pdf,npm包是@react-pdf/renderer(正如名字,是renderer渲染器)可不要和npm上的react-pdf(这个是pdf预览器)搞错了。

它的工作原理很简单:定制了各种组件,允许开发者用JSX语法进行绘制,并可以指定A4尺寸(以上面提到的points => pt为尺寸单位),提供了页首、页尾、分页等组件、属性,写起来就像是写普通的JSX,只不过呢,它可以以canvas而不是dom来做预览渲染。官方demo看一眼就明白了。

demo

显然它很适合作为后端方案,React+SSR这不就来了么!

但我多提一句前端方案,区别不大,只是多了一些坑:

1. 字体,绕不过去的坎

只要是搞pdf生成,字体不一致是小问题,字体缺失才是大头。为了能够让pdf支持中文,客户端加载一个中文字体文件是少不了的(放在后端这就不存在问题了)。我只说4个字:方正黑体。免费、无需授权、体积3M,在中文字体界算是顶尖的小了。

2. 移动端预览

官方提供了一个PDFViewer组件,基于Iframe来做PDF预览,但很不幸,这个组件不支持移动端。

但幸好我们有解决方案,还记得上面提到的非常相像的仓库react-pdf(pdf预览器)么?它正好派上了用场。简单的来说,我们现场生成了pdf,然后将其吐给了pdf预览器。具体可以直接参考代码

3. 样式

虽然react-pdf完全支持JSX,但是具体绘制的内容只能使用官方提供的组件,且使用官方暴露的style属性进行布局,其属性与React原生style的写法基本是一致的。只是要时刻记住,它们不会变成Dom,自然也就不支持class等样式定义方式。所以如果你喜欢一些现成的CSS三方库,如tailwind之类的,很可惜,不能用。

插入需求:图表

PDF里插图表,怎么画这个图表呢?以市面上成熟的ECharts为例,它们其实是提供了生成图片的能力的,只是注意要关闭动画。另外前端渲染与服务端渲染,会有一点点区别。

示例代码中展示了如何在前端去做图片的生成,其实就是先要用客户端的canvas绘制出图表(在视区外绘制),将其生成blob形式的图片,将其丢到React-PDF中。如果是在服务端,可以使用node-echarts-canvas一类的方案。

总结

本文提供了一些PDF生成上的思路,希望能够帮助大家进行比较。最后对于前端+react-pdf的组合,俺可以提供一个示例仓库(只适配了移动端),具体可见这里