xhtmlrenderer 将html转换成pdf,完美css,带图片,手动分页,解决内容断开的问题

来源:xhtmlrenderer 将html转换成pdf,完美css,带图片,手动分页,解决内容断开的问题 - 煮过的花朵 - 博客园

之前用itext7将html导出为pdf,比较方便,代码较少,而且支持base64的图片。但是itext7是收费的,所以换成了xhtmlrenderer。

xhtmlrenderer自动引入依赖包itext2.0.8,而且不能再引入其他版本的itext,因为itext2.0.8是已经被废弃的,里面的很多方法在新版本已经没有了。

itext导出pdf最重要的4个难点:

1.css样式

2.中文不显示

3.图片(itext7支持比较好,不过要收费)

4.分页时内容断开的问题(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>";
     }

 今天解决了分页的时候会断开内容的问题,解决方案就是手动分页,用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;
    }
});

三、图片的支持

  项目中有很多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){
           
              });   
           });
          
         
    });
$("#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效果,自动分页,并且分页不会强制裁剪图片区域。

 

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 好的,以下是一个使用iTextHTML转换为PDF的帮助类的示例代码: ``` import com.itextpdf.text.Document; import com.itextpdf.text.DocumentException; import com.itextpdf.text.PageSize; import com.itextpdf.text.html.simpleparser.HTMLWorker; import com.itextpdf.text.pdf.PdfWriter; import java.io.*; import java.util.List; public class HtmlToPdfConverter { public static void convert(String htmlFilePath, String pdfFilePath) throws IOException, DocumentException { Document document = new Document(PageSize.A4); PdfWriter.getInstance(document, new FileOutputStream(pdfFilePath)); document.open(); HTMLWorker htmlWorker = new HTMLWorker(document); FileInputStream fileInputStream = new FileInputStream(htmlFilePath); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); List<?> htmlList = htmlWorker.parseToList(inputStreamReader, null); for (Object element : htmlList) { document.add((com.itextpdf.text.Element) element); } document.close(); } } ``` 在这个帮助类中,我们使用了iText库的`Document`、`PdfWriter`和`HTMLWorker`类来将HTML转换为PDF。该类有一个名为`convert`的静态方法,该方法接受两个参数:HTML文件的路径和生成PDF文件的路径。在方法中,我们首先创建一个A4大小的`Document`对象,然后使用`PdfWriter`将其与输出文件流关联起来。接着,我们创建一个`HTMLWorker`对象,然后使用`FileInputStream`和`InputStreamReader`将HTML文件读取为`List`类型的对象,最后通过循环将其添加到`Document`中。最后,我们调用`Document`的`close`方法来关闭生成的PDF文件。 请注意,此示例代码仅用于演示目的,并不是生产环境下完整的代码,您需要根据您的实际需求进行调整和扩展。 ### 回答2: 帮助类的设计思路如下: 首先,我们需要引入iText库,将其添加到项目的依赖中。 接下来定义一个名为HtmlToPdfHelper的类,该类包含一个convert方法,用于将HTML文件转换为PDF文件。该方法需要接收两个参数:htmlFilePath(表示待转换的HTML文件路径)和pdfFilePath(表示生成的PDF文件路径)。 在convert方法内部,我们需要进行以下操作: 1. 创建一个Document对象,并设置页面大小和页边距。 2. 创建一个PdfWriter对象,将Document对象与pdfFilePath绑定。 3. 使用ITextRenderer类创建一个ITextRenderer对象,并将htmlFilePath传入其构造函数。 4. 调用ITextRenderer对象的layout方法,将HTML文件布局。 5. 调用ITextRenderer对象的createPDF方法,将布局后的文件保存到Document对象中。 6. 关闭Document对象和PdfWriter对象,完成PDF文件的生成。 下面是完整的HtmlToPdfHelper类的代码示例: ```java import com.lowagie.text.Document; import com.lowagie.text.PageSize; import com.lowagie.text.pdf.PdfWriter; import org.xhtmlrenderer.pdf.ITextRenderer; import java.io.FileOutputStream; public class HtmlToPdfHelper { public static void convert(String htmlFilePath, String pdfFilePath) throws Exception { Document document = new Document(PageSize.A4); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(pdfFilePath)); document.open(); ITextRenderer renderer = new ITextRenderer(); renderer.setDocument(htmlFilePath); renderer.layout(); renderer.createPDF(writer.getDirectContent(), writer.getPageSize()); document.close(); writer.close(); } } ``` 使用上述的帮助类,我们可以将HTML文件转换为PDF文件,只需调用convert方法,并传入合适的参数即可。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值