业务场景
最近接到业务需求:把查询的数据转成指定模板的pdf base64字符串。开始接到任务的时候想的是先生成excel文件再转成pdf,最后转换成base64字符串,后续发现编码过程中,excel转pdf出现了问题,后面选择通过html生成pdf base64的方案一样可以满足业务需求,大致实现过程如下:
一、pom文件配置
<!--pdf配置-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-tools</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.20</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
二、工具类编写
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.pdf.BaseFont;
import com.lowagie.text.Image;
import org.apache.commons.io.IOUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.apache.velocity.tools.generic.DateTool;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.Base64Utils;
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.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFSImage;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextImageElement;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
import java.io.*;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
public class PdfUtil {
private static final String HTML_TEMPLATE_FILE = "htmltemplate/template.html";
private static final String FONT = "font/simsun.ttc";
private static final String _BUSINESS_TEMPLATE_ = "_BUSINESS_TEMPLATE_";
/**
* 通过html生成pdf,返回字节数组
* @param dataMap
* @return
*/
public static String generatePdfByHtmlForByteArray(Map<String, Object> dataMap, String businessTemplate) throws IOException {
String templateFileString = html2String();
if (businessTemplate.contains("<html>") && businessTemplate.contains("</html>")){
//1.完整写法的反馈表
//body,需要加上字符集,例如:<body style="font-family:SimSun">
templateFileString = businessTemplate;
} else {
//2.只写<body></body>中内容的反馈表
if (StringUtils.isEmpty(businessTemplate)) {
templateFileString = templateFileString.replace(_BUSINESS_TEMPLATE_, "");
} else {
templateFileString = templateFileString.replace(_BUSINESS_TEMPLATE_, businessTemplate);
}
}
// 添加时间日期格式化dateTool
dataMap.put("businessTemplate", StringUtils.isNotEmpty(businessTemplate));
dataMap.put("dateTool", new DateTool());
dataMap.put("nowTime", Timestamp.valueOf(LocalDateTime.now()));
String html = getHtmlByVelocity(templateFileString, dataMap);
html = html.replace(" ", " ");
return StringUtils.replaceBase64Blank(Base64Utils.encodeToString(htmlTopdfForByteArray(html)));
}
private static String html2String() throws IOException {
ClassPathResource classPathResource = new ClassPathResource(HTML_TEMPLATE_FILE);
return IOUtils.toString(classPathResource.getInputStream(), "utf-8");
}
/**
* 使用velocity生成html
*
* @param dataMap
* @return
* @throws Exception
*/
private static String getHtmlByVelocity(String html, Map<String, Object> dataMap) throws IOException {
VelocityEngine ve = new VelocityEngine();
ve.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
ve.setProperty(Velocity.INPUT_ENCODING, "UTF-8");//指定编码格式,避免生成模板就造成乱码,影响到转pdf后的文件
// ve.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");
ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");// 这是模板所在路径
ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
ve.init();
return mergeTemplateIntoString(ve, html, "UTF-8", dataMap);
}
/**
* html转pdf返回字节数组
* @param html
* @return
*/
public static byte[] htmlTopdfForByteArray(String html) {
OutputStream os = null;
InputStream insm = null;
byte[] byteArray = null;
//临时文件路径,将pdf字节数组返回之后,就将临时文件删除
try {
ITextRenderer renderer = new ITextRenderer();
// step 3 解决中文支持
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont(FONT, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
SharedContext sharedContext = renderer.getSharedContext();
// 解决base64图片支持问题
sharedContext.setReplacedElementFactory(new B64ImgReplacedElementFactory());
sharedContext.getTextRenderer().setSmoothingThreshold(0);
File tempFile = File.createTempFile(UUID.randomUUID().toString().replace("-", ""), "pdf");
os = new FileOutputStream(tempFile);
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(os);
renderer.finishPDF();
os.close();
insm = new FileInputStream(tempFile);
byteArray = new byte[(int) tempFile.length()];
IOUtils.readFully(insm, byteArray);
insm.close();
//删除临时文件
tempFile.delete();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
}
}
if (insm != null) {
try {
insm.close();
} catch (IOException e) {
}
}
}
return byteArray;
}
public static String mergeTemplateIntoString(VelocityEngine velocityEngine, String html, String encoding, Map<String, Object> data)
throws VelocityException {
StringWriter result = new StringWriter();
VelocityContext velocityContext = new VelocityContext(data);
velocityEngine.evaluate(velocityContext, result, encoding, html);
return result.toString();
}
static class B64ImgReplacedElementFactory implements ReplacedElementFactory {
/**
* 实现createReplacedElement 替换html中的Img标签
*
* @param c 上下文
* @param box 盒子
* @param uac 回调
* @param cssWidth css宽
* @param cssHeight css高
* @return ReplacedElement
*/
@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")) {
String attribute = e.getAttribute("src");
FSImage fsImage;
try {
// 生成itext图像
fsImage = buildImage(attribute, uac);
} 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 {
FSImage fsImage = null;
if (srcAttr.startsWith("data:image/")) {
String b64encoded = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
// 解码
byte[] decodedBytes = Base64Utils.decodeFromString(b64encoded);
try {
fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
} catch (com.lowagie.text.BadElementException e) {
e.printStackTrace();
}
} else {
fsImage = uac.getImageResource(srcAttr).getImage();
}
return fsImage;
}
/**
* 实现reset
*/
@Override
public void reset() {
}
@Override
public void remove(Element arg0) {
}
@Override
public void setFormSubmissionListener(FormSubmissionListener arg0) {
}
}
}
三、测试demo
/**
* 根据数据生成pdf base64字符串
*
*
* */
public String getPdfBase64Str(SendDataFileVO vo) throws IOException {
//html
String html = "<p align=\"center\">\n" +
" <span style='font-size:15.0pt;font-weight: bold;'>xxxxx表</span>\n" +
"</p>\n" +
"<table style=\"table-layout:fixed; width: 95%; \">\n" +
" <tr>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"left\"></td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"center\">$!data.bbrq00</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"right\"></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"left\">名称(盖章):</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"center\">类型</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"right\">名称2:$!data.bankName</td>\n" +
" </tr>\n" +
"</table>\n" +
"<table border=\"1\" cellspacing=\"0\" cellpadding=\"0\" style=\"table-layout:fixed;width: 95%;\">\n" +
" <tr>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">文件名称</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">批次号</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">流水号</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">方式</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">笔数</span></td>\n" +
"\t\t<td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">金额</span></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.fileName</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.busiBatch</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.busiNo</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.zffs00</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.cnt</span></td>\n" +
"\t\t<td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.sum</span></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td height=\"50\" colspan=\"4\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">合计</span></td>\n" +
" <td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.cnt</span></td>\n" +
"\t\t<td height=\"50\" colspan=\"1\" rowspan=\"1\" align=\"center\" style=\"word-wrap : break-word\"><span\n" +
" align=\"center\">$!data.sum</span></td>\n" +
" </tr>\n" +
"</table>\n" +
"\n" +
"<table style=\"table-layout:fixed; width: 90%; padding: 10px\">\n" +
" <tr>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"left\">业务经办人:$!data.ywjb00</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"center\">财务经办人:$!data.cwjb00</td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"left\">业务负责人:$!data.ywfh00</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"center\">财务负责人:$!data.cwfh00</td>\n" +
" <td colspan=\"1\" width=\"33%\" align=\"right\">日期:$!data.dyrq00</td>\n" +
" </tr>\n" +
"</table>\n" +
"<p style=\"text-align: left; font-weight: 700\">注:报盘文件名称须按照支付方式、日期、序号顺序排列。</p>";
Map<String, Object> resultData = new HashMap<>();
//接口数据data
JSONObject params = new JSONObject();
params.put("busiNo",vo.getBusiNo());
params.put("fileName",vo.getFileName());
params.put("bankName",vo.getBankName());
params.put("cnt",vo.getCnt());
params.put("sum",vo.getSum());
params.put("busiBatch",vo.getBusiBatch());
params.put("ywjb00",vo.getYwjb00());
params.put("ywfh00",vo.getYwfh00());
params.put("cwjb00",vo.getCwjb00());
params.put("cwfh00",vo.getCwfh00());
params.put("zffs00",vo.getZffs00());
params.put("bbrq00",vo.getBbrq00());
//获取当前年月日
Integer nowdate = Integer.parseInt(DateUtil.dateNoSymbol().substring(0, 8));
params.put("dyrq00", DateFormatUtil.stringToYMDFormat(nowdate.toString()));
resultData.put("data", params);
String base64File = PdfUtil.generatePdfByHtmlForByteArray(resultData, html);
log.info("===================================================报盘文件字符串《"+vo.getBankName()+"》");
log.info("==================================================="+base64File);
return base64File;
}
四、base64字符串测试地址
https://www.ec95.com/
尾言
以上就是大致的实现过程,唯一不方便的是,对应的模板都需用html画出来才能进行使用,后续有更好的方式也会同步到文档上,感谢阅读。