来源:xhtmlrenderer + iText-HTML转PDF_hunan961的博客-CSDN博客_xhtmlrenderer
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);@Overridepublic 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;}@Overridepublic void reset() {}@Overridepublic void remove(Element e) {}@Overridepublic 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;