首页 > 基础资料 博客日记
使用html2canvas和jspdf生成的pdf 防止文字被切割
2024-08-10 05:00:08基础资料围观420次
Java资料网推荐使用html2canvas和jspdf生成的pdf 防止文字被切割这篇文章给大家,欢迎收藏Java资料网享受知识的乐趣
解决内容被切断问题
支持页眉页脚页码
计算一页高度,在高度位置扫描像素点判断是否纯色 是的话在此切割不是就往上一行
import jsPDF from "jspdf";
import html2canvas from "html2canvas";
/**
* 生成pdf(处理多页pdf截断问题)
* @param {Object} param
* @param {HTMLElement} param.element - 需要转换的dom根节点
* @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-595
* @param {number} [param.contentHeaderWidth=550] - 一页pdf的内容宽度,0-595
* @param {number} [param.contentFooterWidth=550] - 一页pdf的内容宽度,0-595
* @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-842
* @param {string} [param.outputType='save'] - 生成pdf的数据类型,默认是save下载下来,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
* @param {number} [param.scale=window.devicePixelRatio * 2] - 清晰度控制,canvas放大倍数,默认像素比*2
* @param {string} [param.direction='p'] - 纸张方向,l横向,p竖向,默认A4纸张
* @param {string} [param.fileName='document.pdf'] - pdf文件名,当outputType='file'时候,需要加上.pdf后缀
* @param {number} param.baseY - pdf页内容距页面上边的高度,默认 15px
* @param {HTMLElement} param.header - 页眉dom元素
* @param {HTMLElement} param.footer - 页脚dom元素
* @param {string} [param.isPageMessage=false] - 是否显示当前生成页数状态
* @param {string} [param.isTransformBaseY=false] - 是否将baseY按照比例缩小(一般固定A4页边距时候可以用上)
* @returns {Promise} 根据outputType返回不同的数据类型,是一个对象,包含pdf结果及需要计算的元素位置信息
*/
export class PdfLoader {
constructor(element, param = {}) {
if (!(element instanceof HTMLElement)) {
throw new TypeError("element节点请传入dom节点");
}
this.element = element;
this.contentWidth = param.contentWidth || 550;
this.outputType = param.outputType || "save";
this.fileName = param.fileName || "导出的pdf文件";
this.scale = param.scale;
this.baseY = param.baseY == null ? 15 : param.baseY;
this.isTransformBaseY = param.isTransformBaseY || false;
this.header = param.header;
this.footer = param.footer;
this.isPageMessage = param.isPageMessage;
this.direction = param.direction || "p"; // 默认竖向,l横向
this.A4_WIDTH = 595; // a4纸的尺寸[595,842],单位像素
this.A4_HEIGHT = 842;
this.contentFooterWidth = param.contentHeaderWidth || 550;
this.contentHeaderWidth = param.contentFooterWidth || 550;
if (this.direction === "l") {
// 如果是横向,交换a4宽高参数
[this.A4_HEIGHT, this.A4_WIDTH] = [this.A4_WIDTH, this.A4_HEIGHT];
}
// 页眉页脚高度
this.pdfFooterHeight = 0;
this.pdfHeaderHeight = 0;
this.pdf = null;
this.rate = 1; // 缩放比率
this.pages = []; // 当前分页数据
}
/**
* 将元素转化为canvas元素
* @param {HTMLElement} element - 当前要转换的元素
* @param {width} width - 内容宽度
* @returns
*/
async createAndDisplayCanvas() {
let imgData = await this.toCanvas(this.element, this.contentWidth);
let canvasEle = document.createElement("canvas");
canvasEle.width = imgData.width;
canvasEle.height = imgData.height;
canvasEle.style.position = "fixed";
canvasEle.style.top = "0";
canvasEle.style.right = "0";
this.canvasEle = canvasEle;
document.body.appendChild(canvasEle);
const ctx = canvasEle.getContext("2d");
const img = await this.loadImage(imgData.data);
ctx.drawImage(img, 0, 0, imgData.width, imgData.height);
this.scan(ctx, imgData);
}
/**
* 加载图片资源
* @param url 图片资源链接
* @returns 返回Promise对象,当图片加载成功时resolve,否则reject
*/
loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.setAttribute("crossOrigin", "anonymous");
img.src = url;
img.onload = () => {
// 当图像加载完成后进行resolve
resolve(img);
};
img.onerror = () => {
reject(new Error("图像加载失败"));
};
});
}
/**
* 扫描图像并确定每一页的起始高度
* @param ctx 绘制上下文
* @param imgData 图像数据
* @throws 当ctx或imgData为null/undefined时抛出错误
*/
scan(ctx, imgData) {
if (!ctx || !imgData) {
throw new Error("Invalid arguments: ctx or imgData is null/undefined");
}
let originalPageHeight = parseInt(this.originalPageHeight, 10);
let shouldContinueScanning = true;
while (shouldContinueScanning) {
let imageData = ctx.getImageData(0, originalPageHeight, imgData.width, 1);
const uniqueArr = Array.from(new Set(imageData.data));
if (uniqueArr.length === 1) {
this.pages.push(originalPageHeight);
originalPageHeight += parseInt(this.originalPageHeight, 10);
if (originalPageHeight > imgData.height) {
shouldContinueScanning = false;
if (this.canvasEle) {
this.canvasEle.remove();
this.canvasEle = null;
}
}
} else {
if (originalPageHeight == this.pages.at(-1)) {
// 防止无限递减
shouldContinueScanning = false;
} else {
originalPageHeight = Math.max(0, originalPageHeight - 1); // 防止originalPageHeight变为负数
}
}
}
}
async toCanvas(element, width) {
// canvas元素
let canvas = await html2canvas(element, {
allowTaint: true, // 允许渲染跨域图片
scale: this.scale || window.devicePixelRatio * 2, // 增加清晰度
useCORS: true, // 允许跨域
});
// 获取canvas转化后的宽度
const canvasWidth = canvas.width;
// 获取canvas转化后的高度
const canvasHeight = canvas.height;
// 高度转化为PDF的高度
const height = (width / canvasWidth) * canvasHeight;
// 转化成图片Data
const canvasData = canvas.toDataURL("image/jpeg", 1.0);
canvas = null;
return { width, height, data: canvasData };
}
/**
* 生成pdf方法,外面调用这个方法
* @returns {Promise} 返回一个promise
*/
getPdf() {
// 滚动置顶,防止顶部空白
window.pageYOffset = 0;
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
return new Promise(async (resolve, reject) => {
// jsPDF实例
const pdf = new jsPDF({
unit: "pt", // mm,pt,in,cm
format: "a4",
orientation: this.direction,
});
this.pdf = pdf;
let pdfFooterHeight = 0;
let pdfHeaderHeight = 0;
// 距离PDF左边的距离,/ 2 表示居中 ,,预留空间给左边, 右边,也就是左右页边距
let baseX = (this.A4_WIDTH - this.contentWidth) / 2;
// 距离PDF 页眉和页脚的间距, 留白留空
let baseY = this.baseY;
// 元素在网页页面的宽度
const elementWidth = this.element.scrollWidth;
// PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度 转化为 距离Canvas顶部的高度
const rate = this.contentWidth / elementWidth;
this.rate = rate;
if (this.isTransformBaseY) {
this.baseY = baseY = baseY * rate;
}
// 页脚元素 经过转换后在PDF页面的高度
if (this.footer) {
pdfFooterHeight = (
await this.toCanvas(this.footer, this.contentFooterWidth)
).height;
this.pdfFooterHeight = pdfFooterHeight;
}
// 页眉元素 经过转换后在PDF的高度
if (this.header) {
pdfHeaderHeight = (
await this.toCanvas(this.header, this.contentHeaderWidth)
).height;
this.pdfHeaderHeight = pdfHeaderHeight;
}
// 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
const originalPageHeight =
this.A4_HEIGHT - pdfFooterHeight - pdfHeaderHeight - 2 * baseY;
this.originalPageHeight = originalPageHeight;
this.pages = [0]; // 要从0开始
// 计算分页
await this.createAndDisplayCanvas();
const pages = this.pages;
const { width, height, data } = await this.toCanvas(
this.element,
this.contentWidth
);
// 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况
if (pages[pages.length - 1] + originalPageHeight < height) {
pages.push(pages[pages.length - 1] + originalPageHeight);
}
// 根据分页位置 开始分页生成pdf
for (let i = 0; i < pages.length; ++i) {
if (this.isPageMessage) {
// Message.success(`共${pages.length}页, 生成第${i + 1}页`);
}
// 页眉高度
let pdfHeaderH = pdfHeaderHeight;
// 页脚高度
let pdfFooterH = pdfFooterHeight;
// 根据分页位置新增图片,要排除页眉和顶部留白
this.addImage(
baseX,
baseY + pdfHeaderH - pages[i],
pdf,
data,
width,
height
);
// 将 内容 与 页眉之间留空留白的部分进行遮白处理
this.addBlank(0, pdfHeaderH, this.A4_WIDTH, baseY, pdf);
// 将 内容 与 页脚之间留空留白的部分进行遮白处理
this.addBlank(
0,
this.A4_HEIGHT - baseY - pdfFooterH,
this.A4_WIDTH,
baseY,
pdf
);
// 对于除最后一页外,对 内容 的多余部分进行遮白处理
if (i < pages.length - 1) {
// 获取当前页面需要的内容部分高度
const imageHeight = pages[i + 1] - pages[i];
// 对多余的内容部分进行遮白
this.addBlank(
0,
baseY + imageHeight + pdfHeaderH,
this.A4_WIDTH,
this.A4_HEIGHT - imageHeight,
pdf
);
}
// 添加页眉
await this.addHeader(i + 1, this.header, pdf, this.contentHeaderWidth);
// 添加页脚
await this.addFooter(
pages.length,
i + 1,
this.footer,
pdf,
this.contentFooterWidth
);
// 若不是最后一页,则分页
if (i !== pages.length - 1) {
// 增加分页
pdf.addPage();
}
}
try {
const result = await this.getPdfByType(pdf);
resolve({
pdfResult: result,
});
} catch (error) {
reject("生成pdf出错", error);
}
});
}
// 根据类型获取pdf
getPdfByType(pdf) {
let result = null;
switch (this.outputType) {
case "file":
result = new File([pdf.output("blob")], this.fileName, {
type: "application/pdf",
lastModified: Date.now(),
});
break;
case "save":
result = pdf.save(this.fileName);
break;
default:
result = pdf.output(this.outputType);
}
return result;
}
/**
* 添加页眉
* @param {HTMLElement} header -页眉元素
* @param {Object} pdf - pdf实例
* @param {Number} contentWidth -在pdf中占据的宽度(默认占满)
* @returns
*/
async addHeader(pageNo, header, pdf, contentWidth) {
if (!header || !(header instanceof HTMLElement)) {
return;
}
if (!this.__header) {
// 其他页 页头都是一样的,不需要每次都生成
this.__header = await this.toCanvas(header, contentWidth);
}
// 每页都从 0 0 开始?
const { height, data } = this.__header;
let leftX = (this.A4_WIDTH - this.contentHeaderWidth) / 2;
pdf.addImage(data, "JPEG", leftX, 0, contentWidth, height);
}
/**
* 添加页脚
* @param {Number} pageSize -总页数
* @param {Number} pageNo -当前第几页
* @param {HTMLElement} footer -页脚元素
* @param {Object} pdf - pdf实例
* @param {Number} contentWidth - 在pdf中占据的宽度(默认占满)
* @returns
*/
async addFooter(pageSize, pageNo, footer, pdf, contentWidth) {
if (!footer || !(footer instanceof HTMLElement)) {
return;
}
// 页码元素,类名这里写死了
let pageNoDom = footer.querySelector(".pdf-footer-page");
let pageSizeDom = footer.querySelector(".pdf-footer-page-count");
if (pageNoDom) {
pageNoDom.innerText = pageNo;
}
if (pageSizeDom) {
pageSizeDom.innerText = pageSize;
}
// 如果设置了页码的才需要每次重新生成cavan
if (pageNoDom || !this.__footer) {
this.__footer = await this.toCanvas(footer, contentWidth);
}
let leftX = (this.A4_WIDTH - this.contentFooterWidth) / 2;
const { height, data } = this.__footer;
// 高度位置计算:当前a4高度 - 页脚在pdf中的高度
pdf.addImage(
data,
"JPEG",
leftX,
this.A4_HEIGHT - height,
contentWidth,
height
);
}
// 截取图片
addImage(_x, _y, pdf, data, width, height) {
pdf.addImage(data, "JPEG", _x, _y, width, height);
}
/**
* 添加空白遮挡
* @param {Number} x - x 与页面左边缘的坐标(以 PDF 文档开始时声明的单位)
* @param {Number} y - y 与页面上边缘的坐标(以 PDF 文档开始时声明的单位)
* @param {Number} width - 填充宽度
* @param {Number} height -填充高度
* @param {Object} pdf - pdf实例
* @returns
*/
addBlank(x, y, width, height, pdf) {
pdf.setFillColor(255, 255, 255);
// rect(x, y, w, h, style) ->'F'填充方式,默认是描边方式
pdf.rect(x, y, Math.ceil(width), Math.ceil(height), "F");
}
}
文章来源:https://blog.csdn.net/weixin_42689772/article/details/136501838
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: