文章目录
SpringBoot + Echars + Thymeleaf 后端转html,pdf
-
需求:
后端定期跑批,生成pdf文件发送给客户。 -
版本:
SpringBoot:2.1.4.RELEASE -
思路
- 后端将echars报表生成jpg,png图片
- 通过Thymeleaf 模板生成html
- 将html转成pdf文件
1. 需要引用的依赖
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- html 转 pdf 需要用的jar -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>[添加链接描述](https://pan.baidu.com/s/1lvvdfPT1-Q9PENHq4HO5Ow)
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.6</version>
</dependency>
2. 后端将echars报表生成jpg
2.1 phantomjs及echarts-convert资料下载
https://pan.baidu.com/s/1lvvdfPT1-Q9PENHq4HO5Ow
提取码:dq3z
2.1.1 phantomjs介绍
可以理解为一个插件,后端听过指令调用该插件将echars报表生成图片。
2.1.2 echarts-convert.js
echars相关js脚本,phantomjs执行需要的引用依赖。
2.2 echars转image.png
2.2.1 EchartsFileService 调用服务
@Service
public class EchartsFileServiceImpl implements EchartsFileService {
public static final Logger logger = LoggerFactory.getLogger(EchartsFileServiceImpl.class);
@Autowired
private EchartsUtil echartsUtil;
/**
* @param options json echar图标数据
* @return
*/
@Override
public String creatEchartsFile(String options) {
try {
JSON.parseObject(options);
} catch (Exception e) {
logger.error("options is not jsonString");
return null;
}
return echartsUtil.generateEChart(options);
}
}
2.2.2 EchartsUtil 工具生成图片
@Component
public class EchartsUtil {
public static final Logger logger = LoggerFactory.getLogger(EchartsUtil.class);
// phantomjs执行器目录:
@Value("${echars.phantomJs}")
private String phantomJs;
// 图片生成的临时文件目录
@Value("${echars.fileDirectory}")
private String fileDirectory;
@Value("${echars.jsPath}")
private String jsPath;
// 指令: 执行器路径 echarts-convert.js路径 -infile 数据源 -outfile 输出文件
private final String EXEC_CMD = "{0} {1} -infile {2} -outfile {3}";
/**
* @param options 报表数据源
* @return 生成文件地址 返回图片生成的路径
*/
public String generateEChart(String options) {
// 随机生成文件名
String wFileName = UUID.randomUUID().toString();
String imageName = wFileName + ".png";
String jsonPath = writeFile(options, wFileName);
// 生成的文件存储路径
String imagePath = fileDirectory + imageName;
try {
File file = new File(jsonPath); //文件路径
if (!file.exists()) {
File dir = new File(file.getParent());
dir.mkdirs();
file.createNewFile();
}
String cmd = MessageFormat.format(EXEC_CMD, phantomJs, jsPath, jsonPath, imagePath);
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
logger.error("error: {}", e);
}
logger.info("echars file path: {}", imagePath);
return imagePath;
}
/*
* options:json数据
* wFileName:文件输出路径
* 生成phantomjs生成图片的数据文件
* return .json文件全路径
*/
public String writeFile(String options, String wFileName) {
/* option写入文本文件 用于执行命令*/
String wjsonPath = fileDirectory + wFileName + ".json";
BufferedWriter out = null;
try {
File jsonFile = new File(wjsonPath);
if (!jsonFile.exists()) {
File dir = new File(jsonFile.getParent());
dir.mkdirs();
jsonFile.createNewFile();
}
out = new BufferedWriter(new FileWriter(jsonFile));
out.write(options);
out.flush(); // 把缓存区内容压入文件
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close(); // 最后关闭文件
}
} catch (IOException e) {
e.printStackTrace();
}
}
return wjsonPath;
}
}
2.2.3 application.propertyies
## echars图片生成相关插件路径配置
# phantomJs 执行路径
zifisense.echars.phantomJs=xxx\\phantomjs\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe
# echar生成文件目录后最\\或者是/不可缺少
zifisense.echars.fileDirectory=C:\\Users\\Desktop\\model\\
# echarts-convert.js文件路径
zifisense.echars.jsPath=xxx\\echarts-convert\\echarts-convert.js
3. Thymeleaf 模板生成html,pdf
3.1.1 ConstantConfigurations 相关配置信息
/**
* Thymeleaf配置参数 参数配置化对象
*/
@Configuration
@ConfigurationProperties(
prefix = "constant"
)
@Data
public class ConstantConfigurations {
// html文件生成默认路径
private String indexStorage;
// 指定模板解析器模板文件路径前缀,根路径resources
private String resolverPrefix = "/config/templates/model/";
// 指定模板解析器模板文件后缀,根路径resources
private String resolverSuffix = ".html";
}
3.1.2 ThymeleafService 转html pdf
@Service
public class ThymeleafServiceImpl implements ThymeleafService {
public static final Logger logger = LoggerFactory.getLogger(ThymeleafServiceImpl.class);
/**
* sifisense 参数配置化对象
*/
@Autowired
private ConstantConfigurations ConstantConfigurations ;
/**
* 模板引擎
*/
@Autowired
private TemplateEngine templateEngine;
/**
* 创建出一个name.html文件
*
* @param templateName 模板名称
* @param name 生成name.xml
* @param map 模板映射参数
*/
@Override
public String createHtml(String templateName, String name, Map<String, Object> map) {
PrintWriter writer = null;
try {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
// 1. 创建模板解析目录解析器
Set<ITemplateResolver> templateResolvers = templateEngine.getTemplateResolvers();
// 无配置模板解析路径,则代码配置
if(!templateResolvers.iterator().hasNext()) {
// 2. 创建模板解析器 并设置相关属性
resolver.setPrefix(ConstantConfigurations .getResolverPrefix());
resolver.setSuffix(ConstantConfigurations .getResolverSuffix());
// 不允许重复设置 否则会报错
templateEngine.setTemplateResolver(resolver);
}
// 2. 模板上下文 主要存储Model参数
Context context = new Context();
if (map.size() > 0) {
context.setVariables(map);
}
// 3. 创建输出文件
File folder = new File(ConstantConfigurations .getIndexStorage(), name + ".html");
//如果文件不存在,直接创建
if (!folder.exists()) {
folder.createNewFile();
}
// 5. 获取输出目标文件输出流
writer = new PrintWriter(folder, "UTF-8");
// 6. 生成静态模板参数1:template模板名称 参数2:上下文对象 参数3:目标文件输出流
templateEngine.process(templateName, context, writer);
logger.info("http path: {}", folder.getAbsolutePath());
// 返回生成文件路径
return folder.getAbsolutePath();
} catch (IOException e) {
logger.error("createHtml error {}", ExceptionUtil.getStackTrace(e));
} finally {
// flush输出流并关闭
if (writer != null) {
writer.flush();
writer.close();
}
}
return "";
}
/**
* 根据html生成PDF
*
* @param html html内容
* @param file 输出pdf文件的路径
* @throws DocumentException
* @throws IOException
*/
@Override
public void htmlToPdf(String html, File file) {
/**
* 切记 css 要定义在head 里,否则解析失败
* css 要定义字体
* 例如宋体style="font-family:SimSun"用simsun.ttc
*/
if (!file.exists()) {
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
logger.info("开始根据html生成pdf,html={}", html);
OutputStream out = null;
try {
out = new FileOutputStream(file);
ITextRenderer renderer = new ITextRenderer();
// 携带图片,将图片标签转换为itext自己的图片对象
renderer.getSharedContext().setReplacedElementFactory(new PdfBase64ImgReplacedElementFactory());
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
// 解决中文支持问题
ITextFontResolver fontResolver = renderer.getFontResolver();
// 设置语言包文件 //设置字体,否则不支持中文,在html中使用字体,html{ font-family: SimSun;}
fontResolver.addFont("config/templates/model/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// 如果是本地图片使用 file:,这里指定图片的父级目录。html上写相对路径,
// renderer.getSharedContext().setBaseURL("file:/E:/img/")
// 处理图片
//renderer.getSharedContext().setBaseURL(IMG_SAVE_URL);
renderer.layout();
renderer.createPDF(out);
out.flush();
logger.info("pdf生成成功");
} catch (DocumentException e) {
logger.error("pdf生成失败,cause--->" + e.getMessage());
} catch (IOException e) {
logger.error("pdf生成失败,cause--->" + e.getMessage());
} finally {
try {
if (null != out) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void htmlToPdf(String htmlPath, String outPath) {
/**
* 切记 css 要定义在head 里,否则解析失败
* css 要定义字体
* 例如宋体style="font-family:SimSun"用simsun.ttc
*/
if (StringUtils.isBlank(outPath)) {
logger.info("pdf 输出路径为null");
return;
}
String bufferHtml = readHtmlFile(htmlPath);
File file = new File(outPath);
if (!file.exists()) {
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
logger.info("开始根据html生成pdf,html={}", htmlPath);
OutputStream out = null;
try {
out = new FileOutputStream(file);
ITextRenderer renderer = new ITextRenderer();
// 携带图片,将图片标签转换为itext自己的图片对象
renderer.getSharedContext().setReplacedElementFactory(new PdfBase64ImgReplacedElementFactory());
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
// 解决中文支持问题
ITextFontResolver fontResolver = renderer.getFontResolver();
// 设置语言包文件 //设置字体,否则不支持中文,在html中使用字体,html{ font-family: SimSun;}
fontResolver.addFont("config/templates/model/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.setDocumentFromString(bufferHtml);
// 如果是本地图片使用 file:,这里指定图片的父级目录。html上写相对路径,
// renderer.getSharedContext().setBaseURL("file:/E:/img/")
// 处理图片
//renderer.getSharedContext().setBaseURL(IMG_SAVE_URL);
renderer.layout();
renderer.createPDF(out);
out.flush();
logger.info("pdf生成成功");
} catch (DocumentException e) {
logger.error("pdf生成失败,cause--->" + e.getMessage());
} catch (IOException e) {
logger.error("pdf生成失败,cause--->" + e.getMessage());
} finally {
try {
if (null != out) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 删除id.html
*
* @param id
*/
@Override
public void deleteHtml(String id) {
}
public String readHtmlFile(String htmlPath) {
File file = new File(htmlPath);
if (!file.exists()) {
logger.error("html file is not exists, file path: {}", htmlPath);
// 文件不存在直接返回
return "";
}
BufferedReader reader = null;
StringBuffer sbf = new StringBuffer();
try {
reader = new BufferedReader(new FileReader(file));
String tempStr;
while ((tempStr = reader.readLine()) != null) {
sbf.append(tempStr);
}
reader.close();
return sbf.toString();
} catch (IOException e) {
logger.error("error:", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.error("error:", e);
}
}
}
return sbf.toString();
}
}
3.1.3 ImgBase64Util
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.StringUtils;
public class ImgBase64Util {
public static void main(String[] args) throws Exception {
//本地图片地址
String url = "C:/Users/Administrator/Desktop/628947887489084892.jpg";
//在线图片地址
String string = "http://bpic.588ku.com//element_origin_min_pic/17/03/03/7bf4480888f35addcf2ce942701c728a.jpg";
/*
String str = Base64Utils.ImageToBase64ByLocal(url);
String ste = Base64Utils.ImageToBase64ByOnline(string);
System.out.println(str);
Base64Utils.Base64ToImage(str,"C:/Users/Administrator/Desktop/test1.jpg");
Base64Utils.Base64ToImage(ste, "C:/Users/Administrator/Desktop/test2.jpg");*/
}
/**
* 本地图片转换成base64字符串
*
* @param imgFile 图片本地路径
* @return
*/
public static String ImageToBase64ByLocal(String imgFile) {// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
InputStream in = null;
byte[] data = null;
// 读取图片字节数组
try {
in = new FileInputStream(imgFile);
data = new byte[in.available()];
in.read(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return DatatypeConverter.printBase64Binary(data);
}
/**
* 在线图片转换成base64字符串
*
* @param imgURL 图片线上路径
* @return
*/
public static String ImageToBase64ByOnline(String imgURL) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
InputStream is = null;
try {
// 创建URL
URL url = new URL(imgURL);
byte[] by = new byte[1024];
// 创建链接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
is = conn.getInputStream();
// 将内容读取内存中
int len = -1;
while ((len = is.read(by)) != -1) {
data.write(by, 0, len);
}
// 关闭流
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return DatatypeConverter.printBase64Binary(data.toByteArray());
// // 对字节数组Base64编码
// BASE64Encoder encoder = new BASE64Encoder();
// return encoder.encode(data.toByteArray());
}
/**
* base64字符串转换成图片
*
* @param imgStr base64字符串
* @param imgFilePath 图片存放路径
* @return
*/
public static boolean Base64ToImage(String imgStr, String imgFilePath) { // 对字节数组字符串进行Base64解码并生成图片
if (StringUtils.isEmpty(imgStr)) // 图像数据为空
return false;
OutputStream out = null;
try {
byte[] b = DatatypeConverter.parseBase64Binary(imgStr);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {// 调整异常数据
b[i] += 256;
}
}
out = new FileOutputStream(imgFilePath);
out.write(b);
out.flush();
return true;
} catch (Exception e) {
return false;
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.1.4 PdfBase64ImgReplacedElementFactory html转pdf图片处理自定义工厂
public class PdfBase64ImgReplacedElementFactory 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 (BadElementException e1) {
fsImage = null;
} 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图像
*/
protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
BadElementException {
FSImage fiImg = null;
//图片的src要为src="data:image/jpg;base64,{图片的base64code}"这种base64格式
if (srcAttr.toLowerCase().startsWith("data:image/")) {
String base64Code = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
// 解码
byte[] decodedBytes = Base64.decode(base64Code);
fiImg = new ITextFSImage(Image.getInstance(decodedBytes));
} else {
fiImg = uac.getImageResource(srcAttr).getImage();
}
return fiImg;
}
@Override
public void reset() {
}
@Override
public void remove(Element arg0) {
}
@Override
public void setFormSubmissionListener(FormSubmissionListener arg0) {
}
}
3.1.7 EcharTemplate.html 模板
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot中使用ECharts</title>
<style>
html {
font-family: SimSun;
}
.margin_auto {
margin: 0 auto;
}
.title {
height: 100px;
width: 100px;
text-align: center;
line-height: 100px;
margin: 0 auto;
}
table.gridtable {
margin: auto;
font-family: verdana, arial, sans-serif;
font-size: 11px;
color: #333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
table.gridtable th {
font-family: SimSun;
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #dedede;
}
table.gridtable td {
font-family: SimSun;
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #ffffff;
}
</style>
</head>
<body>
<div>
<div class="title" th:text="${title}"></div>
<div class="margin_auto">
<img th:src="@{${imageUrl}}" style="width: 600px;height:400px;"></img>
</div>
</div>
<dev style="text-align: center;">
<table class="gridtable" width='80%' border='1'>
<tr th:each="td: ${tdList}">
<td th:text="${td.A}"></td>
<td th:text="${td.B}"></td>
<td th:text="${td.C}"></td>
</tr>
</table>
</dev>
</body>
</html>
3.1.6 application.properties
## thymeleaf配置
# 开发过程关闭缓存,默认是开启的
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:config/templates/model/
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=utf-8
spring.thymeleaf.mode=HTML
spring.thymeleaf.servlet.content-type=text/html
# H5模板生成的路径
constant.indexStorage=C:\\Users\\Desktop\\model
4. 测试接口
- TestController1
@RestController
@RequestMapping("/test1")
public class TestController1 {
@Autowired
private EchartsFileService echartsFileService;
@Autowired
private ThymeleafService thymeleafService;
@RequestMapping("/create")
public String test() {
Map<String, Object> map = loadModel();
String htmlPath = thymeleafService.createHtml("EcharTemplate", "pdf", map);
// 生成pdf
thymeleafService.htmlToPdf(htmlPath, map.get("pdf_path").toString());
return map.get("pdf_path").toString();
}
/**
* 模拟测试数据
*
* @return
*/
public Map<String, Object> loadModel() {
String options = "{\n" +
" tooltip: {\n" +
" trigger: 'item'\n" +
" },\n" +
" legend: {\n" +
" top: '5%',\n" +
" left: 'center'\n" +
" },\n" +
" series: [\n" +
" {\n" +
" name: '访问来源',\n" +
" type: 'pie',\n" +
" radius: ['40%', '70%'],\n" +
" avoidLabelOverlap: false,\n" +
" itemStyle: {\n" +
" borderRadius: 10,\n" +
" borderColor: '#fff',\n" +
" borderWidth: 2\n" +
" },\n" +
" label: {\n" +
" show: false,\n" +
" position: 'center'\n" +
" },\n" +
" emphasis: {\n" +
" label: {\n" +
" show: true,\n" +
" fontSize: '40',\n" +
" fontWeight: 'bold'\n" +
" }\n" +
" },\n" +
" labelLine: {\n" +
" show: false\n" +
" },\n" +
" data: [\n" +
" {value: 1048, name: '搜索引擎'},\n" +
" {value: 735, name: '直接访问'},\n" +
" {value: 580, name: '邮件营销'},\n" +
" {value: 484, name: '联盟广告'},\n" +
" {value: 300, name: '视频广告'}\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}";
// 创建图片
String imagePath = echartsFileService.creatEchartsFile(options);
while(true){
File file = new File(imagePath);
if(file.exists()) {
break;
}
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
List<TdBean> tbList = new ArrayList<>();
int listSize = 1000;
for(int i = 0 ; i < listSize; i++) {
TdBean tb = new TdBean();
tb.setA("A" + i);
tb.setB("B" + i);
tb.setC("C" + i);
tbList.add(tb);
}
Map<String, Object> map = new HashMap<>();
map.put("tdList", tbList);
map.put("name", "小朋友");
map.put("age", 18);
map.put("email", "xiaopengyou@qq.com");
// 需要将图片转base64
map.put("imageUrl", "data:image/png;base64," + ImgBase64Util.ImageToBase64ByLocal(imagePath));
map.put("pdf_path", imagePath.substring(0, imagePath.lastIndexOf(".")) + ".pdf");
return map;
}
}
- 结果
5 补充
在linux 或是 ubuntu 中文乱码问题解决
在centos中执行:yum install bitmap-fonts bitmap-fonts-cjk
在ubuntu中执行:sudo apt-get install xfonts-wqy