项目有个业务需求,就是客户需要我们提供一个可以导出企业各项数据的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
- 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
> 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
> 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
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
> 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
> 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
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
> 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一页多高自己算的,亲测好用
最后附上切割效果图