导出PDF的实践
本篇主要介绍3种pdf导出的方法和优缺点
直奔实现方式:
方案一:jsPDF
方案二:html2canvas + jsPDF
方案三:pdfkit + svg-to-pdfkit + voilab-pdf-table
方案一:jsPDF
常规的由前端实现html导出为PDF文件,jsPDF即可,该插件提供了文本导出,图片导出,html导出,图形元素导出。我看到很多其他的文章中都提到改插件不支持中文编码,下面会给出解决的案例。
//以导出a4格式的PDF文件为例
//let pdf = new jsPDF('', 'pt', 'a4');
//文本导出
pdf.text('hello world !', 10, 10);
//图片导出,可以选择其他的图片格式,我这里给出的例子是jpeg格式,尝试过png,导出的文件非常大
//此处的canvas来源于html2canvas
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let pageData = canvas.toDataURL('image/jpeg', 1.0); //1.0表示图片清晰质量,0~1
const SIZE = [595.28,841.89]; //a4宽高
const WGUTTER = 20; //横向页边距
var imgWidth = SIZE[0]- WGUTTER*2; //图片在pdf中的宽
var imgHeight = imgWidth / contentWidth * contentHeight; //图片在pdf中的高
pdf.addImage(pageData, 'JPEG', WGUTTER, position, imgWidth, imgHeight );
//html导出
pdf.fromHTML(document.getElementById("id"), 10, 10);
//图形元素导出
pdf.line(20, 20, 60, 20); //直线
pdf.rect(20, 20, 10, 10); //矩形
//svg的导出,借助插件svg-to-pdfkit
SVGtoPDF(doc, svg, 0, 0);
SVGtoPDF(doc, svg, 0, 0, { fontCallback: () => fonturl });
关于jsPDF更多的例子请参考[版本有点旧]:
着重提一下关于中文乱码的问题
//text中文乱码,不做font编码处理直接这样写就会乱码
pdf.text('我很西瓜的瓜!', 10, 10);
//html中如果存在中文编码的话也会乱码
pdf.fromHTML(document.getElementById("id"), 10, 10);
//我这边是用到了一个叫做jsPDF-CustomFonts-support的jsPDF的支持库需要引入的文件如下
<script src="https://cdn.bootcss.com/jspdf/1.3.5/jspdf.min.js"></script>
<script language="javascript" type="text/javascript" src="../jspdf.customfonts.min.js"></script>
<script language="javascript" type="text/javascript" src="../default_vfs.js"></script>
var pdf = new jsPDF('', 'pt', 'a4');
pdf.addFont("msyh.ttf", 'yh', 'normal');
pdf.setFont("yh");
pdf.text("我很西瓜的瓜",10,10);
pdf.save('content.pdf');
npm install jsPDF-CustomFonts-support里面没有default_vfs.js文件,需要根据自己的需要生成,我这里添加的是微软雅黑,请参考:
点评:中文的支持度不高,上面提到的中文乱码解决方法支持到pdf.text(),还不支持pdf.fromHTML(),而且pdf.addFont()会降低pdf导出的性能。
方案二:html2canvas + jsPDF
各自的习惯不同,有的同学喜欢局部图表的导出,即单张单张图表生成图片然后添加到pdf文件对象里,我建议生成一张图片然后添加到pdf文件对象里的方式,这样布局更好控制。生成图片的优点有二,避开中文乱码的坑,布局可控。
缺点1:html2canvas将html转化为canvas并不能100%还原dom结构
html2canvas通过遍历页面DOM结构,收集所有元素信息及对应样式,渲染出canvas image。它只能将它能处理的元素和样式还原生成canvas image,对于不支持的样式,渲染还原效果不佳,不能100%还原dom结构。
//例1
dom结构很深的情况下,有的时候会发现连最基础的文本都无法生成,如果遇到这种情况,尝试把<div>标签改成<span>标签,
如果可以显示文本,再添加样式{display:inline-block;width:100%;}
一些高级的样式,比如
display: -webkit-box;
-webkit-box-flex: 3;
类似上面这种高级的样式canvas是不支持的,如果有用到这种样式需要找替代的方式处理。
关于支持性,这是官方给出的支持报告:
缺点2:需要手动计算来分页
当我们使用html2canvas插件将一个元素渲染成一张图片并将它添加到pdf对象中时,就带来了分页的问题,pdf对象的分页并非自动添加,而是需要我们通过计算图片的高度手动执行addPage()方法,即便如此,也还是存在分页异常的问题,比如说图表被截断,表格,甚至文字被截断,我实现的方式如下:
const SIZE = [595.28,841.89]; //a4宽高
let node = document.getElementById("pdf_content");
let nodeH = node.clientHeight;
let nodeW = node.clientWidth;
let pageH = nodeW / SIZE[0] * SIZE[1];
let modules = node.childNodes;
let pageFooterH = 50; //50为页尾的高度
this.addCover(node.childNodes[0],pageH); //添加封面
this.addPageFooter(node.childNodes[1],1,0); //添加页尾
this.addPageHeader(node.childNodes[2]); //添加页头
for(let i = 0,len = modules.length;i < len;i++){
let item = modules[i];
//div距离body的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1
let beforeH = item.offsetTop + pageFooterH;
let afterH = beforeH + item.clientHeight + pageFooterH;
let currentPage = parseInt(beforeH/pageH);
if(currentPage != parseInt(afterH/pageH)){
//上一个元素底部距离body的高度
let lastItemAftarH = modules[i-1].offsetTop + modules[i-1].clientHeight;
let diff = pageH - lastItemAftarH%pageH - pageFooterH;
//加页尾
this.addPageFooter(item,currentPage+1,diff);
//加页头
this.addPageHeader(item);
if(i == len-1){
let diff = pageH - afterH%pageH + 20; //50为页尾的高度
//加页尾
this.addPageFooter(item,currentPage+1,diff,true);
}
说明:node对象下的每一个一级子元素都被当作一个不可被分割的整体,遍历这些子元素,如果某个元素距离body的高度是pageH的倍数x,当加上该元素自身的高度时是pageH的倍数x+1,那么说明x这一页放不下该元素,该元素需要被放在x+1页上,利用marginTop设置一个留白区域即可。
将node元素生成pdf文件
const WGUTTER = 20; //横向页边距
html2canvas(node).then(function(canvas) {
var contentWidth = canvas.width;
var contentHeight = canvas.height;
//一页pdf显示html页面生成的canvas高度;
var pageHeight = contentWidth / SIZE[0] * SIZE[1];
//未生成pdf的html页面高度
var leftHeight = contentHeight;
//html页面生成的canvas在pdf中图片的宽高
var imgWidth = SIZE[0]- WGUTTER*2;
var imgHeight = imgWidth / contentWidth * contentHeight;
var pageData = canvas.toDataURL('image/jpeg', 1.0);
var pdf = new jsPDF('', 'pt', 'a4', true);
//pdf页面竖向偏移
var position = 0;
if (leftHeight < pageHeight) {
pdf.addImage(pageData, 'JPEG', WGUTTER, position, imgWidth, imgHeight, 'FAST' );
} else {
while(leftHeight > 0) {
pdf.addImage(pageData, 'JPEG', WGUTTER, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= SIZE[1];
//避免添加空白页
if(leftHeight > 0) {
pdf.addPage();
pdf.save('content.pdf');
}
在合理分页的基础上,我们可以添加封面,页头,页尾。
添加封面:
addCover = (node,pageH) => {
let cover = document.createElement("div");
cover.className = "c-page-cover";
cover.style.height = (pageH-50)+"px";
cover.innerHTML = `<img src="./img/logo.png" />
<table>
<tbody>
<tr><td>pdf name</td></tr>
<tr><td>pdf报告生成时间</td></tr>
</tbody>
</table>`;
node.parentNode.insertBefore(cover,node);
}
添加页头:
addPageHeader = (item) => {
let pageHeader = document.createElement("div");
pageHeader.className = "c-page-head";
pageHeader.innerHTML = "页头内容";
item.parentNode.insertBefore(pageHeader,item);
}
添加页尾:
addPageFooter = (item,currentPage,diff,isLastest) => {
let pageFooter = document.createElement("div");
pageFooter.className = "c-page-foot";
pageFooter.innerHTML = "第 "+ currentPage +" 页 ";
isLastest?item.parentNode.insertBefore(pageFooter,null):item.parentNode.insertBefore(pageFooter,item);
pageFooter.style.marginTop = diff+"px";
pageFooter.style.marginBottom = "10px";
}
缺点3:生成的pdf文件比较大,一页将近1MB
顺便提一下将生成的pdf上传到服务器而不是下载到本地的做法:
var datauri = pdf.output('dataurlstring');
然后把这个字符串送到后台解密
后台主要用BASE64Decoder解密,放到文件流里,生成文件,下次直接访问
上传到服务端,不直接在客户端导出,但是生成的文件都比较大,不建议上传到服务端,浏览器上传文件1M以上就容易崩溃,上传到服务端只要改写最后一句即可,后端支持下,把base64编码解码存入文件流生成文件:
方案三:pdfkit + svg-to-pdfkit + voilab-pdf-table
该方案既适用于前端也适用于后端,支持中文
前端:
//需要引入的js文件
<script src="../pdfkit.js"></script>
<script src="../blobstream.js"></script>
<script src="../svg-to-pdfkit.js"></script>
<script src="../voilab-pdf-table.js"></script>
const SIZE = [595.28,841.89]; //a4宽高
let pdf = new PDFDocument({compress: false});
pdf.font('./pdfRelay/msyh.ttf');
//添加svg
let svgs = node.getElementsByTagName("svg");
for (let i = 0; i < svgs.length; i++) {
SVGtoPDF(pdf, svgs[i].outerHTML || svgs[i], 0, 0);
if (i !== svgs.length - 1) {
pdf.addPage();
//添加表格
let keys = ["k1","k2","k3"];
let columns = [];
let width = (SIZE[0]-60*2)/keys.length;
for(var i=0,len=keys.length;i<len;i++){
columns.push({
id: keys[i],
header: keys[i],
width:width,
let table = new pdfTable(pdf, {bottomMargin: 30});
table.setColumnsDefaults({
headerBorder: ['L','T','B','R'],
headerPadding: [5, 0, 5, 0],
border: ['L','T','B','R'],
align: 'center',
fontSize: "10px",
padding: [5, 0, 5, 0],
.addColumns(columns)
.onPageAdded(function (tb) {
tb.addHeader();
table.addBody(data);
//生成文档
let stream =pdf.pipe(blobStream());
stream.on('finish', function() {
let blob = stream.toBlob('application/pdf');
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.download = 'content.pdf';
a.href = url;
a.click();
URL.revokeObjectURL(url);
node的实现方式:
const PDF = require("pdfkit");
const SVGtoPDF = require('svg-to-pdfkit');
const pdfTable = require('voilab-pdf-table');
const fs = require('fs');
const fonturl = './ttf/msyh.ttf';
let pdf = new PDF();
pdf.font(fonturl);
let stream = fs.createWriteStream('./content.pdf');
pdf.pipe(stream);
pdf.addPage();
pdf.image("./img/logo.png",100,100,{width:100});
pdf.addPage();
pdf.text(data,0,0);
//svg,处理中文乱码
pdf.addPage();
SVGtoPDF(pdf, svg对象或者html, 0, 0, { fontCallback: () => fonturl });
//table
pdf.addPage();
let table = new pdfTable(pdf, {bottomMargin: 30});
table.setColumnsDefaults({
headerBorder: ['L','T','B','R'],
headerPadding: [5, 0, 5, 0],
border: ['L','T','B','R'],
align: 'center',
fontSize: "10px",
padding: [5, 0, 5, 0],
.addColumns(columns)