前言
这几天接到一个需求,富文本编辑器的内容不仅要展示出来,还要实现展示的内容导出pdf文件。一开始导出pdf的功能是由后端来做的,然后发现对于宽度太大的图片,导出的pdf文件里部分图片内容被遮盖了,但在前端是正常显示的,只因为class样式后端无法解析。然后,后端开发人员就嫌麻烦,让前端来实现导出pdf的功能。。。
html2canvas(V1.4.1)
html2canvas 用于将 html 元素渲染成图像,可以将整个页面或特定区域以图像形式进行捕获。这对于将复杂的 html 结构转换为 PDF 格式非常有用,因为它可以捕获 html 中的样式、布局和图像等细节。
官网:https://html2canvas.hertzen.com/
引用
npm install html2canvas@1.4.1 --save
或
yarn add html2canvas@1.4.1
或
<script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
示例
<template>
<div id="pdfBody" style="margin: 30px;">
<input class="test_input" type="text" autocomplete="off" placeholder="输入框111"/>
<input class="test_btn" id="testBtn" type="button" value="按钮"/>
</div>
</template>
<script>
import html2canvas from "@/utils/htmlToPdf/html2canvas.min";
export default {
name: "Test",
mounted() {
document.getElementById('testBtn').addEventListener('click', () => {
this.canvasTest();
});
},
methods: {
createCanvas(dom) {
html2canvas(dom, {
useCORS: true, //允许跨域
scale: 2, //按比例增加分辨率
dpi: 200, //将分辨率提高到特定的DPI(每英寸点数)
ignoreElements: (e) => {
//过滤head、body等无用的标签元素
return !(e.contains(dom) || dom.contains(e) || e.tagName === 'STYLE' || e.tagName === 'LINK');
},
onclone: (doc) => {
//Scoped CSS无法直接应用,需要手动处理
let inputDocArr = doc.getElementsByClassName('test_input');
[].forEach.call(inputDocArr, inputDoc => {
inputDoc.style.height = '36px';
inputDoc.style.lineheight = '36px';
inputDoc.style.width = '200px';
inputDoc.style.margin = '20px';
inputDoc.style.borderRadius = '5px';
inputDoc.style.border = '1px solid #DCDFE6';
});
let btnDocArr = doc.getElementsByClassName('test_btn');
[].forEach.call(btnDocArr, btnDoc => {
btnDoc.style.color = '#FFFFFF';
btnDoc.style.backgroundColor = '#1890ff';
btnDoc.style.border = '1px solid #1890ff';
btnDoc.style.height = '36px';
btnDoc.style.width = '80px';
btnDoc.style.borderRadius = '20px';
});
}
}).then(canvas => {
dom.appendChild(canvas);
}).finally(() => {
console.log('create canvas finish!');
});
},
canvasTest() {
let pdfBody = document.getElementById('pdfBody');
this.createCanvas(pdfBody);
}
}
}
</script>
<style scoped>
.test_input {
height: 36px;
line-height: 36px;
width: 200px;
margin: 20px;
border-radius: 5px;
border: 1px solid #DCDFE6;
}
.test_input:focus {
border: 1px solid #1890ff;
outline: none;
}
.test_btn {
color: #FFFFFF;
background-color: #1890ff;
border: 1px solid #1890ff;
height: 36px;
width: 80px;
border-radius: 20px;
cursor: pointer;
}
</style>
配置项
如果想隐藏某个元素不让其显示出来,参考以下几种方案:
1、元素标签添加 data-html2canvas-ignore 属性,示例如下:
2、配置项中添加 ignoreElements 属性,手动过滤某个元素,示例如上代码;
3、配置项中添加 onclone 属性,手动处理某个元素的显示与隐藏,即:element.style.display = 'hidden'。
jsPDF(V1.5.3)
jsPDF 是一个 PDF 生成库,它允许你通过 JavaScript 代码创建和编辑 PDF 文档。
官网:https://github.com/parallax/jsPDF
引用
npm install jsPDF@1.5.3 --save
或
yarn add jsPDF@1.5.3
或
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
示例
<template>
<div id="pdfBody" style="margin: 30px;">
<h3>测试创建pdf文件!</h3>
<input id="testPdf" type="button" value="pdf测试"/>
</div>
</template>
<script>
import jsPDF from "@/utils/htmlToPdf/jspdf.min";
export default {
name: "Test",
mounted() {
document.getElementById('testPdf').addEventListener('click', () => {
this.pdfTest();
});
},
methods: {
pdfTest() {
let doc = new jsPDF({
orientation: 'p',
unit: 'mm',
format: 'a4'
});
doc.text("Hello world!", 10, 10);
doc.save("测试.pdf");
}
}
}
</script>
设置中文字体
设置英文内容是正常显示的,假若内容包含中文,会发现pdf文件里的内容显示乱码,如下图所示:
let doc = new jsPDF({
orientation: 'p',
unit: 'mm',
format: 'a4'
});
doc.text("pdf文件测试!", 10, 10);
doc.save("测试.pdf");
解决这一问题,可以使用 jsPDF 提供的 setFont(font) 方法,具体操作如下:
1、网上下载一个支持中文的字体tff文件,或者拷贝一份本地 window/font/ 路径下的字体文件(ps:有的字体不支持);
2、从GitHub上下载jsPDF源码,然后打开fontconverter目录下的fontconverter.html文件;
3、选择本地的tff文件,点击“Create”按钮,会生成一个js文件;
4、一般情况下,生成的js文件直接引入页面使用会报错,需要手动处理生成新的js文件;
默认生成的js文件内容如下:
(function (jsPDFAPI) {
var font = "XXXXXX";
var callAddFont = function () {
this.addFileToVFS("simfang.ttf", font);
this.addFont("simfang.ttf", "simfang", "normal");
};
jsPDFAPI.events.push(['addFonts', callAddFont])
})(jsPDF.API);
然后复制一份js文件,编辑内容如下格式:
export function addfont(pdf) {
let font = 'XXXXXX';
pdf.addFileToVFS('simfang.ttf', font);
pdf.addFont('simfang.ttf', 'simfang', 'normal');
}
5、将处理好的新的js文件放入项目中,然后在页面中引用,并设置字体。
<template>
<div id="pdfBody" style="margin: 30px;">
<h3>测试创建pdf文件!</h3>
<input id="testPdf" type="button" value="pdf测试"/>
</div>
</template>
<script>
import jsPDF from "@/utils/htmlToPdf/jspdf.min";
import {addfont} from '@/utils/htmlToPdf/simfang';
export default {
name: "Test",
mounted() {
document.getElementById('testPdf').addEventListener('click', () => {
this.pdfTest();
});
},
methods: {
pdfTest() {
let doc = new jsPDF({
orientation: 'p',
unit: 'mm',
format: 'a4'
});
//设置中文字体
addfont(doc);
doc.setFont('simfang');
doc.text("pdf文件测试撒大大大飒飒热通过如果还有就要让他阿松大尔特瑞特好几年,是否然后他又就很尴尬发射任务给他人用不上,二条天皇已经不是v堵塞惹我实在是肉体和set一年德国必须色让他管很多人把!", 10, 10);
doc.save("测试.pdf");
}
}
}
</script>
解决jsPDF文本不自动换行问题
中文乱码问题解决了,然后新的问题又出现了,文本不自动换行。解决方案参考如下:
1、使用 splitTextToSize() 方法:
let doc = new jsPDF({
orientation: 'p',
unit: 'mm',
format: 'a4'
});
//设置中文字体
addfont(doc);
doc.setFont('simfang');
let con = "pdf文件测试撒大大大飒飒热通过如果还有就要让他阿松大尔特瑞特好几年,是否然后他又就很尴尬发射任务给他人用不上,二条天皇已经不是v堵塞惹我实在是肉体和set一年德国必须色让他管很多人把!";
let splitCon = doc.splitTextToSize(con, 190);
console.log('splitCon',splitCon)
doc.text(splitCon, 10, 10);
doc.save("测试.pdf");
2、text() 方法参数添加选项 maxWidth :
let doc = new jsPDF({
orientation: 'p',
unit: 'mm',
format: 'a4'
});
//设置中文字体
addfont(doc);
doc.setFont('simfang');
let con = "pdf文件测试撒大大大飒飒热通过如果还有就要让他阿松大尔特瑞特好几年,是否然后他又就很尴尬发射任务给他人用不上,二条天皇已经不是v堵塞惹我实在是肉体和set一年德国必须色让他管很多人把!";
doc.text(con, 10, 10, {
maxWidth: 190
});
doc.save("测试.pdf");
jsPDF 库 API
文档地址:https://artskydj.github.io/jsPDF/docs/index.html
以下是常用API,仅供参考:
方法 | 说明 |
---|---|
var doc = new jsPDF(orientation, unit, format, compress); | 创建新文档: orientation:"l"(横向)、"p"(纵向); unit:"pt"、"mm"(默认)、"cm"、"m"、"in" or "px"; format:"a3"、"a4(默认)"、"a5"、"letter"、"legal"等 |
doc.addPage() | 添加一个空白页 |
doc.setPage(pageNumber) | 切换到第几个页面操作 |
doc.internal.getNumberOfPages() | 获取总页面数 |
doc.text(text, x, y, options); | 页面中添加文本: text:文本内容; x:距离页面左边的距离; y:距离页面上边的距离; options:可选参数配置 |
doc.setFont(fontName, fontStyle) | 页面文本设置字体: fontName:字体名称; fontStyle:字体风格,如"加粗" |
doc.setFontSize(size); | 设置字体大小 |
doc.setTextColor(ch1, ch2, ch3, ch4); | 设置文本颜色,可以是颜色码或rgb值 |
doc.addImage(imageData,format, x, y, width, height) | 在 PDF 文件中添加一个图像: imageData:图像的数据; format:图像的类型,如"JPEG"; x、y:分别表示图像左上角的坐标; width:图像的宽度; height:图像的高度 |
doc.save(filename) | 生成指定文件名的PDF文件 |
html2canvas + jsPDF
jsPDF 与 html2canvas 结合使用,可以将 html 元素渲染成图像,然后将图像插入到 jsPDF 创建的 PDF 文档中。
示例
import html2canvas from "@/utils/htmlToPdf/html2canvas.min";
import jsPDF from "@/utils/htmlToPdf/jspdf.min";
import {addfont} from '@/utils/htmlToPdf/simfang';
import {Loading} from "element-ui";
export function downloadPdf(title, dom) {
let reqLoading = Loading.service({
fullscreen: true,
text: '正在生成PDF文件......',
spinner: 'el-icon-loading',
background: 'rgba(0,0,0,0.5)'
});
html2canvas(dom, {
useCORS: true, //允许跨域
scale: 2, //按比例增加分辨率
dpi: 200, //将分辨率提高到特定的DPI(每英寸点数)
ignoreElements: (e) => {
return !(e.contains(dom) || dom.contains(e) || e.tagName === 'STYLE' || e.tagName === 'LINK');
}
}).then(canvas => {
// dom.appendChild(canvas);
// 新建JsPDF对象
let PDF = new jsPDF({
orientation: 'p', //参数: l:横向 p:纵向
unit: 'mm', //参数:测量单位("pt","mm", "cm", "m", "in" or "px")
format: 'a4', //A4纸
});
let ctx = canvas.getContext('2d');
let a4w = 190;
let a4h = 272; //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277,底部留5mm的页码显示位置,所以高度为272mm。
let imgHeight = Math.floor(a4h * canvas.width / a4w) - 2; //按A4显示比例换算一页图像的像素高度
let renderedHeight = 0;
//计算总页数
let pageCount = Math.ceil(canvas.height / imgHeight);
let page = document.createElement("canvas");
page.width = canvas.width;
//设置中文字体
addfont(PDF);
PDF.setFont('simfang');
while (renderedHeight < canvas.height) {
page.height = Math.min(imgHeight, canvas.height - renderedHeight); //可能内容不足一页
//用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0);
//canvas转图片数据保留10mm边距
PDF.addImage(page.toDataURL('image/jpeg', 1), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width));
renderedHeight += imgHeight;
//计算当前页及文字
let pageNumberText = '第' + (renderedHeight / imgHeight) + '页/共' + pageCount + '页';
//设置文字大小
PDF.setFontSize(4);
//设置文字颜色
PDF.setTextColor('#999');
//在页脚添加页码
PDF.text(pageNumberText, ((a4w + 8) / 2), (a4h + 16));
//判断是否分页,如果后面还有内容,添加一个空页
if (renderedHeight < canvas.height) {
PDF.addPage();
}
}
PDF.save(title + ".pdf");
}).finally(() => {
if (reqLoading) {
reqLoading.close();
}
});
}
常见问题
截取的图片显示空白
1、跨域问题
html2canvas 默认是不支持跨域图片的,对于跨域的图片默认是无法截取进去的,需要开启跨域配置:
- 将图片转成 base64 形式;
- 设置配置项 allowTaint: true 或 useCORS: true;
- img 标签中添加 crossOrigin="anonymous";
- 图片服务器配置 Access-Control-Allow-Origin: *
2、图片未加载完成
若图片未加载完成,此时截取的也是空白,解决方法就是设置延时一定事件后处理,或图片添加 load 事件,等图片加载完在进行截图。
3、需要截图的dom元素太多
若出现前面内容都正常截取,后面内容出现空白,大概率就是dom元素太多了。本人就碰到这种情况,即使延时10秒后截取,后面的部分还是空白,没办法,最后只能后端实现导出pdf功能了。。。
截图后部分css效果未正常显示
1、部分css样式 html2canvas 不支持
html2canvas 无法支持全部的css样式,关于支持哪些css样式或不支持哪些css样式,可以参考文档:https://html2canvas.hertzen.com/features
2、scoped 作用域的css无法应用
在vue中,为了避免css样式的交叉污染,都会使用 scoped 作用域的css样式,但是该作用域下的css样式无法在 html2canvas 中使用,解决方案可以在 onclone 配置项中过滤元素手动处理样式(详情可参考上述示例)
其他的问题可以参考博文:https://www.cnblogs.com/padding1015/p/9225517.html
总结
使用 html2canvas + jsPDF 纯前端导出pdf的方式还是有很多问题的,一般情况下都是后端进行文件的导出,前端配合解决样式问题。
另外我看还有使用 dom-to-image 或 modern-screenshot 的,不知道效果怎么样,有时间可以试一下。