xhtmlrenderer + iText - HTML轉PDF
xhtmlrendere+itext2.0.8 將html轉成pdf,帶樣式、圖片(也支持二維碼、條形碼)等
主要步驟
- 生成html(css樣式直接放在style中)
- html轉換pdf方法
- 數據返回給前端
詳細過程
- 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;
以上就是我在實際開發過程中遇到的問題和解決方法,在這裏做個記錄,也希望對其他人有所幫助。