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){ }); }); });

 四、后台代碼

   項目中引入中文字體,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("&lt;","<").replaceAll( "&gt;",">").replaceAll("<br/>","\n|\r\n|\r" )
                .replaceAll("&nbsp;"," ");
        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效果,自動分頁,並且分頁不會強制裁剪圖片區域。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM