项目有个业务需求,就是客户需要我们提供一个可以导出企业各项数据的word平台,数据是从企通查和天眼查查过来的,其中有个表是企业的发展历程数据,需要我们将各大模块数据从第三方查询出来然后导出word
根据我们课题研究,最后决定用Freemarker模板引擎画发展历程页面(要图片显示的),然后将发展历程的ftl文件转化成html再转成img,然后将发展历程的img转成base64再引入进word页面的ftl最后导出word。这样word里不但有图片也有自己画的表格。然后转图片这块出现了问题
由于项目原因不能公开页面截图了
首先在网上找解决办法,大多数都是用的xhtmlrenderer包
这个包我亲测了,现在是2022年,我试了,很多样式直接导出来不显示,尤其是CSS3的,像我们页面用的flex啥的压根样式都不对,所以这就有问题了,毕竟工作还得给人家弄是么
我去maven远程仓库查了下jar包
可以看到这个jar包已经有十年没升级了,已经不维护了
不过他还有另外一个版本
这个flying-saucer倒是还在维护,不过也根本不好使,border-radius该不好使还不好使。
后来看到CSDN有个老哥同样也遇到个这个问题 https://blog.csdn.net/xcc_2269861428/article/details/85246815
他想实现的功能跟我们差不多,我就试了一下,果真好用,而且还简单
工具下载地址
https://wkhtmltopdf.org/downloads.html
最后附上我们的转换代码
application.yml
# FTL转ImageBase54
ftlToImg:
resourcePath: "templates"
modelName: "companyInfo.ftl"
inputFileName: "E:/JianBao/"
wkhtmltopdf: "F:/wkhtmltopdf/bin/wkhtmltoimage.exe"
# WORD导出目录
create:
report_createFilePath_50_folder: E://JianBao/downloadPath/
spring.freemarker.template-loader-path=classpath:/templates/
FtlToHtmlToImg
package com.cei.utils;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import sun.misc.BASE64Encoder;
import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
@Component
public class FtlToHtmlToImg {
private static FtlToHtmlToImg staticInstance = new FtlToHtmlToImg();
// 资源绝对路径
@Value("${ftlToImg.resourcePath}")
private String resourcePath;
// 文件ftl模板名称
@Value("${ftlToImg.modelName}")
private String modelName;
// 文件输出路径
@Value("${ftlToImg.inputFileName}")
private String inputFileName;
// wkhtmltopdf平台路径(安装本地的绝对路径,到bin里面的exe)
@Value("${ftlToImg.wkhtmltopdf}")
private String wkhtmltopdf;
@Autowired
FreeMarkerConfigurer freeMarkerConfigurer;
@PostConstruct
public void init() {
staticInstance.resourcePath = resourcePath;
staticInstance.modelName = modelName;
staticInstance.inputFileName = inputFileName;
staticInstance.wkhtmltopdf = wkhtmltopdf;
staticInstance.freeMarkerConfigurer = freeMarkerConfigurer;
}
/**
* 获取模板转img
*
* @param
* @return
*/
public List<Map<String, Object>> createImgBase64(List<Map<String, Object>> dataList, BigInteger companyId, String companyName) {
File file = new File(staticInstance.inputFileName + companyId + ".html");
// 如果文件夹不存在则创建文件夹
if (!file.exists() && !file.isDirectory()) {
//创建上级目录
file.getParentFile().mkdirs();
}
//ftl转html
String ftlFile = staticInstance.modelName;
String html = null;
try {
html = ftlToString(dataList, ftlFile, companyId, companyName);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TemplateException e) {
throw new RuntimeException(e);
}
// html转图片转base64
List<Map<String, Object>> ImageBase64 = new ArrayList<>();
try {
ImageBase64 = ImageRender(html, companyId, companyName);
} catch (IOException e) {
e.printStackTrace();
}
return ImageBase64;
}
/**
* @param map 需要填充的数据集合
* @param templateName 被填充的ftl文件
* @return html数据
* @throws IOException
* @throws TemplateException
* @Description: 将ftl文件转html文件
* @Author:
* @Date:
*/
public String ftlToString(List<Map<String, Object>> dataList, String templateName, BigInteger companyId, String companyName) throws IOException, TemplateException {
LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
String value = "";
// Configuration configuration = new Configuration();
// Resource resource = new ClassPathResource(staticInstance.resourcePath);
// File sourceFile = resource.getFile();
// String ftlPath = sourceFile.getAbsolutePath();
String filName = templateName;
String encoding = "UTF-8";
StringWriter out = new StringWriter();
// configuration.setDirectoryForTemplateLoading(new File(ftlPath));
Template template = staticInstance.freeMarkerConfigurer.getConfiguration().getTemplate(filName, encoding);
template.setEncoding(encoding);
Map<String, Object> map = new HashMap<>();
map.put("historyList", dataList);
template.process(map, out);
out.flush();
out.close();
value = out.getBuffer().toString();
LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
return value;
}
/**
* @param html html代码
* @return base64图片
* @throws IOException
* @Description: 将html转换成图片并切割
* @Author:
* @Date:
*/
public List<Map<String, Object>> ImageRender(String html, BigInteger companyId, String companyName) throws IOException {
LoggerUtil.info("开始输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
// 将html输出为文件
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(staticInstance.inputFileName + companyId + ".html"), StandardCharsets.UTF_8));
bufferedWriter.write(html);
bufferedWriter.newLine();
File source = new File(staticInstance.inputFileName + companyId + ".html");
bufferedWriter.flush();
bufferedWriter.close();
LoggerUtil.info("结束输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
// 将输出的html文件转化为图片
ProcessBuilder pb = new ProcessBuilder(staticInstance.wkhtmltopdf, staticInstance.inputFileName + companyId + ".html", staticInstance.inputFileName + companyId + ".png");
Process process;
try {
LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
process = pb.start();
//注意,调用process.getErrorStream()而不是process.getInputStream()
BufferedReader errStreamReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line = null;
line = errStreamReader.readLine();
while (line != null) {
line = errStreamReader.readLine();
}
errStreamReader.close();
process.destroy();
LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
LoggerUtil.info("开始切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
// 读入大图
File file = new File(staticInstance.inputFileName + companyId + ".png");
FileInputStream fis = new FileInputStream(file);
BufferedImage image = ImageIO.read(fis);
fis.close();
// 如果图片太大将切割图片
// word里一页需要图片多高
// 平均高度1726是算出来的,在word里一页大小是 height:697.4pt width:413.7pt
// 工具生成的图片宽度是1024,所以缩放最终得的结果是word里一页高度需要图片高度1726
int picHeight = 1726;
int picHeightTemp = 0;
// 计算每个小图的宽度和高度
int chunkWidth = image.getWidth();
int chunkHeight = picHeight;
int rows = 1;
// 图片竖着分几块
if (image != null && image.getHeight() > 1726) {
rows = (image.getHeight() / 1726) + 1;
picHeightTemp = image.getHeight() % 1726;
} else {
picHeightTemp = image.getHeight();
}
// 图片被切割成几块
int chunks = rows;
//大图中的一部分
int count = 0;
BufferedImage imgs[] = new BufferedImage[chunks];
for (int x = 0; x < rows; x++) {
// if判断,最后一张如果固定写死高度,会导致图片有上一张图内容拼接
// 所以最后一张切割的高度,需要取余判断图片按平均分配高度之后,最后一张是剩余了多高
if ((x + 1) == rows) {
//设置小图的大小和类型
imgs[count] = new BufferedImage(chunkWidth, picHeightTemp, image.getType());
//写入图像内容
// drawImage的参数,自己点进去源码里看官方注释
Graphics2D gr = imgs[count++].createGraphics();
gr.drawImage(image, 0, 0, chunkWidth, picHeightTemp, 0, chunkHeight * x, chunkWidth, image.getHeight(), null);
gr.dispose();
} else {
imgs[count] = new BufferedImage(chunkWidth, chunkHeight, image.getType());
Graphics2D gr = imgs[count++].createGraphics();
gr.drawImage(image, 0, 0, chunkWidth, chunkHeight, 0, chunkHeight * x, chunkWidth, chunkHeight * x + chunkHeight, null);
gr.dispose();
}
}
// 图片被切割的base64
List<Map<String, Object>> base64List = new ArrayList<>();
// 输出小图
for (int i = 0; i < imgs.length; i++) {
//ImageIO.write(imgs[i], "jpg", new File("C:\\img\\split\\img" + i + ".jpg"));
// ImageIO.write(imgs[i], "png", new File("C:\\Users\\87151\\Desktop\\ceshi\\" + i + ".png"));
Map<String, Object> map = new HashMap<>();
// 图片base64
map.put("url", imageToBase64(imgs[i]));
// 图片文件名,不能重复,要不word生成的图片也重复
map.put("name", 10000 + i);
// 动态高度,防止最后一张拉伸
if (i + 1 == imgs.length) {
map.put("myheight", picHeightTemp / 2.475);
} else {
map.put("myheight", "697.4");
}
base64List.add(map);
}
LoggerUtil.info("结束切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
// 删除临时文件缓存
deleteTempFile(companyId);
return base64List;
} catch (IOException e) {
e.printStackTrace();
}
return new ArrayList<>();
}
/**
* 文件BufferedImage类型转BASE64
*
* @param bufferedImage
* @return
*/
public String imageToBase64(BufferedImage bufferedImage) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();//io流
try {
ImageIO.write(bufferedImage, "png", baos);//写入流中
baos.flush();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes = baos.toByteArray();//转换成字节
BASE64Encoder encoder = new BASE64Encoder();
String png_base64 = encoder.encodeBuffer(bytes).trim();//转换成base64串
png_base64 = png_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n
return png_base64;
}
/**
* 删除临时文件缓存
*/
public void deleteTempFile(BigInteger companyId) {
LoggerUtil.info("开始清除企业ID【" + companyId + "】的临时缓存文件成功");
// 读取临时文件
File file1 = new File(staticInstance.inputFileName + companyId + ".html");
File file2 = new File(staticInstance.inputFileName + companyId + ".png");
// 删除文件
if (file1 != null) {
file1.delete();
}
if (file2 != null) {
file2.delete();
}
LoggerUtil.info("结束清除企业ID【" + companyId + "】的临时缓存文件成功");
}
}
需要调用的类
// 发展历程数据写进FTL,再把FTL转成HTML,再把HTML转成IMG,把IMG转成BASE64返回来
FtlToHtmlToImg ftlToHtmlToImg = new FtlToHtmlToImg();
List<Map<String, Object>> devolopBase64List = ftlToHtmlToImg.createImgBase64(devolopEsResult, companyId, companyName);
注意我们这边的参数,如果扒用的话需要酌情修改,我们参数是1.数据参数 2.企业id 3.企业名,企业ID和企业名字单纯就是为了传过去打印Log4j日志的,工具类里面的LoggerUtil是我们自己封装的Log4j的类,自己删了就行。
工具类里面的方法大多数都是直接网上找的,不过找的帖子不是一个,太多没法发转发链接了,部分我们自己业务后补的是我自己写的。
spring.freemarker.template-loader-path=classpath:/templates/
这句配置对应工具类里的
@Autowired
FreeMarkerConfigurer freeMarkerConfigurer;
必加,因为如果单纯的用 new ClassPathResource 的话打包成jar包运行会获取不到resource的资源
这里可以参考我另一个文章 https://blog.csdn.net/qq_37241221/article/details/126048613
导出word的话,里面图片太大超过了word一页内容,word可能要么压缩图片高度,要么就直接后面内容不显示了,我这里自己写了个切割方案,会根据图片的高度自己判断该不该切割,然后导入到word,1726那个数字是我根据word一页多高自己算的,亲测好用
最后附上切割效果图