前言
最近工作中有需要将前端html导出为pdf的需求,之前是由后端导出,因为一些样式的问题,后来商量改为前端导出。调研了一段前端导出PDF的方法,发现大致有以下几种
- html2Canvas+jspdf
- 打印预览另存为pdf
- node服务端使用puppeteer
由于我的需求是需要导出局部html,且对node使用及部署不熟练,时间紧迫首选了第一种方案。但是网上大部分文章都提到了第一种方案的缺点:
- html2canvas插件在IE的兼容性问题
- 清晰度问题
- 分页问题
- 文字图片截断问题
所以我直接去看了下jspdf插件的文档及demo,发现也能满足自己的功能,只是网上单纯使用jspdf导出PDF的资料并不多,官方文档及demo也是很简单,所以本文将介绍如何使用jspdf来导出PDF,并解释一些常见问题和解决方案。
基本用法
以下是一个包含导出表格,文字,图片的基本使用法案例:
import jsPDF from 'jspdf';
import 'jspdf-autotable';
// 定义页面边距
const PAGE_MARGIN = 10;
// 创建一个新的PDF文档实例
const doc = new jsPDF({
unit: "mm", // 单位,本示例为mm
format: "a4", // 页面大小
orientation: "portrait", // 页面方向,portrait: 纵向,landscape: 横向
putOnlyUsedFonts: true, // 只包含使用的字体
compress: true, // 压缩文档
precision: 16, // 浮点数的精度
});
const text = "Page 1 - Hello, World!";
// 添加第一页内容
doc.text(text, PAGE_MARGIN, PAGE_MARGIN);
// 获取文本高度
const textHeight = doc.getTextDimensions(text).h;
// 添加表格
const data = [
["ID", "Name", "Age", "City"],
[1, "John Doe", 30, "New York"],
[2, "Jane Smith", 25, "Los Angeles"],
[3, "Bob Johnson", 45, "Chicago"],
[4, "Lisa Chen", 35, "San Francisco"],
[5, "Mike Lee", 50, "Miami"]
];
doc.autoTable({
startY: textHeight + PAGE_MARGIN, // 开始渲染表格的高度位置
head: [data[0]],
body: data.slice(1),
margin: { top: PAGE_MARGIN }, // 上边距
});
// 添加第二页内容
doc.addPage();
doc.text("Page 2 - Hello, World2!.", PAGE_MARGIN, PAGE_MARGIN);
// 保存PDF文件
doc.save("example.pdf");
以上示例即可完成一份简单的PDF导出功能了,只需要根据自己的内容进行不同的渲染即可。具体的api可以参考文档,接下来说一下我在使用中遇到的问题及解决方案。
注意事项
1.中文乱码问题
如果按照上述示例直接使用jspdf导出有中文文本的PDF,会发现有乱码的情况,这是因为jspdf默认情况下不支持中文字符集。我们可以通过添加中文字体库,来解决乱码问题。步骤如下:
-
下载思源黑体字体,下载地址:
https://github.com/Pal3love/Source-Han-TrueType/releases
。 -
转换字体,打开jspdf提供的在线字体转化网站:
https://rawgit.com/MrRio/jsPDF/master/fontconverter/fontconverter.html
。选择我们下载好的字体文件,不同的字体样式,选择不同的fontStyle。然后将转换后的文件扔到我们的项目下,并在使用的地方进行引用。
-
使用如下:
// 引入转换后字体文件
import '@/assets/SourceHanSerifCN-Regular-normal.js';
import jsPDF from 'jspdf';
import 'jspdf-autotable';
const doc = new jsPDF();
// 设置字体,第二个参数为fontStyle,引入的字体是什么fontStyle就设置成什么,如果这里要使用blod粗体,则需要再引入转换后的粗体字体文件
doc.setFont('SourceHanSerifCN-Regular', 'normal');
// 添加表格
const data = [
["ID", "Name", "Age", "City"],
[1, "John Doe", 30, "New York"],
[2, "Jane Smith", 25, "Los Angeles"],
[3, "Bob Johnson", 45, "Chicago"],
[4, "Lisa Chen", 35, "San Francisco"],
[5, "Mike Lee", 50, "Miami"]
];
doc.autoTable({
head: [data[0]],
body: data.slice(1),
styles: {
font: 'SourceHanSerifCN-Regular', // 设置表格字体
fontStyle: 'normal', // 设置表格字体样式
},
});
2.图片宽高缩放问题
在我这次导出PDF的需求中,我需要导出大量的图片在PDF中,所以不同的图片大小都需要根据页面的大小去进行比例换算进行缩放,我这里是一张图片一页,缩放计算如下:
// 定义页面边距
const PAGE_MARGIN = 10;
// 页面宽高
const width = doc.internal.pageSize.getWidth();
const height = doc.internal.pageSize.getHeight();
// 内容最大宽高
const pdfCentWidth = width - PAGE_MARGIN * 2;
const pdfCentHeight = height - PAGE_MARGIN * 2;
const imgUrl = 'https://images.pexels.com/photos/730981/pexels-photo-730981.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750';
// 获取图片宽高
const { width: imgWidth, height: imgHeight } = doc.getImageProperties(item);
// 换算比例
const widthRatio = pdfCentWidth / imgWidth;
const heightRatio = pdfCentHeight / imgHeight;
const ratio = Math.min(widthRatio, heightRatio);
// 得到图片宽高
const w = imgWidth * ratio;
const h = imgHeight * ratio;
// 添加图片
doc.addImage(
imgUrl,
'JPEG',
PAGE_MARGIN,
PAGE_MARGIN,
w,
h,
);
3.动态内容自动分页处理
由于页面中的内容都是根据后端数据动态渲染的,所以在导出PDF的时候,这些内容都有着不可固定的高度,我们需要计算这些高度来判断是否需要新增下一页。如下示例:
import jsPDF from 'jspdf';
import 'jspdf-autotable';
function generatePDF(data) {
const doc = new jsPDF();
// 定义页面边距
const PAGE_MARGIN = 10;
// 页面宽高
const width = doc.internal.pageSize.getWidth();
const height = doc.internal.pageSize.getHeight();
// 内容最大宽高
const pdfCentWidth = width - PAGE_MARGIN * 2;
const pdfCentHeight = height - PAGE_MARGIN * 2;
let startY = PAGE_MARGIN;
// 计算文本高度,并在需要时分页
const text = '测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本';
// splitTextToSize方法将文本分割为适合页面宽度的行
const splitText = doc.splitTextToSize(text, pdfCentWidth);
splitText.forEach((line) => {
// getTextDimensions计算每一行文本行高
const lineHeight = doc.getTextDimensions(line).h;
// 如果当前起点大于最大内容高度,则新增一页,并且初始化startY
if (startY + lineHeight > pdfCentHeight) {
doc.addPage();
startY = PAGE_MARGIN;
}
doc.text(line, PAGE_MARGIN, startY + lineHeight / 2);
startY += lineHeight;
});
// 间隔
startY += PAGE_MARGIN;
// 添加表格,并在需要时分页
const headers = [['Name', 'Email', 'Country']];
const body = data.map((item) => [item.name, item.email, item.country]);
doc.autoTable({
head: headers,
body: body,
startY: startY,
pageBreak: 'auto', // 自动分页
});
// 表格渲染结束的位置
const tableY = doc.lastAutoTable.finalY;
startY = tableY + PAGE_MARGIN;
// 添加表格
const imgUrl = 'https://images.pexels.com/photos/730981/pexels-photo- 730981.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750';
// 获取图片宽高
const { width: imgWidth, height: imgHeight } = doc.getImageProperties(item);
// 换算比例
const widthRatio = pdfCentWidth / imgWidth;
const heightRatio = pdfCentHeight / imgHeight;
const ratio = Math.min(widthRatio, heightRatio);
// 得到图片宽高
const w = imgWidth * ratio;
const h = imgHeight * ratio;
// 如果图片高度大于剩余高度,则新增一页
if (startY + h > pdfCentHeight) {
doc.addPage();
startY = PAGE_MARGIN;
}
// 添加图片
doc.addImage(
imgUrl,
'JPEG',
PAGE_MARGIN,
startY,
w,
h,
);
startY += h;
// 保存文件
doc.save('example.pdf');
}
4.图片过多且太大导致导出报错RangeError: Invalid string length
由于导出内容中有大量的图片,且都是未经过压缩的,所以出现了大量十几M的图片,在某次导出大量图片时出现了jspdf报错RangeError: Invalid string length,超出了 JavaScript 支持的最大字符串长度。经过查询文档,在 addImage
方法中,可以传递一个 compression
参数,用于指定对图片的压缩方式,常用的取值有:
NONE
: 不进行压缩,保持原始质量。FAST
: 使用快速的压缩方式,压缩比较低。MEDIUM
: 使用中等压缩方式,压缩比较平衡。SLOW
: 使用慢速的压缩方式,压缩比较高。
经过测试解决了当前问题,但是源头还是应该在上传图片时进行压缩,来保证资源的快速加载及导出速度优化。
总结
以上是我使用jspdf的经验,希望能够帮到想要使用纯前端导出PDF的你。可能我的场景很简单,但是我会在后续继续更新遇到的问题与解决方案。