由于pdf打印比较稳定,很多时候我们会有把网页转成pdf的需求。先利用canvas转成图片,然后再把图片转成pdf,这里会用到两个插件(html2canvas和jspdf)。正好我最近也有类似需求,在开发过程中也遇到了很多问题,趁着还有印象在这里记录一下。
首先是需要先把页面指定容器的内容用html2canvas转成图片,这个很简单,网上一搜都有现成代码。
let canvasContent = document.getElementById('main'),//需要截图的包裹的(原生的)DOM 对象 width = canvasContent.clientWidth,//canvasContent.offsetWidth || document.body.clientWidth; //获取dom 宽度 height = canvasContent.clientHeight,//canvasContent.offsetHeight; //获取dom 高度 canvas = document.createElement("canvas"), //创建一个canvas节点 scale = 1; //定义任意放大倍数 支持小数 canvas.width = width * scale; //定义canvas 宽度 * 缩放 canvas.height = height * scale; //定义canvas高度 *缩放 canvas.style.width = width * scale + "px"; canvas.style.height = height * scale + "px"; canvas.getContext("2d").scale(scale, scale); //获取context,设置scale let opts = { scale: scale, // 添加的scale 参数 canvas: canvas, //自定义 canvas logging: false, //日志开关,便于查看html2canvas的内部执行流程 width: width, //dom 原始宽度 height: height, useCORS: true, // 【重要】开启跨域配置 };
html2canvas(canvasContent, opts).then(function (canvas) {
var imgUrl = canvas.toDataURL('image/jpeg', 1);
// 这里拿到的imgUrl就是截屏后的图片
})
接下来就是考虑把上边得到的图片转成pdf了,这里也有现成的插件,就是jspdf。
let canvasContent = document.getElementById('main'),//需要截图的包裹的(原生的)DOM 对象width = canvasContent.clientWidth,//canvasContent.offsetWidth || document.body.clientWidth; //获取dom 宽度height = canvasContent.clientHeight,//canvasContent.offsetHeight; //获取dom 高度canvas = document.createElement("canvas"), //创建一个canvas节点scale = 1; //定义任意放大倍数 支持小数canvas.width = width * scale; //定义canvas 宽度 * 缩放canvas.height = height * scale; //定义canvas高度 *缩放canvas.style.width = width * scale + "px";canvas.style.height = height * scale + "px";canvas.getContext("2d").scale(scale, scale); //获取context,设置scalelet opts = {scale: scale, // 添加的scale 参数canvas: canvas, //自定义 canvaslogging: false, //日志开关,便于查看html2canvas的内部执行流程width: width, //dom 原始宽度height: height,useCORS: true, // 【重要】开启跨域配置};//部分配置,其他另配let html = () => {html2canvas(canvasContent, opts).then(function (canvas) { var pageHeight = width / 595.28 * 841.89; //未生成pdf的html页面高度 var leftHeight = height; //pdf页面偏移 var position = 0; //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高 var imgWidth = 595.28; var imgHeight = imgWidth / width * height; var pageData = canvas.toDataURL('', 1.0); var pdf = new jsPDF('', 'pt', 'a4'); //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89) //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < pageHeight) { pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight); } else { while (leftHeight > 0) { pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight); leftHeight -= pageHeight; position -= 841.89; //避免添加空白页 if (leftHeight > 0) { pdf.addPage(); } } } pdf.save('Diary.pdf');});};html();
但是由于我们上边得到的图片是一张大图,所以在转pdf的时候需要分页处理。上边的代码也实现了分页,但是会有问题,就是可能会出现一行字被分割成两部分,一部分在上一页,另一部分在下一页。
于是想到了把得到的图片传给后台,后台再把图片转成pdf,但是尝试后发现,其实后台把图片转成pdf的时候也不能自动给他分页。
import html2canvas from 'html2canvas';import jsPDF from "jsPDF";import fileDownload from 'js-file-download';
downLoad = () => { html2canvas(canvasContent, opts).then(function (canvas) { var base64String = canvas.toDataURL('image/jpeg', 1); //这里对base64串进行操作,去掉url头,并转换为byte var bytes = window.atob(base64String.split(',')[1]); // console.log(bytes, 'bytesbytes') var array = []; for (var i = 0; i < bytes.length; i++) { array.push(bytes.charCodeAt(i)); } var blob = new Blob([new Uint8Array(array)], { type: 'image/jpeg' }); var fd = new FormData(); fd.append('files', blob, Date.now() + '.jpg'); // fd.append('headers', { // 'Content-Type':'application/octet-stream' // }); // fd.append('responseType', 'blob'); // async function postDownload(url) { // const request = { // method: 'POST', // body:JSON.stringify(fd), // headers: { // // 'Content-Type': 'application/json;charset=UTF-8' // 'Content-Type':'application/octet-stream' // } // }; // const response = await fetch(url, request); // // const filename = response.headers.get('content-disposition').split(';')[1].split('=')[1]; // const blob = await response.blob(); // const link = document.createElement('a'); // // link.download = decodeURIComponent(filename); // link.download = decodeURIComponent('1.pdf'); // link.style.display = 'none'; // link.href = URL.createObjectURL(blob); // document.body.appendChild(link); // link.click(); // URL.revokeObjectURL(link.href); // document.body.removeChild(link); // } // postDownload('/api/admin/convert/imageToPdf'); // return axios.post('/api/admin/convert/imageToPdf', fd ).then(res => { fileDownload(res, Date.now() + '.pdf'); }); });};render(){....}
这里我们可以考虑在生成图片的时候不生成一张大图,而是按a4纸大小(这里假如我们是要打印a4规格的)生成多张小图,同时生成小图的时候判断最后一行有没有超出该页纸,然后把这些小图传给后台生成pdf。其实我们都已经生成分页小图了,就没必要再调接口,利用jspdf完全就可以前端转pdf了。拆分小图的时候我们可以根据页面的高度有没有超出a4纸高度来判断需不需要分页,到这里最大的难点就是判断这页的最后一行内容有没有显示完整。由于我这里的显示的是试题,数据格式还是比较整齐的,于是我想了一个偷奸取巧的办法,就是先把所有数据存储起来,页面上的容器作为单页的容器,大小就是一个a4纸的大小,然后我再从所有数据的第一条数据开始,取几条数据(这里就只能估计一下一页大概可以放几条数据,多了肯定没问题,就是筛选时候多执行几遍)放到这个单页容器中,如果我放进去的内容溢出了就证明我取的这几条数据内容在这页放不下了,再给他去掉一条数据放进去直到内容不超出,这样就找到了第一页的内容,记录截取到第几条数据,第二页的时候从这条数据开始利用同样的方法截取,根据截取到的索引和所有数据的长度可以判断出是否是最后一页。这样是不会出现一行内容被分在两页的情况了,也实现了分页,但是也有个问题,因为我是按一条数据去判断是否分页的,所以在一页的末尾可能会出现一段空白。总归是比一段内容被截断要好一些。
import html2canvas from 'html2canvas';import jsPDF from "jsPDF";
import html2canvas from 'html2canvas';
import jsPDF from 'jsPDF';export default class TestPaperStrong extends React.Component { transformRes = res => { // 给每个小题加索引 let num = 0; return { ...res, examSectionVos: res?.examSectionVos?.map(item => ({ ...item, testList: item?.testList?.map(i => { if (i.testForm !== '8' && i.testForm !== '14') { if (item?.score) { return { ...i, itemIndex: ++num, score: item?.score }; } return { ...i, itemIndex: ++num }; } if (item?.score) { return { ...i, itemIndex: num + 1, courseTestOtherVo: i?.courseTestOtherVo?.map(ii => ({ ...ii, itemIndex: ++num, score: item?.score })) }; } return { ...i, itemIndex: num + 1, courseTestOtherVo: i?.courseTestOtherVo?.map(ii => ({ ...ii, itemIndex: ++num })) }; }) })) }; } aa = (pdf, print, callBack) => { /*图片跨域及截图模糊处理*/ let canvasContent = document.getElementById('main'),//需要截图的包裹的(原生的)DOM 对象 width = canvasContent.clientWidth,//canvasContent.offsetWidth || document.body.clientWidth; //获取dom 宽度 height = canvasContent.clientHeight,//canvasContent.offsetHeight; //获取dom 高度 canvas = document.createElement("canvas"), //创建一个canvas节点 scale = 1; //定义任意放大倍数 支持小数 canvas.width = width * scale; //定义canvas 宽度 * 缩放 canvas.height = height * scale; //定义canvas高度 *缩放 canvas.style.width = width * scale + "px"; canvas.style.height = height * scale + "px"; canvas.getContext("2d").scale(scale, scale); //获取context,设置scale let opts = { scale: scale, // 添加的scale 参数 canvas: canvas, //自定义 canvas logging: false, //日志开关,便于查看html2canvas的内部执行流程 width: width, //dom 原始宽度 height: height, useCORS: true, // 【重要】开启跨域配置 }; //部分配置,其他另配 html2canvas(canvasContent, opts).then((canvas) => { //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高 var imgWidth = 595.28; var imgHeight = imgWidth / width * height; var pageData = canvas.toDataURL('', 1.0); //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89) //当内容未超过pdf一页显示的范围,无需分页 pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight); if (print) { pdf.save('Diary.pdf'); } else { pdf.addPage(); } callBack(); }); } computedHeightFlat = () => { let { flatPageIndex, activeFlagList } = this.state; let ele = document.getElementById('content'); // if(!ele) return; let height = ele.scrollHeight; let width = ele.offsetWidth; let a4Height = 595.28 / width * height; if (a4Height > 841.89) { this.setState({ flatPageIndex: [flatPageIndex[0], flatPageIndex[1] - 1], flatPageData: activeFlagList.slice(flatPageIndex[0], flatPageIndex[1]) }, () => { this.computedHeightFlat(); }); } else { this.aa(this.pdf, false, () => { let { flatPageIndex, activeFlagList } = this.state; let testListLength = activeFlagList.length; if (flatPageIndex[1] + 1 + 3 <= testListLength) { this.setState({ flatPageData: activeFlagList.slice(flatPageIndex[1] + 1, flatPageIndex[1] + 1 + 4), flatPageIndex: [flatPageIndex[1] + 1, flatPageIndex[1] + 1 + 3] }, () => { this.computedHeightFlat(); }); } else { this.setState({ flatPageData: activeFlagList.slice(flatPageIndex[1] + 1, testListLength), flatPageIndex: [flatPageIndex[1] + 1, testListLength - 1] }, () => { this.aa(this.pdf, true, () => { }); }); } }); } } componentDidMount() { this.pdf = new jsPDF('', 'pt', 'a4'); this.ele = document.getElementById('wrapper'); let { examId } = getParamWithHash(); if (!examId) return; const params = { examId }; axios.get(getTestExamByExamId, { params }).then(res => { if (!res) return; res = this.transformRes(res || {}); let activeFlagList = []; let { examSectionVos } = res; let aa = examSectionVos?.forEach(item => { let testList = item?.testList || []; activeFlagList.push(...testList); }); activeFlagList = activeFlagList.map((item, index) => ({ ...item, itemIndex: index + 1 })); this.setState({ flatPageData: activeFlagList.slice(0, 3), flatPageIndex: [0, 2], activeFlagList, }, () => { this.computedHeightFlat(); }); }); } render() { let { flatPageData } = this.state; return ( <div className={styles.wrapper} id="wrapper"> <div id='main' className={`${styles.main} ${(window.android || window.webkit) ? styles.mainIpad : ''}`}> <div className={styles.content} id='content'> {flatPageData?.map((item, index) => { return (<RenderTopic key={index} testDetailItem={item} upDateActiveFlagList={this.upDateActiveFlagList} />); })} </div> </div> </div> ); }}
(以上只是部分代码,并非完整可运行代码)
这里其实还有个问题就是假如我第一条数据内容特别多,超出一页了已经,就会死循环,这里需要做特殊处理,我没打算采用这种方案,所以也没做处理。这里的空白如果非要处理的话,可以根据每条数据再细化,比如如果判断最后一条数据超出页面时候,可以把最后这条数据按标题,题干,解析等等拆分,这样能把空白尽量减小,但是判断起来会特别麻烦。。。
后来看到了这篇文章,提供了解决分页问题的方法。引用修改后效果很好。代码如下:
export default class DownLoadPaper extends Component {
onDownLoad = ()=>{
let targetDom = document.getElementById('main');
// 按白色间隙动态为A4纸分页
let htmlToPdf = (canvasContent)=>{
html2canvas(canvasContent,{
logging:false,
useCORS:true
}).then((canvas)=>{
let {title} = this.state;
let {onCloseModal} = this.props;
let leftHeight = canvas.height;
let position = 0;
let a4Width = 595.28;
let a4Height = 841.89 - 10;
let a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); let pageData = canvas.toDataURL('image/jpeg',1.0);
let pdf = new jsPDF('x','pt','a4');
let index = 0,
canvas1 = document.createElement('canvas'),
height;
pdf.setDisplayMode('fullwidth','continuous','FullScreen');
let createImpl = (canvas)=>{
if(leftHeight >0) {
index++;
let checkCount = 0;
if(leftHeight > a4Height) {
let i = position + a4HeightRef;
for (i = position + a4HeightRef;i >= position;i--){
let isWrite = true;
for(let j = 0;j < canvas.width;j++){
let c = canvas.getContext('2d').getImageData(j,i,1,1).data;
if(c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
isWrite = false;
break;
}
}
if(isWrite) {
checkCount++;
if(checkCount >=10){
break;
}
}else{
checkCount = 0;
}
}
height = Math.round(i - position) || Math.min(leftHeight,a4HeightRef); if(height <=0){
height = a4HeightRef;
}
}else{
height = leftHeight;
}
canvas1.width = canvas.width;
canvas1.height = height;
let ctx = canvas1.getContext('2d');
ctx.drawImage(canvas,0,position,canvas.width,height,0,0,canvas.width,height);
pdf.addPage();
pdf.addImage(canvas1.toDataURL('image/jpeg',1.0),'JPEG',0,0,a4Width,a4Width / canvas1.width * height);
pdf.text(index + '',a4Width / 2 - 5,a4Height);
leftHeight -= height;
position += height;
this.setState({
pdfProgress:index,
pdfTotal:index + Math.ceil(leftHeight / a4HeightRef)
})
if(leftHeight > 0){
setTimeout(createImpl,500,canvas);
}else{
pdf.save(title + '.pdf');
this.setState({
isShowLoading:false
},()=>{
onCloseModal && onCloseModal();
});
}
}
}
// 当内容未超过pdf一页显示的范围,无需分页
if(leftHeight < a4Height) {
pdf.addImage(pageData,'JPEG',0,0,a4Width,a4Width/canvas.width*leftHeight);
pdf.text('1',a4Width / 2 -5,a4Height);
pdf.save(title + '.pdf');
this.setState({
isShowLoading:false
},()=>{
onCloseModal && onCloseModal();
})
} else {
try {
pdf.deletePage(1);
this.setState({
pdfTotal:index + Math.ceil(leftHeight / a4Height)
});
setTimeout(createImpl,500,canvas);
} catch(err){
console.log(err);
}
}
});
}
htmlToPdf(targetDom);
}
render(){
return (
<div>
<main style={{padding:isShowLoading?'30px 400px':''}}>
<div id="main">这里是需要截屏的容器</div>
{
isShowLoading && <div>
{
pdfTotal ? <p>正在生成第{pdfProgress}页,共{pdfTotal}页。。。</p>:<p>解析中。。。</p>
}
</div>
}
</main>
</div>
);
}
}
页码可以通过pdf.text(index + '',a4Width / 2 - 5,a4Height)插入进去,第一个参数是页码(必须是字符串),第二个和第三个参数是插入文本在页面的位置。如果想插入一个水印可以通过pdf.addImage('图片地址', 'JPEG', a4Width-50, 0, 50, 50),第三个第四个参数是位置,第五个第六个参数是图片大小,要在插入页面截图后边插入,否则就被页面截图盖住了。
这时候发现页面上还少个总页数,但是这个总页数还不能取进度条里边的总页数(因为它是变化的,可能会在后边分页的时候会增加一页),找了api只找到了获取当前页码(pdf.internal.getNumberOfPages())的,没有找到获取总页数的。后来就想可以先把图片拆成小图,暂时不生成pdf,等到所有小图都解析完了,再生成pdf,因为这个时候当前页总页数就很好获取了,直接从图片集合里取索引和长度就可以。
onDownLoad = () => { var targetDom = document.getElementById('main'); // 按白色间隙动态为A4纸分页 let htmlToPdf = (canvasContent) => { html2canvas(canvasContent, { logging: false, //日志开关,便于查看html2canvas的内部执行流程 useCORS: true, // 【重要】开启跨域配置 }).then((canvas) => { let { title } = this.state; let { onCloseModal } = this.props; var leftHeight = canvas.height; var position = 0; var a4Width = 595.28; var a4Height = 841.89 - 10; var a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); var pageData = canvas.toDataURL('image/jpeg', 1.0); var pdf = new jsPDF('x', 'pt', 'a4'); var index = 0, canvas1 = document.createElement('canvas'), height; pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); let createImpl = (canvas) => { if (leftHeight > 0) { index++; var checkCount = 0; if (leftHeight > a4HeightRef) { var i = position + a4HeightRef; for (i = position + a4HeightRef; i >= position; i--) { var isWrite = true; for (var j = 0; j < canvas.width; j++) { var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data; if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) { isWrite = false; break; } } if (isWrite) { checkCount++; if (checkCount >= 10) { break; } } else { checkCount = 0; } } height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); if (height <= 0) { height = a4HeightRef; } } else { height = leftHeight; } canvas1.width = canvas.width; canvas1.height = height; var ctx = canvas1.getContext('2d'); ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height); // pdf.addPage(); // pdf.addImage(canvas1.toDataURL('image/jpeg', 1.0), 'JPEG', 0, 0, a4Width, a4Width / canvas1.width * height); // pdf.text(index + '', a4Width / 2 - 5, a4Height); leftHeight -= height; position += height; this.setState({ pdfProgress: index, pdfTotal: index + Math.ceil(leftHeight / a4HeightRef), imgs: [...this.state.imgs, canvas1.toDataURL('image/jpeg', 1.0)] }) if (leftHeight > 0) { setTimeout(createImpl, 500, canvas); } else { // pdf.save(title + '.pdf'); let { imgs } = this.state; pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); let b = imgs.map((item, index) => { pdf.addPage(); pdf.addImage(item, 'JPEG', 0, 0, a4Width, a4Width / canvas1.width * height); pdf.text(index + 1 + '(' + imgs.length + ')', a4Width / 2 - 5, a4Height); }) pdf.save(title + '.pdf'); this.setState({ isShowLoading: false, }, () => { // onCloseModal && onCloseModal(); }); } } } //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < a4HeightRef) { // pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, a4Width / canvas.width * leftHeight); // pdf.text('1', a4Width / 2 - 5, a4Height); // pdf.save(title + '.pdf'); this.setState({ isShowLoading: false, imgs: [...this.state.imgs, pageData] }, () => { onCloseModal && onCloseModal(); }) } else { try { pdf.deletePage(1); this.setState({ pdfTotal: index + Math.ceil(leftHeight / a4HeightRef) }); setTimeout(createImpl, 500, canvas); } catch (err) { console.log(err); } } }); } htmlToPdf(targetDom); }
由于图片集合里我只存了图片路劲,这里又出现了问题(图片的高度没有存下来,导致生成时候都取了最后一张图片的高度)
所以在保存图片路劲的时候还需要把这张图片高度也存下来。改为
this.setState({ pdfProgress: index, pdfTotal: index + Math.ceil(leftHeight / a4HeightRef), imgs: [...this.state.imgs, { url: canvas1.toDataURL('image/jpeg', 1.0), canvasheight: a4Width / canvas1.width * height }] })
pdf.addImage(item.url, 'JPEG', 0, 0, a4Width, item.canvasheight)时候取这个高度就可以了。
这是代码:
onDownLoad = () => { var targetDom = document.getElementById('main'); // 按白色间隙动态为A4纸分页 let htmlToPdf = (canvasContent) => { html2canvas(canvasContent, { logging: false, //日志开关,便于查看html2canvas的内部执行流程 useCORS: true, // 【重要】开启跨域配置 }).then((canvas) => { let { title } = this.state; let { onCloseModal } = this.props; var leftHeight = canvas.height; var position = 0; var a4Width = 595.28; var a4Height = 841.89 - 10; var a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); var pageData = canvas.toDataURL('image/jpeg', 1.0); var pdf = new jsPDF('x', 'pt', 'a4'); var index = 0, canvas1 = document.createElement('canvas'), height; pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); let createImpl = (canvas) => { if (leftHeight > 0) { index++; var checkCount = 0; if (leftHeight > a4HeightRef) { var i = position + a4HeightRef; for (i = position + a4HeightRef; i >= position; i--) { var isWrite = true; for (var j = 0; j < canvas.width; j++) { var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data; if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) { isWrite = false; break; } } if (isWrite) { checkCount++; if (checkCount >= 10) { break; } } else { checkCount = 0; } } height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); if (height <= 0) { height = a4HeightRef; } } else { height = leftHeight; } canvas1.width = canvas.width; canvas1.height = height; var ctx = canvas1.getContext('2d'); ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height); // pdf.addPage(); // pdf.addImage(canvas1.toDataURL('image/jpeg', 1.0), 'JPEG', 0, 0, a4Width, a4Width / canvas1.width * height); // pdf.text(index + '', a4Width / 2 - 5, a4Height); leftHeight -= height; position += height; this.setState({ pdfProgress: index, pdfTotal: index + Math.ceil(leftHeight / a4HeightRef), imgs: [...this.state.imgs, { url: canvas1.toDataURL('image/jpeg', 1.0), canvasheight: a4Width / canvas1.width * height }] }) if (leftHeight > 0) { setTimeout(createImpl, 500, canvas); } else { // pdf.save(title + '.pdf'); let { imgs } = this.state; imgs.map((item, index) => { pdf.addPage(); pdf.addImage(item.url, 'JPEG', 0, 0, a4Width, item.canvasheight); pdf.text(index + 1 + '(' + imgs.length + ')', a4Width / 2 - 5, a4Height); }) pdf.save(title + '.pdf'); this.setState({ isShowLoading: false, }, () => { // onCloseModal && onCloseModal(); }); } } } //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < a4HeightRef) { pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, a4Width / canvas.width * leftHeight); pdf.text('1(1)', a4Width / 2 - 5, a4Height); pdf.save(title + '.pdf'); this.setState({ isShowLoading: false, }, () => { onCloseModal && onCloseModal(); }) } else { try { pdf.deletePage(1); this.setState({ pdfTotal: index + Math.ceil(leftHeight / a4HeightRef) }); setTimeout(createImpl, 500, canvas); } catch (err) { console.log(err); } } }); } htmlToPdf(targetDom); }
后续遇到的几个坑:
1、分页时候拆分不精细(页尾空白大),我看了下我的页面,有边框的区域是没法单独拆分的,因为分页时候是按小块区域判断的,只要有一处不是空白就会放到下一页。能不加边框的尽量不加边框。还有就是行高太小的话也检测不到行与行之间的空隙。
2、内容太多时,html2canvas生不成图片。查阅资料发现各浏览器对页面内容大小是有限制的。我采取的解决办法是把页面拆开渲染(保证每一小部分在可生成范围内),然后利用canvas生成单张小图保存起来,最后统一通过jsPDF转成pdf。部分代码如下:
import html2canvas from 'html2canvas';import jsPDF from "jspdf";export default class DownLoadPaper extends Component { constructor(props) { super(props); this.state = { modalValue: 1, title: '', flatDataAll: [], isShowLoading: false, pdfProgress: 1, pdfTotal: 0, isShowStemForSplitScreenshot: false, // 内容过多时候单独处理,只显示题目 isShowAnswerForSplitScreenshot: false, // 内容过多时候单独处理,只显示解析 imgsForSplitScreenshot: [], // 存放所有图片的容器 pdfTotalStemForSplitScreenshot: 0, // 内容过多时候单独处理,题目部分总页数 pdfTotalAnswerForSplitScreenshot: 0, // 内容过多时候单独处理,解析部分总页数 }; } generatePicture = (canvasContent, totalNum) => { html2canvas(canvasContent, { logging: false, //日志开关,便于查看html2canvas的内部执行流程 useCORS: true, // 【重要】开启跨域配置, backgroundColor: '#fff', }).then((canvas) => { let { title, isShowAnswerForSplitScreenshot } = this.state; let { onCloseModal } = this.props; var leftHeight = canvas.height; var position = 0; var a4Width = 595.28; var a4Height = 841.89 - 20; var a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); var pageData = canvas.toDataURL('image/jpeg', 1.0); var index = 0, canvas1 = document.createElement('canvas'), height; let createImpl = (canvas) => { if (leftHeight > 0) { var checkCount = 0; if (leftHeight > a4HeightRef) { index++; var i = position + a4HeightRef; for (i = position + a4HeightRef; i >= position; i--) { var isWrite = true; for (var j = 0; j < canvas.width; j++) { var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data; if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) { isWrite = false; break; } } if (isWrite) { checkCount++; if (checkCount >= 10) { break; } } else { checkCount = 0; } } height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); if (height <= 0) { height = a4HeightRef; } } else { height = leftHeight; } canvas1.width = canvas.width; canvas1.height = height; var ctx = canvas1.getContext('2d'); ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height); if (isShowAnswerForSplitScreenshot) { this.setState({ pdfTotalAnswerForSplitScreenshot: index + Math.ceil(leftHeight / a4HeightRef) }) } else { this.setState({ pdfTotalStemForSplitScreenshot: index + Math.ceil(leftHeight / a4HeightRef), pdfTotalAnswerForSplitScreenshot: totalNum - Math.ceil(leftHeight / a4HeightRef) - index, }) } this.setState({ imgsForSplitScreenshot: [...this.state.imgsForSplitScreenshot, { imgSrc: canvas1.toDataURL('image/jpeg', 1.0), imgHeight: a4Width / canvas1.width * height }] }, () => { leftHeight -= height; position += height; if (leftHeight > 0) { setTimeout(createImpl, 500, canvas); } else { let { isShowAnswerForSplitScreenshot, imgsForSplitScreenshot } = this.state; if (isShowAnswerForSplitScreenshot) { imgsForSplitScreenshot.map((item, index1) => { this.pdf.addPage(); this.pdf.addImage(item?.imgSrc, 'JPEG', 0, 0, a4Width, item?.imgHeight); this.pdf.text(index1 + 1 + '', a4Width / 2 - 5, a4Height + 10); }) this.pdf.save(title + '.pdf'); setTimeout(() => { this.setState({ isShowLoading: false, isShowAnswerForSplitScreenshot: false }, () => { onCloseModal && onCloseModal(); }) }, 3000) } else { this.setState({ isShowAnswerForSplitScreenshot: true, isShowStemForSplitScreenshot: false, }, () => { this.generatePicture(canvasContent) }) } } }) } } //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < a4HeightRef) { this.pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, a4Width / canvas.width * leftHeight); if (isShowAnswerForSplitScreenshot) { this.setState({ isShowLoading: false, pdfTotalAnswerForSplitScreenshot: 1, imgsForSplitScreenshot: [...this.state.imgsForSplitScreenshot, { imgSrc: pageData, imgHeight: a4Width / canvas.width * leftHeight }] }, () => { this.pdf.deletePage(1); this.state.imgsForSplitScreenshot.map((item, index1) => { this.pdf.addPage(); this.pdf.addImage(item?.imgSrc, 'JPEG', 0, 0, a4Width, item?.imgHeight); this.pdf.text(index1 + 1 + '', a4Width / 2 - 5, a4Height + 10); }) this.pdf.save(title + '.pdf'); onCloseModal && onCloseModal(); }) } else { this.setState({ imgsForSplitScreenshot: [...this.state.imgsForSplitScreenshot, { imgSrc: pageData, imgHeight: a4Width / canvas.width * leftHeight }], pdfTotalStemForSplitScreenshot: 1, pdfTotalAnswerForSplitScreenshot: totalNum - 1, }) } } else { try { this.pdf.deletePage(1); if (isShowAnswerForSplitScreenshot) { this.setState({ pdfTotalAnswerForSplitScreenshot: index + Math.ceil(leftHeight / a4HeightRef) }); } else { this.setState({ pdfTotalStemForSplitScreenshot: index + Math.ceil(leftHeight / a4HeightRef), pdfTotalAnswerForSplitScreenshot: totalNum - Math.ceil(leftHeight / a4HeightRef) - index, }); } setTimeout(createImpl, 500, canvas); } catch (err) { console.log(err); } } }); } splitScreenshot = (canvasContent, totalNum) => { this.setState({ isShowStemForSplitScreenshot: true }, () => { this.pdf = new jsPDF('x', 'pt', 'a4'); this.pdf.setFontSize(11); this.pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); this.generatePicture(canvasContent, totalNum) }) } onDownLoad = () => { var targetDom = document.getElementById('main'); // 按白色间隙动态为A4纸分页 let htmlToPdf = (canvasContent) => { html2canvas(canvasContent, { logging: false, //日志开关,便于查看html2canvas的内部执行流程 useCORS: true, // 【重要】开启跨域配置, backgroundColor: '#fff', }).then((canvas) => { let { title, modalValue } = this.state; let { onCloseModal } = this.props; var leftHeight = canvas.height; var position = 0; var a4Width = 595.28; var a4Height = 841.89 - 20; var a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); if (Math.ceil(leftHeight / a4HeightRef) > 40 && modalValue === 1) { // 内容过多时候,html2canvas截图有问题,要分开截取。 this.splitScreenshot(canvasContent, Math.ceil(leftHeight / a4HeightRef)); return; } var pageData = canvas.toDataURL('image/jpeg', 1.0); var pdf = new jsPDF('x', 'pt', 'a4'); pdf.setFontSize(11); var index = 0, canvas1 = document.createElement('canvas'), height; pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); let createImpl = (canvas) => { if (leftHeight > 0) { index++; var checkCount = 0; if (leftHeight > a4HeightRef) { var i = position + a4HeightRef; for (i = position + a4HeightRef; i >= position; i--) { var isWrite = true; for (var j = 0; j < canvas.width; j++) { var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data; if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) { isWrite = false; break; } } if (isWrite) { checkCount++; if (checkCount >= 10) { break; } } else { checkCount = 0; } } height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); if (height <= 0) { height = a4HeightRef; } } else { height = leftHeight; } canvas1.width = canvas.width; canvas1.height = height; // console.log(index, 'height:', height, 'pos', position); var ctx = canvas1.getContext('2d'); ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height); // var pageHeight = Math.round(a4Width / canvas.width * height); // pdf.setPage(null, pageHeight); // console.dir(pdf,'------') // pdf.text('150', 400, 400) pdf.addPage(); pdf.addImage(canvas1.toDataURL('image/jpeg', 1.0), 'JPEG', 0, 0, a4Width, a4Width / canvas1.width * height); // pdf.addImage('https://cdn.zjyjc.com/test/资源/图片/新闻/202201/435299b72fbd7551c22fe8b7e42c1b30.png', 'JPEG', a4Width-50, 0, 50, 50) pdf.text(index + '', a4Width / 2 - 5, a4Height + 10); leftHeight -= height; position += height; this.setState({ pdfProgress: index, pdfTotal: index + Math.ceil(leftHeight / a4HeightRef) }) if (leftHeight > 0) { setTimeout(createImpl, 500, canvas); } else { pdf.save(title + '.pdf'); setTimeout(() => { this.setState({ isShowLoading: false, }, () => { onCloseModal && onCloseModal(); }); }, 1000) } } } //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < a4HeightRef) { pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, a4Width / canvas.width * leftHeight); pdf.text('1', a4Width / 2 - 5, a4Height + 10); pdf.save(title + '.pdf'); this.setState({ isShowLoading: false }, () => { onCloseModal && onCloseModal(); }) } else { try { pdf.deletePage(1); this.setState({ pdfTotal: index + Math.ceil(leftHeight / a4HeightRef) }); setTimeout(createImpl, 500, canvas); } catch (err) { console.log(err); } } }); } htmlToPdf(targetDom); } render() { return ( <div className="downLoadPaperWrapper"> <main style={{ padding: isShowLoading ? '30px 400px' : '' }}> {/* <main style={{width:'1220px'}}> */} <div className="downLoadPaperRadioWrapper"> <Radio.Group onChange={this.onChange} value={modalValue}> <Radio value={1}>题目+解析(卷尾)</Radio> <Radio value={2}>题目+解析(每道题目之后)</Radio> <Radio value={3}>仅题目(无解析)</Radio> </Radio.Group> </div> <div className="downLoadPaperContentWrapper"> <div className="downLoadPaperContentWrapperR" id="downLoadPaperContentWrapperR"> <div className="downLoadPaperContentWrapperRTestContainer" id="main"> <h2 className="downLoadPaperContentWrapperRTestContainerTitle">{title}</h2> { !isShowAnswerForSplitScreenshot && flatDataAll?.map((item, index) => (<RenderTopic modalValue={modalValue} key={item?.id} testDetailItem={item} upDateActiveFlagList={() => { }} />)) } { modalValue === 1 && !isShowStemForSplitScreenshot && <> <h2 style={{ textAlign: 'center', marginTop: '0px', marginBottom: '0px' }}>参考答案</h2> { flatDataAll?.map((item, index) => (<AnswerComponent isShowNum={true} key={index} testDetailItem={item} />)) } </> } </div> </div> </div> { isShowLoading && <div className="downLoadPaperContentLoadingWrapper"> { pdfTotal ? <p>正在生成第<span class="pdfProgress">{pdfProgress}</span>页,共<span class="pdfTotal">{pdfTotal}</span>页...</p> : (isShowStemForSplitScreenshot || isShowAnswerForSplitScreenshot) ? '' : <p>解析中。。。</p> } { (isShowStemForSplitScreenshot || isShowAnswerForSplitScreenshot) && (imgsForSplitScreenshot?.length) && (pdfTotalStemForSplitScreenshot + pdfTotalAnswerForSplitScreenshot) ? <p>正在生成第<span class="pdfProgress">{imgsForSplitScreenshot?.length || 1}</span>页,共<span class="pdfTotal">{(pdfTotalStemForSplitScreenshot + pdfTotalAnswerForSplitScreenshot) || 1}</span>页...</p> : pdfTotal ? '' : <p>解析中。。。</p> } </div> } </main> <footer> <div className="downLoadPaperBtnWrapper"> <div style={{ display: isShowLoading ? 'none' : '' }} className="downLoadPaperBtnL" onClick={onCloseModal}>取消</div> <div style={{ display: isShowLoading ? 'none' : '' }} className="downLoadPaperBtnR" onClick={() => { this.setState({ isShowLoading: true }, () => { this.onDownLoad(); }); }}>确认</div> </div> </footer> </div> ); }}
3、部分css样式支持不是很好,我这里是波浪线显示成了下划线,查阅官网发现它本身只支持下划线。
虽然不支持,但是问题还得解决,首先想到的是图片边框,试了下不支持。然后只能换成背景图了。属性选择器也不支持。div[style*="text-decoration:underline wavy"] { color: red; text-decoration: none !important; background-image: url(/public_static/123.png); background-repeat: repeat-x; background-position: left top; background-size: auto 100%; }这么写是识别不出来的。只能正则替换行内样式了(数据返回的就是行内样式)。
replaceWavy = (str) => { if (!str) { return ''; } return str.replace(/text-decoration:underline wavy;/g, "background-image:url(/public_static/wavy.jpg);background-repeat:repeat-x;background-position:left 92%;background-size:20px 30%;"); }
问题解决:
后来发现u标签自带下划线也是不显示的,给它设置text-decoration-line:underline也是不管用的(其他标签设置下划线是生效的),采用下边框替代。
最后搞了一个好玩的东西,那两个插件都可以引入线上的,那我是不是可以在打开的页面控制台动态引入script标签,然后执行下载代码就可以截取网页截屏或者指定容器截屏了,试了下部分网页是可以的。
bbb = (targetDom) => { targetDom = targetDom || document.body; let script1 = document.createElement("script"); script1.src = 'https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.js'; document.body.appendChild(script1); let script2 = document.createElement("script"); script2.src = 'https://cdn.bootcss.com/jspdf/1.3.4/jspdf.debug.js'; document.body.appendChild(script2); // 按白色间隙动态为A4纸分页 let htmlToPdf = (canvasContent) => { window.html2canvas(canvasContent, { logging: false, //日志开关,便于查看html2canvas的内部执行流程 useCORS: true, // 【重要】开启跨域配置 }).then((canvas) => { var leftHeight = canvas.height; var position = 0; var a4Width = 595.28; var a4Height = 841.89 - 10; var a4HeightRef = Math.floor(canvas.width / a4Width * a4Height); var pageData = canvas.toDataURL('image/jpeg', 1.0); var pdf = new jsPDF('x', 'pt', 'a4'); var index = 0, canvas1 = document.createElement('canvas'), height; pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); let createImpl = (canvas) => { if (leftHeight > 0) { index++; var checkCount = 0; if (leftHeight > a4HeightRef) { var i = position + a4HeightRef; for (i = position + a4HeightRef; i >= position; i--) { var isWrite = true; for (var j = 0; j < canvas.width; j++) { var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data; if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) { isWrite = false; break; } } if (isWrite) { checkCount++; if (checkCount >= 10) { break; } } else { checkCount = 0; } } height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); if (height <= 0) { height = a4HeightRef; } } else { height = leftHeight; } canvas1.width = canvas.width; canvas1.height = height; canvas1.style.background = '#fff'; var ctx = canvas1.getContext('2d'); ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height); pdf.addPage(); pdf.addImage(canvas1.toDataURL('image/jpeg', 1.0), 'JPEG', 0, 0, a4Width, a4Width / canvas1.width * height); pdf.text(index + '', a4Width / 2 - 5, a4Height); leftHeight -= height; position += height; if (leftHeight > 0) { setTimeout(createImpl, 500, canvas); } else { pdf.save('哈哈哈' + '.pdf'); } } }; //当内容未超过pdf一页显示的范围,无需分页 if (leftHeight < a4HeightRef) { pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, a4Width / canvas.width * leftHeight); pdf.text('1', a4Width / 2 - 5, a4Height); pdf.save('哈哈哈' + '.pdf'); } else { try { pdf.deletePage(1); setTimeout(createImpl, 2000, canvas); } catch (err) { console.log(err); } } }); }; window.setTimeout(() => { htmlToPdf(targetDom); }, 500); }