前言
在前端开发中遇到这样一个需求:页面上有一个 download
按钮,点击 download
按钮的时候,需要将当前页面的一部分下载到 pdf
保存。
以下将用到 html2canvas
和 jspdf
这两个库来实现将 HTML
页面转换为 pdf
的功能。
html2canvas
html2canvas 一个开源的 JavaScript 库,可以将 HTML 元素渲染成 canvas,并生成图片。原理是通过遍历页面 DOM 结构,收集所有元素信息及相应样式,渲染出 canvas image。
使用
// 安装
npm install html2canvas
html2canvas 的 API 很简单,以下是一个简单的使用:
import html2canvas from 'html2canvas';
html2canvas(element, {
// options
}).then(canvas => {
// canvas is the final rendered <canvas> element
})
jspdf
jspdf 一个用于在客户端生成 pdf 文件的JavaScript 库。
使用
// 安装
npm install jspdf
jspdf 使用 new jsPDF()
来创建 pdf,以下是一个简单的示例:
import { jsPDF } from "jspdf";
// Default export is a4 paper, portrait, using millimeters for units
const pdf = new jsPDF({
// options
});
pdf.text("Hello world!", 10, 10);
pdf.save("a4.pdf");
html2canvas + jspdf
使用 html2canvas 将 HTML 元素渲染成 canvas,使用 toDataURL
方法将 canvas 生成图片,再将图片按需求添加到 jspdf 创建的 pdf 中,最后保存。
下面是一个简单的 demo :
js 代码部分:
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
function downloadHandler(elementId) {
// elementId 为需要下载的元素的 id
const element = document.getElementById(elementId);
if (element) {
html2canvas(element, {
useCORS: true, // 允许加载跨域图片
onclone: (clone) => {
// clone 为克隆出来的整个页面的 DOM 节点
if (elementId) {
const cloneDOM = clone.getElementById(elementId);
// 将需要下载的元素的宽度调整为 A4 纸的宽度
// 注意:这里的操作不会修改原 DOM,因此会和页面渲染的不一致,如果需要保持和页面一致则不需要此操作
cloneDOM.style.width = `592.28px`;
}
},
}).then((canvas) => {
// canvas 渲染的是需要下载的元素
const pdf = new jspdf.jsPDF("p", "pt", [592.28, 841.89]); // 创建一个 pdf
const ctx = canvas.getContext("2d");
const margin = 30; // pdf 每页的边距
const a4w = 592.28 - 2 * margin,
a4h = 841.89 - 2 * margin; // pdf 内容的宽高
const imgHeight = Math.floor((a4h * canvas.width) / a4w); // 将 canvas 按 pdf 内容的高度分页,imgHeight 为分页的高度
let renderedHeight = 0; // 已经添加到 pdf 的高度
while (renderedHeight < canvas.height) {
// canvas 没有全部添加到 pdf
const page = document.createElement("canvas"); // 创建一个新的画布
// 将 canvas 切割后渲染到新的画布上
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight);
page
.getContext("2d")
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
);
// 将新画布生成图片并添加到 pdf 中
pdf.addImage(
page.toDataURL("image/jpeg", 1.0),
"JPEG",
margin,
margin,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
);
renderedHeight += imgHeight;
if (renderedHeight < canvas.height)
// 如果 canvas 没有被全部添加到 pdf,则新增一页空白 pdf
pdf.addPage();
page.remove(); // 移除新的画布
}
// 保存 pdf
pdf.save("demo.pdf");
});
}
}
踩坑记录
1. 图片跨域
在实际开发过程中可能会出现这样一个问题:客户端页面上的 https 图片能正常渲染,而 download 下来的 pdf 加载图片却跨域了。
排查问题发现,页面上的图片 url 都携带一个一次性的签名参数,客户端通过签名参数正常访问图片后,html2canvas 再去请求图片时签名参数失效了,所以跨域无法渲染图片。
解决办法:
第一步:设置储存图片的服务器允许跨域访问(* 为允许所有来源访问,也可以设置为指定的来源),Access-Control-Allow-Origin: *
第二步:设置 img 标签允许跨域渲染图片,crossOrigin="anonymous"
<img
src={img.accessUrl}
alt=""
width="100%"
crossOrigin="anonymous"
/>
第三步:设置 html2canvas 允许访问跨域图片,useCORS: true
html2canvas(element, { useCORS: true }).then(canvas => {})
完成以上三步,生成的 pdf 图片显示正常了。
2. 如何分页
一个简单的实现分页的思路:累计长图添加到 pdf 的高度,如果累计高度 < 长图高度,则继续将长图添加到新一页 pdf,否则已经添加完成。
以下是代码实现:
// 计算 canvas 等比变化后的宽度高度(宽度 = pdf 宽 - 2 * margin)
const imageSize = ImageSize.calculate(canvas.width, canvas.height);
let renderedHeight = 0; // 累计添加到 pdf 的高度
let position = 10; // y 坐标,针对页面上边缘
const pageData = canvas.toDataURL('image/jpeg', 1.0); // 长图
const pdf = new jsPDF('p', 'pt', A4Size.SIZE);
while (renderedHeight < imageSize.height) {
pdf.addImage(pageData, 'jpeg', ImageSize.MARGIN, position, imageSize.width, imageSize.height);
renderedHeight += A4Size.height;
position -= A4Size.height;
if (renderedHeight < imageSize.height) {
pdf.addPage();
}
}
由于 canvas 转成图片后是长图,imageSize.height > 每页 pdf 的高度,所以 position 为负数,为新的一页 pdf 开始的高度位置。
3. 如何设置每页的页边距
jsPDF addImage
方法可以通过 x、y 坐标来设置页面的左、上边距,右、下边距需要通过控制图片的宽高来实现。那么对于上面第二个问题的长图来说,从第二页开始就无法设置上下边距了。
这是因为添加到 pdf 每页的都是完整的长图,如果 position > 0 那么每页 pdf 看到的图片都是长图高度为 0 的位置开始。
因此,如果需要设置上下边距,只能将长图截断成适合每页 pdf 的高度,每次添加到 pdf 的都是截断出来的新的一张图片,如此可以实现页面上下边距:
const pdf = new jspdf.jsPDF('p', 'pt', [592.28, 841.89]); // 创建一个 pdf
const ctx = canvas.getContext('2d');
const margin = 30; // pdf 每页的边距
const a4w = 592.28 - 2 * margin, a4h = 841.89 - 2 * margin; // pdf 内容的宽高
const imgHeight = Math.floor(a4h * canvas.width / a4w); // 将 canvas 按 pdf 内容的高度切割,imgHeight 为切割的高度
let renderedHeight = 0; // 已经添加到 pdf 的高度
while (renderedHeight < canvas.height) { // canvas 没有全部添加到 pdf
const page = document.createElement("canvas"); // 创建一个新的画布
// 将 canvas 切割后渲染到新的画布上
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight);
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0);
// 将新画布生成图片并添加到 pdf 中
pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', margin, margin, a4w, Math.min(a4h, a4w * page.height / page.width));
renderedHeight += imgHeight;
if (renderedHeight < canvas.height) // 如果 canvas 没有被全部添加到 pdf,则新增一页空白 pdf
pdf.addPage();
page.remove(); // 移除新的画布
}