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;
以上就是我在实际开发过程中遇到的问题和解决方法,在这里做个记录,也希望对其他人有所帮助。