之前用 itext7 将 html 导出为 PDF, 比较方便, 代码较少, 而且支持 base64 的图片. 但是 itext7 是收费的, 所以换成了 xhtmlrenderer.
xhtmlrenderer 自带 itext2.0.8, 而且不能再引入其他版本的 itext, 因为 itext2.0.8 是已经被废弃的, 里面的很多方法在新版本已经没有了.
itext 导出 PDF 最重要的 2 个难点:
1.CSS 样式
2. 中文不显示
2. 图片 (itext7 支持比较好, 不过要收费)
一, 首先引入包
只需要这个就够了, 它会自动引入 itext2.0.8
- <dependency>
- <groupId>org.xhtmlrenderer</groupId>
- <artifactId>core-renderer</artifactId>
- <version>R8</version>
- </dependency>
二, 页面 CSS 样式的采集
看过很多篇 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>";
- }
三, 图片的支持
项目中有很多 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 标签闭合的问题, iimg 标签是自闭和标签. 正常情况下, 浏览器不会去识别你的 img 的闭合标签, 即使你的 img 标签有 </img > 或 < img src=""/>, 浏览器最后显示还是 < 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){
- });
- });
- });
四, 后台代码
项目中引入中文字体, 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 效果, 自动分页, 并且分页不会强制裁剪图片区域.
来源: https://www.cnblogs.com/trisolaris2018/p/10754914.html