1、简要描述
上一篇博客主要讲的是pdf文件转换成canvas,然后进行相关的操作。本篇博客主要讲html中dom如何生成pdf文件(前端生成pdf),后端生成pdf当然也可以,原理也是将html网页通过后端服务导出成pdf,不深入讲,这里着重讲前端生成pdf。
2、相关插件及知识
还是使用的老朋友html2canvas和jspdf插件
1、jspdf
"jspdf": "^2.5.1"
使用方法:
import JsPDF from 'jspdf';const PDF = new jsPDF({unit: "mm", // 单位,本示例为mmformat: "a4", // 页面大小orientation: "portrait", // 页面方向,portrait: 纵向,landscape: 横向putOnlyUsedFonts: true, // 只包含使用的字体compress: true, // 压缩文档precision: 16, // 浮点数的精度
});// 或者const PDF = new JsPDF('p', 'mm', [210, 297]);<!-- 常用方法 -->
// 添加图片
PDF.addImage(imageData, // 此值可以为下面这些类型 string | HTMLImageElement | HTMLCanvasElement | Uint8Array | RGBAData'JPEG', // 转换后的格式x, // 被切割的imageData的横坐标y, // 被切割的imageData的纵坐标w, // 当前图片的宽度h, // 当前图片的高度
);
// 添加新的一页
PDF.addPage();
// 输出格式
PDF.output(type: "arraybuffer"): ArrayBuffer;
PDF.output(type: "blob"): Blob;
PDF.output(type: "bloburi" | "bloburl"): URL;
// 本地保存为pdf文件
PDF.save('lindadayo.pdf')
2、html2canvas
"html2canvas": "^1.4.1"
// 实例方法
html2canvas(dom, config).then(function(canvas) {})
config相关配置参考下图:
3、源码
1、dom结构
2、核心逻辑
这里为什么要将dom进行分区处理呢?请看第四点疑难解答
/*** 生成pdf* @param CommonPage 需要转换的dom节点* @param i 分区索引* @returns*/async generatePdf(CommonPage?: Element, childLen?: number) {PDF = new JsPDF('p', 'mm', [210, 297]); // pdf实例for (let i = 0; i < childLen; i++) {await asyncSingleAreaControl(CommonPage, i)}generateUploadPdf();},/*** 上传pdf文件*/async generateUploadPdf() {// 文件重命名const pdfName = pdfNameHandle()const uri = PDF.output('blob')const file = await blobUriToFile(uri, pdfName)// 此时的file是File类对象,你可以选择上传到服务器噢~当然你也可以选择直接导出到前端// PDF.output('lindadayo.pdf');},/*** 单个分区生成pdf操作* @param CommonPage 父节点dom* @param i 分区索引* @returns*/async asyncSingleAreaControl(CommonPage, i) {const canvas = await singleHandle(CommonPage, i)await areaPage(canvas, i)},/*** 分区pdf处理* @param canvas 各个分区dom转换后的canvas* @param areaNo 分区索引*/areaPage(canvas, areaNo) {// 是否是第一个分区(作用于是否开始就addPage)const isFirstArea = areaNo === 0return new Promise((resolve, _reject) => {// a4纸宽高const A4Origin = {width: PDF.internal.pageSize.getWidth(),height: PDF.internal.pageSize.getHeight()}const contentWidth = canvas.width;/*** html2canvas放大3.125倍时精度丢失导致多了2像素* 3368: 高度285mm纸张html2canvas放大300dpi后像素* 3366:正常实际高度*/const contentHeight = canvas.height <= 3368 ? 3366 : canvas.height;const pageHeight = Math.round(contentWidth / A4Origin.width * A4Origin.height);let leftHeight = contentHeight;let position = 0;const imgWidth = A4Origin.width;const imgHeight = Math.ceil(A4Origin.width / contentWidth * contentHeight);const pageData = canvas.toDataURL('image/jpeg', 1);// 非首个分区,得先addPage,因为不然会少一页 && 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (!isFirstArea && leftHeight > 0) {PDF.addPage()}while (leftHeight > 0) {PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));position -= A4Origin.height;leftHeight -= pageHeight// 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (leftHeight > 0) {PDF.addPage()}}resolve(true)})},/*** 单页pdf处理// * @param root 总节点* @param index 分区索引*/async singleHandle(CommonPage, index) {// 报错Unable to find element in cloned iframe解决方法// getDiv在外部声明, 内部赋值try {getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)const res = await html2canvas(getDiv, {useCORS: true,allowTaint: true,scale: 3.125}).then(function(canvas) {return canvas})return res} catch (e) {console.log(e)}}
4、疑难解答
1、为什么要对dom进行分区操作?
其实如果你不使用html2canvas的参数scale,就没必要进行分区,但是在很多时候,你不放大canvas的话,会导致pdf中的图片很模糊,还有锯齿,所以要对canvas进行方法,但是放大后,会导致一些问题:生成pdf后,超过15000px以后的dom会有样式丢失,所以得对dom进行分区操作,让每个分区的dom高度 * 放大倍数不超过15000px。我们一般都会导出a4纸大小,a4纸宽高是210mm*297mm,换算成像素是793.29px * 1122.52px,如果你选择放大两倍,那么,单页高度就是2245px,结论为一个分区能够放六个a4纸高度的dom,所以你在开发页面时,就要做好这种页面结构噢~
2、html2canvas仍然报图片出错/跨域的问题,即使后端oss已经解决跨域了
这个涉及知识点:img标签获取属于非跨域操作,Image类实例化属于跨域操作,所以得再html2canvas依赖中打补丁
/dist/html2canvas.js
3、报错Unable to find element in cloned iframe解决方法
在分区中循环处理dom生成canvas时会报出这种错误,原因是html2canvas第一参数的变量应该设置为全局变量而不应该是局部变量
try {getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)const res = await html2canvas(getDiv, {useCORS: true,allowTaint: true,scale: 3.125}).then(function(canvas) {return canvas})return res} catch (e) {console.log(e)}
--- 有问题可以随时评论噢~ ---