缘由:因业务原因,本公司做教育产品的,需要对试题进行转化,重组成试卷的方式。。。由前端将html转成pdf----本文针对产生截断问题记录一下。
前沿:
试题涉及到数学公式和化学公式,试卷涉及密封线等一些试卷属性。并且试题结构比较复杂。
一:
因为导出的是a4纸,我们一开始的方案是计算试题的高度与每页a4的高度做比较,如果超过a4高度,在对于试题上方加一个白色空白元素。对当前页面进行填充。余下的(挤下去)放在下一页。。但是由于试题的复杂性。多题嵌套,并且可能存在一道小题就超出一张a4纸的高度。。。故这个方式不好计算。
二: 在我们公司前端伙伴的帮助下。用了一个方法。识别颜色的办法。。。文字的颜色和背景色进行比对。
/**
* pdf管理对象
* @param {String} selector class、id选择器
* @param {Object} options 配置参数
* @property {String} options.type 尺寸类型 a3/a4 对应尺寸为[297mm, 420mm]/[210mm,297mm]
* @property {Array} options.bgColors 背景色数组,用于判断此色号是否作为可分割线,需要传入6位hex数值或rgba,如#ffffff,rgab(255,255,255,1)
* @property {Array} options.padding 内容与页面边距,单位mm,支持参数合并,例[10, 15]/[10]/[10,15,10,15]
* @returns {Object} manage pdf管理对象
* @property {Function} manage.save 导出pdf
*/
export function pdfManage(selector, options = {}) {
let { bgColors, type, padding } = options;
let paddingTop, paddingLeft, paddingRight, paddingBottom;
bgColors = bgColors || ['#ffffff']; //过滤背景色数组默认赋值
let types = ['a3', 'a4'];
//类型对应尺寸mm
let typeRect = {
a3: {
width: 297,
height: 420
},
a4: {
width: 210,
height: 297
}
};
type = type || 'a4'; //类型默认赋值
//校验类型是否符合
if (!types.includes(type)) {
throw new Error('type只能是' + types.toString() + '中的一种');
}
padding = padding || [0]; //padding默认赋值
//校验padding格式是否符合
if (!Array.isArray(padding) || ![1, 2, 4].includes(padding.length)) {
throw new Error('padding不符合格式');
}
//校验padding必须为0或正数
if (padding.some(v => v < 0 || (!v && v != 0))) {
throw new Error('padding不小于0');
}
//根据padding参数数量,分配四个位置对应的padding值
switch (padding.length) {
//四个位置padding相同
case 1:
paddingTop = paddingLeft = paddingRight = paddingBottom = padding[0];
break;
//上下padding取数组第一位,左右padding取数组第二位
case 2:
paddingTop = paddingBottom = padding[0];
paddingLeft = paddingRight = padding[1];
break;
//数组位置分别对应上、右、下、左的padding
case 4:
[paddingTop, paddingRight, paddingBottom, paddingLeft] = padding;
break;
default:
break;
}
const pdf = new JsPDF('p', 'mm', type); // A4纸,纵向
let bgColorsList = [];
bgColors.forEach(color => {
bgColorsList.push(JSON.stringify(getRgbaData(color)));
});
let managePromise = new Promise(resolve => {
makeHtmlToCanvas(selector).then(canvas => {
/* 新增pdf下载-start */
var ctx = canvas.getContext('2d');
var typeW = typeRect[type].width - paddingLeft - paddingRight;
var typeH = typeRect[type].height - paddingTop - paddingBottom; // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
var imgHeight = Math.ceil((typeH * canvas.width) / typeW); // 按A4显示比例换算一页图像的像素高度
var renderedHeight = 0;
pdf.page = 0;
while (renderedHeight < canvas.height) {
//创建canvas存放每页的图片
var page = document.createElement('canvas');
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页
var pageCtx = page.getContext('2d');
// 用getImageData剪裁指定区域,并画到前面建立的canvas对象中
let minHeight = Math.min(imgHeight, canvas.height - renderedHeight);
let imgData = ctx.getImageData(
0,
renderedHeight,
canvas.width,
minHeight
);
let loopIndex = minHeight;
//从下往上一行行数据做遍历,查找可以作为截断的行号(当整行的像素点都与背景色相符,则代表此行可以用来截断)
while (loopIndex > 0 && bgColorsList.length > 0) {
let bgFitCount = 0;
//每四个数字,分别代表r,g,b,a的值
for (
let i = (loopIndex - 1) * 4 * imgData.width;
i < loopIndex * 4 * imgData.width;
i += 4
) {
let imgDataI = [
imgData.data[i],
imgData.data[i + 1],
imgData.data[i + 2],
imgData.data[i + 3]
];
//当前可用于过滤的背景色列表包含了此像素点
if (bgColorsList.includes(JSON.stringify(imgDataI))) {
bgFitCount += 1;
} else {
break;
}
}
//找到符合条件的行,退出遍历
if (bgFitCount === imgData.width) {
break;
} else {
//未找到,继续遍历上一行
loopIndex -= 1;
}
}
//遍历完整页数据,还未找到可以用于截断的行,则不做处理,按默认情况截断
if (loopIndex <= 0) {
loopIndex = minHeight;
}
imgData = ctx.getImageData(0, renderedHeight, canvas.width, loopIndex);
pageCtx.putImageData(imgData, 0, 0);
//当前页被截断过,则需要在某尾添加被截断高度的填充色(bgColors[0])作为补充
if (loopIndex < minHeight) {
let fillBgHeight = minHeight - loopIndex;
pageCtx.beginPath();
pageCtx.fillStyle = bgColors[0];
pageCtx.fillRect(0, loopIndex, canvas.width, fillBgHeight);
}
//将处理好的图片正式输出到pdf中
pdf.addImage(
page.toDataURL('image/jpeg', 1.0),
'JPEG',
paddingLeft,
paddingTop,
typeW,
Math.min(typeH, (typeW * page.height) / page.width)
);
//本页渲染完成,继续处理下一页
renderedHeight += loopIndex;
if (renderedHeight < canvas.height) {
pdf.addPage();
} // 若是后面还有内容,添加一个空页
++pdf.page;
// delete page
}
console.log(pdf, 'pdf');
resolve();
});
});
let manage = {
el: selector,
//保存pdf
save(pdfFileName) {
managePromise.then(() => {
pdf.save(pdfFileName);
});
},
//输出pdf
output(type, options) {
managePromise.then(() => {
pdf.output(type, options);
});
},
//获取pdf宽度
getPageWidth() {
pdf.getPageWidth();
},
//获取pdf高度
getPageHeight() {
pdf.getPageHeight();
},
//添加元素
addContent(el, options = {}) {
managePromise = new Promise(resolve => {
makeHtmlToCanvas(el).then(canvas => {
let { x, y, pages } = options;
x = x || 0;
y = y || 0;
pages = pages || 'all';
if (pages !== 'all' && !Array.isArray(pages)) {
throw new Error('options参数pages只能为all或者数字型数组');
}
for (let i = 0; i < pdf.page; i++) {
if (pages !== 'all' && !pages.includes(i + 1)) {
continue;
}
pdf.setPage(i + 1);
pdf.addImage(
canvas.toDataURL('image/jpeg', 1.0),
'JPEG',
x,
y,
canvas.width / 2,
canvas.height / 2
);
}
resolve();
// pdf.save('123');
});
});
}
};
return manage;
}