从 html 到 pdf,为 Slowly 信件换衣裳

有一天,我发现 Slowly 应用有 web 端,网页也是简洁的风格,看着陈列的一封封信件,产生想要保存的想法,于是开始了从 html 到 pdf 的换装之旅。

Slowly

Slowly,是一个慢的应用,在这里可以结识天南地北的笔友,交流的信件根据地点不同,派送时间从几天到几小时不等。在这里可以讨论各种东西,每次看到派送的小箭头,心中一片期待。我很喜欢这里。

选型

window 对象上有 print 方法,可以直接调用系统的打印方法,选择保存为 pdf,粗暴解决问题。在我的这个场景下,如此会保存整个窗口的内容,样式不美观,我不满意这种效果。看了一些资料后,我选择的方法借助 jsPDF 和 html2canvas 两个三方库,html2canvas 用于把 html 页面转化为 canvas,如此可以保持页面原有的 css 样式,处理 canvas 为图片后,使用 jsPDF 插入 pdf 文档,解决核心问题。

1
2
3
const canvas = await html2canvas(dom);

pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, 0, imgWidth, imgHeight);

html2canvas

在 web 端,一封封信件就是一个个 dom 元素,第一步需要获得这些 dom 元素,生成的 pdf 文档是 a4 规格的,需要对 dom 元素宽度进行微调,开始我使用 cloneNode 方法进行复制,但 html2canvas 方法会报错,后面搜索得知,html2canvas 方法只对 document 中的 dom 元素生效,把复制后的元素插入到 document 中可以解决问题。

笔友之间的沟通下,会有多封信件,这里需要操作浏览器在信件页面上跳转,使用 window.history.back() 操作浏览器返回上一页面,然后点击下一封信件,抓取内容。但因每个动作之间的间隔过短,点击时页面还没渲染好,会造成错误。我简单地阻塞一段时间,等待页面渲染完成后点击。

1
2
3
4
5
6
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

window.history.back();
await sleep(300);

在我的场景之下,希望 pdf 文档是按照信件的时间顺序来组成的,html2canvas 方法又是异步操作,这就需要处理每个 html2canvas 方法的先后问题。要是简单地使用 then 方法来调整,百来份信件岂不是要 then 到天上去,这里借助数组的 reduce 方法很简洁得排列每个方法的顺序,解决顺序问题。

1
2
3
4
5
6
7
8
doms.reduce(
(chain, dom) => chain.then(
async () => await html2pdf(dom)
),
Promise.resolve()
).then(
() => savePdf()
)

上面的代码按照数组元素的索引排列异步操作的顺序,最后返回的也是一个 Promise,可以继续操作,在这里可以进行 pdf 的保存操作。

jsPDF

在插入图片到 pdf 文档的过程中,遇到一个问题,过长的图片如何分页。因为是图片,jsPDF 并不知道在何处分页,这是需要自己处理的。

1
2
3
4
5
6
7
8
const imgWidth = pageWidth;
const imgHeight = canvas.height / canvas.width * imgWidth;
const count = canvas.height / pageHeight;

for (let i = 0; i <= count; i++) {
pdf.addPage(pageWidth, pageHeight);
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, - i * pageHeight, imgWidth, imgHeight);
}

根据 canvas 的宽高比例和 pdf 页面宽度计算出图片的高度,求出图片占置页面的数量,jsPDF 中的addImage 方法的三、四个参数是指定图片放置起点的,不断调整起点位置,便可正确分页。

我把这个小工具发布为脚本,完整代码在这里,Greasy Fork 脚本地址在这里,欢迎使用。要说这次折腾的最大收获是什么,当然是PromiseDOM美美的 pdf 文档!