以前用itext7将html导出为pdf,比较方便,代码较少,并且支持base64的图片。可是itext7是收费的,因此换成了xhtmlrenderer。css
xhtmlrenderer自动引入依赖包itext2.0.8,并且不能再引入其余版本的itext,由于itext2.0.8是已经被废弃的,里面的不少方法在新版本已经没有了。html
itext导出pdf最重要的4个难点:前端
1.css样式java
2.中文不显示node
3.图片(itext7支持比较好,不过要收费)jquery
4.分页时内容断开的问题(itext7不会出现这种问题,不过要收费)c++
1、首先引入包web
只须要这个就够了,它会自动引入itext2.0.8ajax
org.xhtmlrenderer
core-renderer
R8
2、页面css样式的采集chrome
看过不少篇itext的文章,都没有达到想象中要求。大可能是说将css路径改成绝对路径,或者将css写在页面中,这都不现实。真正的项目中,你的项目经理是不会让你这么作的。
因此我找到一个能将页面全部css采集起来的js方法。传入你的标签的id,返回一个包含该id的区域的全部css样式 ,加上html,head和body标签,组成一个html的字符串。将字符串传给后台去生成pdf。值得注意的是我加了这个字体body{font-family: SimSun;},这个字符是中文字体,后端必须与前端一致。且看后面。
functiongetElementChildrenAndStyles(selector) {var html = $(selector).prop("outerHTML");
selector= selector.split(",").map(function(subselector){return subselector + "," + subselector + " *";
}).join(",");
elts=$(selector);var rulesUsed =[];//文档的全部样式表
sheets =document.styleSheets;for(var c = 0; c < sheets.length; c++) {//rules 和 cssRules 的计数方法也是不同的!rules 是第几个选择器;cssRules 是第几条规则,
//分别用于IE7和chrome
var rules = sheets[c].rules ||sheets[c].cssRules;for(var r = 0; r < rules.length; r++) {//selectorText: $节点
var selectorText =rules[r].selectorText;var matchedElts =$(selectorText);//找到dom节点里全部节点,并将其push到数组里
for (var i = 0; i < elts.length; i++) {if (matchedElts.index(elts[i]) != -1) {
rulesUsed.push(rules[r]);break;
}
}
}
}//重组style
var style = rulesUsed.map(function(cssRule){if(cssRule.style) {var cssText = cssRule.selectorText+'{'+cssRule.style.cssText.toLowerCase()+'}';
}else{var cssText = cssRule.selectorText+'{'+cssRule.cssText+'}';
}returncssText;
}).join("\n");return "
\n\n"+ html+"";
}
今天解决了分页的时候会断开内容的问题,解决方案就是手动分页,用js计算高度而后超过页面高度的就换页,这样就不会出现自动换页的时候内容断开了。
1.我将须要显示的元素都添加class= ‘pdf-page-range’
2. class='pageNext' .pageNext{page-break-after: always;} 这个css表示下一个元素将会换页,转pdf的时候itext会自动识别。
3.在前面的基础上插入如下代码便可,须要图片转换以后执行,
注意:这个修改了网页内容,若是想保留原网页内容,自行想办法 -。-!
//后端低版本的itext对分页的处理很是不友好,因此前端页面强制分页。
//我将须要显示的元素都添加class= ‘pdf-page-range’
//class='pageNext' .pageNext{page-break-after: always;} 这个css表示下一个元素将会换页。
functionpdfPageRange(){var heigth= 0; $(".pdf-page-range").each(function(){var $this = $(this);var $table = $this.find('table');var $next = $this.next();var $prevPage = $this.prev('.pageNext');
index= $(".pageNext").length;var tagName = $this[0].tagName;varelement_tag;if($table&&$table.length>0){
element_tag= $table[0];
}if(tagName=='table'||tagName=='TABLE'){
element_tag= $this[0];
}if(element_tag){
heigth=tablePage($(element_tag),heigth)return true;
}//不是table的处理
heigth += $this[0].offsetHeight;if(heigth>1000){
$this.before("
heigth= $this[0].offsetHeight;
}
});
}//table单独算高度
functiontablePage($table,heigth){var $trList = $table.find('tr');var $thead = $table.find('tr.thead');
$trList.each(function(){
heigth+= $(this)[0].offsetHeight;if(heigth>1000){
$(this).before($thead.prop("outerHTML"));
$thead_add= $(this).prev().prev();
$thead_add.addClass('pageNext');
heigth= $(this)[0].offsetHeight+$thead_add[0].offsetHeight;
}
});returnheigth;
}
});
3、图片的支持
项目中有不少Echarts作的图表,这个生成的图表都是canvas标签,而itext是不支持canvas标签的。因此要把图表所有换成base64的img标签。这里引入一个js。
html2canvas.js,它能将制定区域截图。请看如下。
注意:
1.html2canvas()方法返回的是Promise类型,为何要将全部 html2canvas()方法的返回值集中起来而后使用Promise.all(canvasArray).then()方法。由于html2canvas()是异步的,你的下面的js已经处理完了,它可能还没截图完成。Promise.all(canvasArray).then()方法,会在全部截图已经完成以后执行。因此我把ajax请求放在里面。(请看代码)
2. img标签闭合的问题,img标签是自闭合标签。正常状况下,浏览器不会去识别你的img的闭合标签,即便你的img标签有或,浏览器最后显示仍是, 因此我用一个字符串代替“/”, 后台再用“/”代替这个字符串,你也能够前端就替换。(请看代码)
3.必须给img加上宽度和高度,否则被后台转换以后尺寸会变得很小。
$("#itextpdf").click(function(){
var canvasArray =[];
$(".charts").each(function(){var $this=$(this);var canvasIndex =html2canvas(
$this,
{ scale:5,background:'#FFFFFF',onrendered:function(canvas){var imgBase64 = canvas.toDataURL('image/jpeg', 1.0);
$this.html("");//标签被jquery获取后,自定义属性closingtags会变成closingtags="",你能够加个css将图片隐藏起来,而后在html字符串里面再加一个显示的css。
$this.append (" ")
}
}
);
canvasArray.push(canvasIndex);
});
Promise.all(canvasArray).then(function() {var str = getElementChildrenAndStyles('#basket');
$.post("/ecloud/sa/saerrorquestions/exportpdf.do",{"str":str },function(r){
});
});
});
4、后台代码
项目中引入中文字体,html字符串中也必须引入。个人字体css是 body{font-family: SimSun;}
packagecn.myc.ykt3.util;importjava.io.FileOutputStream;importjava.io.OutputStream;importorg.xhtmlrenderer.pdf.ITextFontResolver;importorg.xhtmlrenderer.pdf.ITextRenderer;importcom.lowagie.text.pdf.BaseFont;public classItextHtmlTopdf {/***
*@paramhtmlStr html字符串
*@return*@throwsException*/
public String exportpdf(String htmlStr ) throwsException {if(StringUtils.isBlank(htmlStr)) {return null;
}
htmlStr= htmlStr.trim().replaceAll("<","").replaceAll("
","\n|\r\n|\r")
.replaceAll(" "," ");
htmlStr= htmlStr.replace("closingtags=\"\"", "/");
String classpath= this.getClass().getResource("/").getPath().replaceFirst("/", "");
String webappRoot= classpath.replaceAll("/target/classes", "/src/main/webapp");//-----版本2.0.8
ITextRenderer renderer = newITextRenderer();
OutputStream os= new FileOutputStream("C:/Users/Administrator/Desktop/createSamplePDF3.pdf");//若是携带图片则加上如下两行代码,将图片标签转换为Itext本身的图片对象,Base64ImgReplacedElementFactory为图片处理类
renderer.getSharedContext().setReplacedElementFactory(newBase64ImgReplacedElementFactory());
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(1);
renderer.setDocumentFromString(htmlStr);
ITextFontResolver fontResolver=renderer.getFontResolver();//解决中文支持问题,参数为字体的路径,html页面也必须引入字体
fontResolver.addFont(webappRoot+"static/sanalysis/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.layout();
renderer.createPDF(os);
os.close();return null;
}
}
Base64ImgReplacedElementFactory图片处理类
packagecn.myc.ykt3.util;importjava.io.IOException ;importorg.w3c.dom.Element ;importorg.xhtmlrenderer.extend.FSImage ;importorg.xhtmlrenderer.extend.ReplacedElement ;importorg.xhtmlrenderer.extend.ReplacedElementFactory ;importorg.xhtmlrenderer.extend.UserAgentCallback ;importorg.xhtmlrenderer.layout.LayoutContext ;importorg.xhtmlrenderer.pdf.ITextFSImage ;importorg.xhtmlrenderer.pdf.ITextImageElement ;importorg.xhtmlrenderer.render.BlockBox ;importorg.xhtmlrenderer.simple.extend.FormSubmissionListener ;importcom.lowagie.text.BadElementException ;importcom.lowagie.text.Image ;importcom.lowagie.text.pdf.codec.Base64 ;public class Base64ImgReplacedElementFactory implementsReplacedElementFactory {/*** 实现createReplacedElement 替换html中的Img标签
*
*@paramc 上下文
*@parambox 盒子
*@paramuac 回调
*@paramcssWidth css宽
*@paramcssHeight css高
*@returnReplacedElement*/
publicReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac,int cssWidth, intcssHeight) {
Element e=box.getElement();if (e == null) {return null;
}
String nodeName=e.getNodeName();//找到img标签
if (nodeName.equals("img")) {
String attribute= e.getAttribute("src");
FSImage fsImage;try{//生成itext图像
fsImage =buildImage(attribute, uac);
}catch(BadElementException e1) {
fsImage= null;
}catch(IOException e1) {
fsImage= null;
}if (fsImage != null) {//对图像进行缩放
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}return newITextImageElement(fsImage);
}
}return null;
}/*** 将base64编码解码并生成itext图像
*
*@paramsrcAttr 属性
*@paramuac 回调
*@returnFSImage
*@throwsIOException io异常
*@throwsBadElementException BadElementException*/
protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throwsIOException,
BadElementException {
FSImage fsImage;if (srcAttr.startsWith("data:image/")) {
String b64encoded= srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(),
srcAttr.length());//解码
byte[] decodedBytes =Base64.decode(b64encoded);
fsImage= newITextFSImage(Image.getInstance(decodedBytes));
}else{
fsImage=uac.getImageResource(srcAttr).getImage();
}returnfsImage;
}/*** 实现reset*/
public voidreset() {
}
@Overridepublic voidremove(Element arg0) {}
@Overridepublic voidsetFormSubmissionListener(FormSubmissionListener arg0) {}
}
个人页面
导出的pdf效果,自动分页,而且分页不会强制裁剪图片区域。