项目场景:
最近在做AIGC这快,由AI生成pdf,再导出pdf,需要导出PDF格式
实现思路:
正常前端导pdf实现思路就是 将html页面生成pdf文件,需首先将页面转换为图片,然后再输出成pdf。
一开始根据常规方法,使用html2canvas和jspdf,
const pdfDoms = document.querySelectorAll('.pdfDom');
const doc = new jsPDF('l', 'pt', [842, 598]);
let newArr = []
for (let index = 0; index < pdfDoms.length; index++) {
let dom = pdfDoms[index];
let canvas = await html2canvas(dom, { useCORS: true, scale: 2 })
let imgData = canvas.toDataURL('image/png');
let width = dom.offsetWidth;
let height = dom.offsetHeight;
let bili = width / height;
let height2 = 842 / bili;
if (index != pdfDoms.length - 1) {
newArr.push({
size: {
width: 842,
height: height2,
},
content: imgData
})
}
}
大概走这么个流程,先建一个宽842,高598的pdf,然后每页根据dom节点的高度,自适应的把每页插入pdf,最后导出。好了,第一个坑来了,生成的每页pdf的高度,全是一样的,改不了,因为最外层写死了,当时想到一个方案,要不把每个导出的dom节点高度固定吧,但是这样一来,用户的内容就有限了,这个肯定行不通(于是我在那调节高度,这个地方踩坑了3天)
后来无意中发现一个库(PDF-LIB),地址:SaveOptions · PDF-LIB,看了一下官方文档,好像需要把每个数据节点传入,再生成pdf,想了一下准备放弃,这时候突然看到可以直接把图片导pdf的一个小demo,于是试了试,
const topdf = async (imgs, title) => {
const doc = await PDFDocument.create()
for (const key in imgs) {
if (Object.hasOwnProperty.call(imgs, key)) {
const element = imgs[key];
const image = await doc.embedPng(element)
const pdfPage = doc.addPage([image.width, image.height])
pdfPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
})
}
}
const pdfBytes = await doc.save()
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
var downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = `${title}.pdf`; // 设置下载文件的名称
downloadLink.click();
}
同样的原理,通过dom转图片,再插入pdf,但是这里有个地方需要注意,
const pdfBytes = await doc.save()
这个pdfBytes生成的是个流,如果在node环境下,可以直接通过fs模块导出,但是我们现在在浏览器环境下,需要把这个流转换成二进制blob,再通过浏览器导出,
可以每页自适应高度导出,但是好像还是有偏差
原来的dom在浏览器上是这么显示的
但是导出长这样了
页面文字样式偏移了, 查了很多,大家都踩了这个坑,用html2canvas绘制完图片后,始终会有个偏移距离,之前的解决办法是设 scrollY: 0,scrollX: 0
这两个参数为0,但是怎么改都不行,
后来想了一下,又找了个组件库dom-to-image,这个和html2canvas的使用方法差不多,
let canvas = await domtoimage.toPng(domitem,options)
直接在第一个参数上传dom就可以,然后再结合PDF-LIB来导出,这时候样式不会乱了,每页pdf的高度也都是自适应,但是又出新问题了,导出的分辨率实在太低了,这样用户是真的没法使用,在某些需要大屏展示的场景下,肯定不行,体验感很差,于是开始debig,开始修改dom-to-image这个npm包的源码,在里面加了缩放
function newCanvas (domNode) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
var scale = options.scale || 1; // 默认值1
canvas.width = (options.width * scale) || util.width(domNode);
canvas.height = (options.height * scale) || util.height(domNode);
ctx.scale(scale, scale) // 添加了scale参数
if (options.bgcolor) {
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
这样一来,再结合传入的宽高,完美解决,最后就差一步了,这样的话,只能在我本地有效,同事拉代码后依旧是原来的清晰度,同事那边不会生效,打包后也不会生效,于是直接把依赖中的文件拿出来吧,直接本地引入,这样线上到时候也能用了
最后放上完整的导出代码
// import domtoimage from 'dom-to-image';
import domtoimage from './dom-to-image';
import { PDFDocument } from 'pdf-lib'
import { sleep } from './secret';
/**
* 导出PDF
* @param {导出后的文件名} title
* @param {要导出的dom节点:react使用ref} ele
*/
const topdf = async (imgs, title) => {
const doc = await PDFDocument.create()
for (const key in imgs) {
if (Object.hasOwnProperty.call(imgs, key)) {
const element = imgs[key];
const image = await doc.embedPng(element)
const pdfPage = doc.addPage([image.width, image.height])
pdfPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
})
}
}
const pdfBytes = await doc.save()
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
var downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = `${title}.pdf`; // 设置下载文件的名称
downloadLink.click();
}
const exportPDF = async (title) => {
const pdfDoms = document.querySelectorAll('.pdfDom');
let newArr = []
for (let index = 0; index < pdfDoms.length; index++) {
let domitem = pdfDoms[index];
const options = {
scale: 4,
height: domitem.scrollHeight ,
width: domitem.scrollWidth ,
};
let canvas = await domtoimage.toPng(domitem,options)
newArr.push(canvas)
sleep(1000)
}
topdf(newArr,title)
}
export default exportPDF;