网页
html
生成A4大小分页的
pdf
,翻遍了整个互联网发现没有很系统的整理与分析,甚至对
jsPDF
的解析也没有几篇。遇到过几次,用的比较多,完成代码编写后特此整理分析,自我记录。
存在图片/组件/文字被分割的现象,即分页处理
包括页头、页脚、上下安全间隔的情况
富文本分页情况
通过
深度搜索优先遍历
,从顶部遍历需要转换的
HTML
节点, 并将节点分为三种情况进行处理(
1. 普通节点。2. 需要进行分页处理并且内部可能包含也需要分页处理子节点的节点。3. 需要进行分页内部不包含需要分页处理的节点,即深度搜索的终点节点
),通过
从高到低
遍历维护一个
分页数组pages
,该数组记录每一页的起始位置,如:
pages[0]
对应
第一页起始位置
,
pages[1]
对应
第二页起始位置
图解如下:
通过深度遍历后得出每页起始位置的数组,遍历数组,通过
jspdf
的
addImage
接口对
canvas
进行画面截取,由于
addImage
只能
固定位置的左上角起始点
,不能进行
非常精确的上下定位截取
(下一节会详解
addImage
),会造成
截取多余的内容
(如上图
页面1
中
pages[1]
下方的内容会和
页面2
中
pages[1]
下方的内容会一样(除长度外),而
页面1
中
pages[1]
下方的内容是多余的(是属于
页面2
的内容))因此需要对页面不需要的内容
使用jspdf的addBlank进行空白遮挡处理
。
jsPDF.addImage
详解
官方文档链接
addImage - Documentation (artskydj.github.io)
需要注意的点是
坐标(x,y)
的取值, (x,y)对应的是添加图片的左上角取值,宽高则是根据转化成canvas的宽高取值,图解如下
因此在对一个长图片进行截取时,往往将y值设未负数,让需要截取图片的起始位置落于当前的pdf页面内,在当前案例下,每一页的图片摆放坐标
y = -pages[i]
jsPDF.rect
详解
文档链接
context2d - Documentation (artskydj.github.io)
该接口的参数
(x,y)坐标、宽高
与addImage接口的一致
当前pdf页需要的内容的高度为
pages[i] - pages[i-1]
, 除去顶部这个高度外以下的内容都是不需要的,因此得到每一页添加空白的
y坐标值为
- pages[i] - pages[i-1]
,高度h为
一页pdf的高度(此处为A4页的高度) - pages[i] - pages[i-1]
,宽度为A4宽度,x为0, 图解如下:
深度优先遍历三种类型的节点
通过深度优先遍历操作,可以从高到低去遍历需要进行跨页判断的元素,检测是否跨页,并记录分页点,从而避免跨页问题。
1. 普通节点
当遍历到普通节点,即不需要进行分页判断的节点时,只需要进行
2步操作
:
当前节点距离顶部的高度 - pages最后一位元素的值(即上一页的分界点)得出的差值是否 大于 页面的高度
, 如果大于,则证明当前节点已经跨页,进行操作
pages.push(pages[pages.length - 1] + 一页PDF的高度)
对子节点进行深度遍历
2. 需要进行跨页判断,且内部也含有
可能跨页/需要进行跨页判断
的节点
当元素进行到该类型的节点时, 需要进行
3步操作
:
需要进行与
普通节点第一步
相同的判断
(检测当前节点距离顶部的距离 + 节点自身的高度) 是否大于 (
pages
最后一位元素(即当前页 顶部位置) + 一页PDF的高度(当前指A4的高度))
如果条件为真,则证明该节点属于跨页元素,
距离页面顶部距离的值
top
是分页点,往
pages
中
push
top
且由于内部还存在需要进行跨页检测的节点,因此需要对子节点进行深度遍历
3. 需要进行跨页判断,但内部不含有
可能跨页/需要进行跨页判断
的节点, 即深度终点
该节点只需要进行 内部含有
可能跨页/需要进行跨页判断
的节点 的第一第二步操作, 由于内部不再含有,因此不需要遍历子节点,为搜索的叶子节点。
html2Canvas生成图片模糊导致导出的PDF也模糊的问题
通过
scale
参数, 对
canvas
进行等比放大,可以使
canvas
生成的图片更清晰。
async function toCanvas(element, width) {
const canvas = await html2canvas(element, {
scale: window.devicePixelRatio * 3
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const height = (width / canvasWidth) * canvasHeight;
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
return { width, height, data: canvasData };
样例及代码
gitee仓库: output_pdf_demo: jsPDF + html2canvas , 网页HTML导出A4格式PDF 处理分页切割问题 (gitee.com)
npm install
& npm run serve
即可运行
分页效果:
富文本分页:
table行分页:
组件分页:
样例注意事项
样例比上述讲的情况内,引入了页眉、页脚、还有上下左右间距的情况,图解如下:
需要做的额外处理:
图片摆放的Y坐标由原来的-pages[i]
变成了 baseY + 页头元素高度 - pages[i]
中间实际内容部分与页眉/页脚之间的边距也需要进行遮白处理
内容的高度才为PDF页面的实际高度,判断分页的依据应该以内容高度为准
富文本文字的分页处理
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import { message } from 'ant-design-vue';
const A4_WIDTH = 592.28;
const A4_HEIGHT = 841.89;
async function toCanvas(element, width) {
const canvas = await html2canvas(element, {
scale: window.devicePixelRatio * 2
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const height = (width / canvasWidth) * canvasHeight;
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
return { width, height, data: canvasData };
* 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
* @param {Object} param
* @param {HTMLElement} param.element - 需要转换的dom根节点
* @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
* @param {string} [param.filename='document.pdf'] - pdf文件名
* @param {HTMLElement} param.header - 页眉dom元素
* @param {HTMLElement} param.footer - 页脚dom元素
export async function outputPDF({ element, contentWidth = 550,
footer, header, filename = "测试A4分页.pdf" }) {
if (!(element instanceof HTMLElement)) {
return;
const pdf = new jsPDF({
unit: 'pt',
format: 'a4',
orientation: 'p',
const { width, height, data } = await toCanvas(element, contentWidth);
async function addHeader(header, pdf, contentWidth) {
const { height: headerHeight, data: headerData, width: hWidth } = await toCanvas(header, contentWidth);
pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, headerHeight);
async function addFooter(pageNum, now, footer, pdf, contentWidth) {
const newFooter = footer.cloneNode(true);
newFooter.querySelector('.pdf-footer-page').innerText = now;
newFooter.querySelector('.pdf-footer-page-count').innerText = pageNum;
document.documentElement.append(newFooter);
const { height: footerHeight, data: footerData, width: fWidth } = await toCanvas(newFooter, contentWidth);
pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - footerHeight, contentWidth, footerHeight)
function addImage(_x, _y, pdf, data, width, height) {
pdf.addImage(data, 'JPEG', _x, _y, width, height);
function addBlank(x, y, width, height, pdf) {
pdf.setFillColor(255, 255, 255);
pdf.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');
const { height: tfooterHeight } = await toCanvas(footer, contentWidth)
const { height: theaderHeight } = await toCanvas(header, contentWidth);
const baseX = (A4_WIDTH - contentWidth) / 2;
const baseY = 15;
const originalPageHeight = (A4_HEIGHT - tfooterHeight - theaderHeight - 2 * baseY);
const elementWidth = element.offsetWidth;
const rate = contentWidth / elementWidth
const pages = [rate * getElementTop(element)];
function getElementTop(element) {
let actualTop = element.offsetTop;
let current = element.offsetParent;
while (current && current !== null) {
actualTop += current.offsetTop;
current = current.offsetParent;
return actualTop;
function traversingNodes(nodes) {
for (let i = 0; i < nodes.length; ++i) {
const one = nodes[i];
const isDivideInside = one.classList && one.classList.contains('divide-inside');
const isIMG = one.tagName === 'IMG';
const isTableCol = one.classList && ((one.classList.contains('ant-table-row')));
const isEditor = one.classList && (one.classList.contains('editor'));
let { offsetHeight } = one;
let offsetTop = getElementTop(one);
const top = rate * (offsetTop)
if (isDivideInside) {
updatePos(rate * offsetHeight, top, one);
traversingNodes(one.childNodes);
else if (isTableCol || isIMG) {
updatePos(rate * offsetHeight, top, one);
else if (isEditor) {
updatePos(rate * offsetHeight, top, one);
traversingEditor(one.childNodes)
else {
updateNomalElPos(top)
traversingNodes(one.childNodes);
return;
function traversingEditor(nodes) {
for (let i = 0; i < nodes.length; ++i) {
const one = nodes[i];
let { offsetHeight } = one;
let offsetTop = getElementTop(one);
const top = contentWidth / elementWidth * (offsetTop)
updatePos(contentWidth / elementWidth * offsetHeight, top, one);
function updateNomalElPos(top) {
if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) {
pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
function updatePos(eheight, top) {
if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {
pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
else if ((top + eheight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) && (top != (pages.length > 0 ? pages[pages.length - 1] : 0))) {
pages.push(top);
traversingNodes(element.childNodes);
if (pages[pages.length - 1] + originalPageHeight < height) {
pages.push(pages[pages.length - 1] + originalPageHeight);
for (let i = 0; i < pages.length; ++i) {
message.success(`共${pages.length}页, 生成第${i + 1}页`)
addImage(baseX, baseY + theaderHeight - pages[i], pdf, data, width, height);
addBlank(0, theaderHeight, A4_WIDTH, baseY, pdf);
addBlank(0, A4_HEIGHT - baseY - tfooterHeight, A4_WIDTH, baseY, pdf);
if (i < pages.length - 1) {
const imageHeight = pages[i + 1] - pages[i];
addBlank(0, baseY + imageHeight + theaderHeight, A4_WIDTH, A4_HEIGHT - (imageHeight), pdf);
await addHeader(header, pdf, A4_WIDTH)
await addFooter(pages.length, i + 1, footer, pdf, A4_WIDTH);
if (i !== pages.length - 1) {
pdf.addPage();
return pdf.save(filename)
参考文档及博文
jsPDF - Documentation (artskydj.github.io)
配置型 | HTML2CANVAS 中文文档 (allenchinese.github.io)
【原创】jspdf+html2canvas生成多页pdf防截断处理 - 简书 (jianshu.com)