以前用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
<dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>core-renderer</artifactId> <version>R8</version> </dependency>
2、页面css样式的采集chrome
看过不少篇itext的文章,都没有达到想象中要求。大可能是说将css路径改成绝对路径,或者将css写在页面中,这都不现实。真正的项目中,你的项目经理是不会让你这么作的。
因此我找到一个能将页面全部css采集起来的js方法。传入你的标签的id,返回一个包含该id的区域的全部css样式 ,加上html,head和body标签,组成一个html的字符串。将字符串传给后台去生成pdf。值得注意的是我加了这个字体body{font-family: SimSun;},这个字符是中文字体,后端必须与前端一致。且看后面。
function getElementChildrenAndStyles(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+'}'; } return cssText; }).join("\n"); return "<html><head><meta charset='UTF-8'/> <style>\n"
+ style
+"\n td{background:white!important;}"
+"\n body{font-family: SimSun;} \n</style>\n\n</head><body>"
+ html+"</body></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表示下一个元素将会换页。
function pdfPageRange(){ 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; var element_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("<div class='pageNext' ></div> "); heigth = $this[0].offsetHeight; } }); } //table单独算高度 function tablePage($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; } }); return heigth; } });
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标签有</img>或<img src="" />,浏览器最后显示仍是<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 ("<img class=“hidden” alt='' src='"+ imgBase64+"' closingtags > ") } } ); 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;}
package cn.myc.ykt3.util; import java.io.FileOutputStream; import java.io.OutputStream; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; import com.lowagie.text.pdf.BaseFont; public class ItextHtmlTopdf { /** * * @param htmlStr html字符串 * @return * @throws Exception */ public String exportpdf(String htmlStr ) throws Exception { if (StringUtils.isBlank(htmlStr)) { return null; } htmlStr = htmlStr.trim().replaceAll("<","<").replaceAll( ">",">").replaceAll("<br/>","\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 = new ITextRenderer(); OutputStream os = new FileOutputStream("C:/Users/Administrator/Desktop/createSamplePDF3.pdf"); // 若是携带图片则加上如下两行代码,将图片标签转换为Itext本身的图片对象,Base64ImgReplacedElementFactory为图片处理类 renderer.getSharedContext().setReplacedElementFactory(new Base64ImgReplacedElementFactory()); 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图片处理类
package cn.myc.ykt3.util; import java.io.IOException ; import org.w3c.dom.Element ; import org.xhtmlrenderer.extend.FSImage ; import org.xhtmlrenderer.extend.ReplacedElement ; import org.xhtmlrenderer.extend.ReplacedElementFactory ; import org.xhtmlrenderer.extend.UserAgentCallback ; import org.xhtmlrenderer.layout.LayoutContext ; import org.xhtmlrenderer.pdf.ITextFSImage ; import org.xhtmlrenderer.pdf.ITextImageElement ; import org.xhtmlrenderer.render.BlockBox ; import org.xhtmlrenderer.simple.extend.FormSubmissionListener ; import com.lowagie.text.BadElementException ; import com.lowagie.text.Image ; import com.lowagie.text.pdf.codec.Base64 ; public class Base64ImgReplacedElementFactory implements ReplacedElementFactory { /** * 实现createReplacedElement 替换html中的Img标签 * * @param c 上下文 * @param box 盒子 * @param uac 回调 * @param cssWidth css宽 * @param cssHeight css高 * @return ReplacedElement */ public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) { 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 new ITextImageElement(fsImage); } } return null; } /** * 将base64编码解码并生成itext图像 * * @param srcAttr 属性 * @param uac 回调 * @return FSImage * @throws IOException io异常 * @throws BadElementException BadElementException */ protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException, 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 = new ITextFSImage(Image.getInstance(decodedBytes)); } else { fsImage = uac.getImageResource(srcAttr).getImage(); } return fsImage; } /** * 实现reset */ public void reset() { } @Override public void remove(Element arg0) {} @Override public void setFormSubmissionListener(FormSubmissionListener arg0) {} }
个人页面
导出的pdf效果,自动分页,而且分页不会强制裁剪图片区域。