【前端】jspdf+dom-to-image生成多页A4pdf加防截断处理 [ 前端 ]
晚安月亮 文章 正文
椰奶冻
{{nature("2024-05-29 15:27:46")}}更新首先,分析原始需求:点击导出为PDF文件时,弹出一个Modal框,预览生成的PDF文件,然后点击生成后下载PDF文件。
1. 预览时的文件是图片 or PDF or iFrame?
感觉预览图片会简单一点,还可以放大。那就用dom-to-image对DOM元素进行截图吧,然后上传图片url到oss,在预览窗口预览图片。
2. 下载时如何进行分页下载?
思路是将DOM图片按固定尺寸分批次调用 pdf.addPage() 和 pdf.addImage()方法生成pdf页面并插入剪切后的图片,如此循环操作,直至将整张图片遍历完毕后结束,最后调用pdf.save()方法进行保存
3. 分页截断怎么处理?
jspdf分页有个比较不好的地方就内容过长的时候虽然会虽然能做到分页,但是会把内容给截断,解决思路是给每个可能会被截断元素加上类,然后动态的计算该元素的位置是否在下一页和上一页之间,如果在的话就添加一个空白元素把这个元素给挤下去,这样就能实现
4.对于HTML生成PDF的前后端方案的分析
纯前端方案:
纯前端的方案,存在着浏览器环境依赖或一定的局限性, 一定程度上难以做到多端导出统一(也许可以,但是过于繁琐)。 以下是尝试过的方案:
1. printjs/window.print()
通过调取浏览器原生的打印功能进行打印,缺点: 对于自定义页眉页脚的自定义不友好。 需要用户手动打印/进行微调,对用户不够友好。
2. jsPDF + dom-to-image
实现是通过dom-to-image将HTML元素 转化 canvas 再转化成 JPED/PNG, 再通过jsPDF生成PDF文件。
优点: 生成符合完整符合样式的PDF,所见即所得,页头页尾自定义化高, 水印可以通过fixed布局元素生成。
缺点:需要手动计算分页点。 由于是通过转化成图片来生成PDF。 可以通过分割HTML元素来规避这个问题,但是操作会更加繁复。
最终选用jsPDF + dom-to-image的方案,和产品沟通后预览样式为dom(简单了很多)。html2Canvas也可以实现相同的效果,我这边用的是dom-to-image
实现原理:
动态计算每页dom元素的高度(元素margin会导致高度计算不准确,建议使用padding),将确保不被分割的dom 元素 加上特定的tag 标签(我这里用的是class='whole-node'),判断此dom 元素的最上面和最下面是否在同一页中,如果不在说明不处理就会被截断,怎么处理,不能被截断的元素上方插入对应的空白块占位,已达到将当前元素放到下一页的目的(当前页面高度 - dom元素最上方位置 = 需要插入空白块的高度)
import { jsPDF } from 'jspdf';
import { message } from 'antd';
import domtoimage from 'dom-to-image';
import moment from 'moment';
const convertPdf = (name: string, setLoading: any) => {
try {
const title = '文件名称';
const A4_WIDTH = 592.28;
const A4_HEIGHT = 880;
// 要生成的dom
const printDom: any = document.querySelector('#pdf_page');
const pageHeight = (printDom.offsetWidth / A4_WIDTH) * A4_HEIGHT;
const wholeNodes = document.querySelectorAll('.whole-node');
// 在不能被截断的元素上方插入对应的空白块占位
wholeNodes.forEach((node: any) => {
const topPageNum = Math.ceil(node.offsetTop / pageHeight);
const bottomPageNum = Math.ceil((node.offsetTop + node.offsetHeight) / pageHeight);
if (topPageNum !== bottomPageNum) {
// 说明dom会被截断
const divParent = node.parentNode;
const newBlock = document.createElement('div');
newBlock.className = 'emptyDiv';
const _H = topPageNum * pageHeight - node.offsetTop;
// 空白块高度
newBlock.style.height = _H + 60 + 'px';
divParent.insertBefore(newBlock, node);
}
});
domtoimage
.toPng(printDom)
.then((dataUrl) => {
const emptyDivs = document.querySelectorAll('.emptyDiv');
//dom 已经转换为canvas 对象,删除插入的空白块
emptyDivs.forEach((div: any) => div.parentNode.removeChild(div));
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const contentWidth = canvas.width;
const contentHeight = canvas.height;
const imgWidth = A4_WIDTH;
const imgHeight = (A4_WIDTH / contentWidth) * contentHeight;
const pageData = canvas.toDataURL('image/jpeg', 1.0);
const PDF = new jsPDF('portrait', 'pt', 'a4');
let position = 0;
let leftHeight = contentHeight;
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= A4_HEIGHT;
if (leftHeight > 0) {
PDF.addPage();
}
}
PDF.save(`${title}.pdf`);
message.success('保存成功');
setLoading(false);
};
})
.catch((error) => {
console.error('Error generating PDF:', error);
setLoading(false);
message.error('保存失败');
});
} catch (error) {
console.error('Error generating PDF:', error);
setLoading(false);
message.error('保存失败');
}
};
export default convertPdf;
参考文档
{{nature('2022-06-23 23:10:58')}} {{format('1317')}}人已阅读
{{nature('2023-02-03 16:12:08')}} {{format('1199')}}人已阅读
{{nature('2024-05-29 15:14:38')}} {{format('571')}}人已阅读
{{nature('2022-09-16 11:11:28')}} {{format('522')}}人已阅读
目录
标签云
一言
评论 1
{{userInfo.data?.nickname}}
{{userInfo.data?.email}}