xhtmlrenderer + iText-HTML轉PDF

xhtmlrenderer + iText - HTML轉PDF

xhtmlrendere+itext2.0.8 將html轉成pdf,帶樣式、圖片(也支持二維碼、條形碼)等

主要步驟

  1. 生成html(css樣式直接放在style中)
  2. html轉換pdf方法
  3. 數據返回給前端

詳細過程

  1. html模板:
private static final String DEFAULT_HTML = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
            "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" +
            "<head>\n" +
            "<meta charset=\"utf-8\" />\n" +
            "<style> \n" +
            "  body{ padding:0; margin:0; font-family:Microsoft YaHei; @page {size:20mm, 35mm;}} \n" +
            "</style>\n" +
            "</head>\n" +
            "<body>\n" +
            "  ${CONTENT}\n" +
            "</body>\n" +
            "</html>";

實際內容替換DEFAULT_HTML中的${CONTENT}

2.html轉pdf
在這裏插入圖片描述
方法代碼:

    public static void htmlToPdf2(String html, ByteArrayOutputStream os) throws IOException {
            try {
                ITextRenderer renderer = new ITextRenderer();
                renderer.setDocumentFromString(html);
                ITextFontResolver fontResolver = renderer.getFontResolver();
                // 獲取字體文件路徑
                fontResolver.addFont(getFontPath2(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
                renderer.getSharedContext().setReplacedElementFactory(new ImgReplacedElementFactory());
                renderer.layout();
                renderer.createPDF(os);
                os.flush();
            } catch (Exception e) {
                logger.error("Html:" + html);
                logger.error("Html To Pdf Failed", e);
    //            throw new CommonException("Html To Pdf Failed:" + e.getMessage());
            } finally {
                if (os != null) {
                    os.close();
                }
            }
        }
        
    // 字體路徑  
    private static String getFontPath2() {
            return HrptConstants.FONT_PATH + File.separator + HrptConstants.TTF_NAME_2;
        }
    /**
     * <p>
     * 圖片處理優化-支持html中img標籤的src爲url或者base64
     * </p>
     */
    public class ImgReplacedElementFactory implements ReplacedElementFactory {
    
        private final static String IMG_ELEMENT_NAME = "img";
        private final static String SRC_ATTR_NAME = "src";
        private final static String URL_PREFIX_NAME = "data:image";
        private final static String URL_BASE64 = "base64,";
    
        private final static Logger LOGGER = LoggerFactory.getLogger(ImgReplacedElementFactory.class);
    
        @Override
        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_ELEMENT_NAME)) {
                String url = e.getAttribute(SRC_ATTR_NAME);
                FSImage fsImage;
                try {
                    InputStream imageStream = this.getImageStream(url, BaseConstants.Digital.ZERO);
                    byte[] bytes = IOUtils.toByteArray(imageStream);
                    // 生成itext圖像
                    fsImage = new ITextFSImage(Image.getInstance(bytes));
                } catch (Exception e1) {
                    fsImage = null;
                }
                if (fsImage != null) {
                    // 對圖像進行縮放
                    if (cssWidth != -1 || cssHeight != -1) {
                        fsImage.scale(cssWidth, cssHeight);
                    }
                    return new ITextImageElement(fsImage);
                }
            }
            return null;
        }
    
        @Override
        public void reset() {
    
        }
    
        @Override
        public void remove(Element e) {
    
        }
    
        @Override
        public void setFormSubmissionListener(FormSubmissionListener listener) {
    
        }
    
        /**
         * 重複獲取網絡圖片3次,若三次失敗則不再獲取
         * @param url
         * @param tryCount
         * @return
         */
        private InputStream getImageStream(String url, int tryCount) {
            if (tryCount > BaseConstants.Digital.TWO) {
                return null;
            }
            if (URL_PREFIX_NAME.equals(url.substring(0, 10))) {
                byte[] bytes = Base64.decode(url.substring(url.indexOf(URL_BASE64) + 7));
                //轉化爲輸入流
                return new ByteArrayInputStream(bytes);
            }
            try {
                HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
                connection.setReadTimeout(5000);
                connection.setConnectTimeout(5000);
                connection.setRequestMethod("GET");
                if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                    InputStream inputStream = connection.getInputStream();
                    return inputStream;
                } else {
                    tryCount += 1;
                    LOGGER.info("connectionError : {} , msg : {}", connection.getResponseCode(), connection.getResponseMessage());
                    return getImageStream(url, tryCount);
                }
            } catch (IOException e) {
                LOGGER.error("connectionIOException : {} , trace : {}", e.getMessage(), e.getStackTrace());
            }
            return null;
        }
    
    }

3.最後通過response導出pdf給前端

最後,對於在開發過程中碰到的問題,做下記錄和總結。

  • 字體問題,漢字不顯示

html模板body裏面font-family屬性不要落了
在這裏插入圖片描述

字體路徑要找的到你的字體文件
在這裏插入圖片描述

font-family屬性中的字體要和應用的字體文件字體相對應,舉例:font-family中設置的是Microsoft YaHei字體,那麼添加的字體文件就一定要是微軟雅黑的(圖中的msyh.ttc就是微軟雅黑的字體文件)

字體下載:常用的字體在windows的自帶font文件夾下基本上都有,實在沒有就去網上自己找吧

  • html中的img圖片標籤後綴問題

xhtmlrenderer會對html轉成xml,所以對於html格式要求嚴格,現在前端生成的html中img標籤往往都是不帶後綴的,所以在接口調用時會報錯,小問題,把html中的img加上後綴就好

        // templateContent -- html內容
        Document doc = Jsoup.parse(templateContent);
        // img標籤後綴處理
        Elements img = doc.getElementsByTag("img");
        if (!img.isEmpty()) {
            for (Element element:img) {
                if (!element.toString().contains("/>") && !element.toString().contains("</img>")) {
                    templateContent = templateContent.replace(element.toString(),element.toString() + "</img>");
                }
            }
        }
  • 接口報錯,html轉pdf報錯。
    Html To Pdf Failed:Cant load the XML resource (using TRaX transformer). org.w 3c.dom.DOMException: NOT_FOUND_ERR: An attempt is made to reference a node in a context where it does n ot exist.
    在這裏插入圖片描述

這個問題蠻困擾的,html明明沒有問題,然後一步一步debug發現,是因爲html中有些標籤中加了id屬性導致的,根據源碼看到的是,id轉xml默認給的namespace都是空字符串而導致,查看http://www.w3.org/1999/xhtml也沒看到說div標籤和img標籤支持id屬性,最後做了html字符串處理,把id替換成了title

    // id處理,Element對象不支持id屬性
    templateContent = templateContent.replaceAll("id=","title=");
  • img圖片src爲base64 code出現的一點問題

這裏的業務場景是HTML中會有二維碼或者條形碼,用的都是img標籤,後端會使用實際數據(這裏是資產的編碼)的二進制內容轉成base64編碼然後放入src中
替換,主要注意要加前綴URL_PREFIX_NAME :

    private final static String ID_BAR = "stylesBarCode";
    private final static String ID_QR = "stylesQrCode";
    private final static String SRC_ATTR_NAME = "src";
    private final static String URL_PREFIX_NAME = "data:image/png;base64,";
    private final static String CHARACTER_ENCODING = "utf-8";

// img的src處理
        Element barElement = doc.getElementById(ID_BAR);
        Element qrElement = doc.getElementById(ID_QR);
        Base64.Encoder encoder = Base64.getEncoder();
        if (barElement != null) {
            byte[] bytes = CodeUtils.generateBarCode(assetVO.getAspAssetNum(), 60, 5, CHARACTER_ENCODING, "code39");
            templateContent = templateContent.replace(barElement.attr(SRC_ATTR_NAME), URL_PREFIX_NAME + encoder.encodeToString(bytes));
        }
        if (qrElement != null) {
            byte[] bytes = CodeUtils.generateQrCode(assetVO.getAspAssetNum(), 20, 20, CHARACTER_ENCODING);
            templateContent = templateContent.replace(qrElement.attr(SRC_ATTR_NAME), URL_PREFIX_NAME + encoder.encodeToString(bytes));
        }

工具方法,生成二維碼,生成條形碼(用的zxing):

/**
     * 生成二維碼
     *
     * @param text              內容
     * @param width             寬
     * @param height            高
     * @param characterEncoding 字符編碼
     * @return 二進制內容
     */
    public static byte[] generateQrCode(String text, int width, int height, String characterEncoding) {
        QRCodeWriter writer = new QRCodeWriter();
        HashMap<EncodeHintType, Object> config = new HashMap<>(BaseConstants.Digital.SIXTEEN);
        config.put(EncodeHintType.CHARACTER_SET, characterEncoding);
        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
            BitMatrix bar = writer.encode(text, BarcodeFormat.QR_CODE, width, height, config);
            MatrixToImageWriter.writeToStream(bar, HrptConstants.ImageType.PNG, stream);
            return stream.toByteArray();
        } catch (Exception e) {
            throw new CommonException(HrptMessageConstants.ERROR_GENERATE_QRCODE);
        }
    }

    /**
     * 生成條形碼
     *
     * @param text              內容
     * @param width             寬
     * @param height            高
     * @param characterEncoding 字符編碼
     * @param barCodeType       條形碼類型
     * @return 二進制內容
     */
    public static byte[] generateBarCode(String text, int width, int height, String characterEncoding, String barCodeType) {
        BarCodeType codeType = BarCodeType.valueOf2(barCodeType);
        switch (codeType) {
            case CODE_39:
                return generateBarCode39(text, width, height, characterEncoding);
            case CODE_93:
                return generateBarCode93(text, width, height, characterEncoding);
            case CODE_128:
                return generateBarCode128(text, width, height, characterEncoding);
            default:
                throw new CommonException(HrptMessageConstants.UNSUPPORTED_CODE_TYPE);
        }

    }
    
    /**
     * 生成Code39條形碼
     *
     * @param text              內容
     * @param width             寬
     * @param height            高
     * @param characterEncoding 字符編碼
     * @return 二進制內容
     */
    public static byte[] generateBarCode39(String text, int width, int height, String characterEncoding) {
        Code39Writer writer = new Code39Writer();
        HashMap<EncodeHintType, Object> config = new HashMap<>(BaseConstants.Digital.SIXTEEN);
        config.put(EncodeHintType.CHARACTER_SET, characterEncoding);
        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
            BitMatrix bar = writer.encode(text, BarcodeFormat.CODE_39, width, height, config);
            MatrixToImageWriter.writeToStream(bar, HrptConstants.ImageType.PNG, stream);
            return stream.toByteArray();
        } catch (Exception e) {
            throw new CommonException(HrptMessageConstants.ERROR_GENERATE_BARCODE);
        }
    }

    /**
     * 生成Code93條形碼
     *
     * @param text              內容
     * @param width             寬
     * @param height            高
     * @param characterEncoding 字符編碼
     * @return 二進制內容
     */
    public static byte[] generateBarCode93(String text, int width, int height, String characterEncoding) {
        Code93Writer writer = new Code93Writer();
        HashMap<EncodeHintType, Object> config = new HashMap<>(BaseConstants.Digital.SIXTEEN);
        config.put(EncodeHintType.CHARACTER_SET, characterEncoding);
        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
            BitMatrix bar = writer.encode(text, BarcodeFormat.CODE_93, width, height, config);
            MatrixToImageWriter.writeToStream(bar, HrptConstants.ImageType.PNG, stream);
            return stream.toByteArray();
        } catch (Exception e) {
            throw new CommonException(HrptMessageConstants.ERROR_GENERATE_BARCODE);
        }
    }

    /**
     * 生成Code128條形碼
     *
     * @param text              內容
     * @param width             寬
     * @param height            高
     * @param characterEncoding 字符編碼
     * @return 二進制內容
     */
    public static byte[] generateBarCode128(String text, int width, int height, String characterEncoding) {
        Code128Writer writer = new Code128Writer();
        HashMap<EncodeHintType, Object> config = new HashMap<>(BaseConstants.Digital.SIXTEEN);
        config.put(EncodeHintType.CHARACTER_SET, characterEncoding);
        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
            BitMatrix bar = writer.encode(text, BarcodeFormat.CODE_128, width, height, config);
            MatrixToImageWriter.writeToStream(bar, HrptConstants.ImageType.PNG, stream);
            return stream.toByteArray();
        } catch (Exception e) {
            throw new CommonException(HrptMessageConstants.ERROR_GENERATE_BARCODE);
        }
    }
  • ImgReplacedElementFactory類中的getImageStream方法(代碼上文貼過了)

如果是base64地址的就不用http請求了,直接轉二進制再轉InputStream,需要注意的是Base64的import不要用錯了,否則圖片解析不出

import com.lowagie.text.pdf.codec.Base64;

以上就是我在實際開發過程中遇到的問題和解決方法,在這裏做個記錄,也希望對其他人有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章