• 关于我用xhtmlrenderer将html转换img结果样式飞了的这档事


    项目有个业务需求,就是客户需要我们提供一个可以导出企业各项数据的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

    1. # FTL转ImageBase54
    2. ftlToImg:
    3. resourcePath: "templates"
    4. modelName: "companyInfo.ftl"
    5. inputFileName: "E:/JianBao/"
    6. wkhtmltopdf: "F:/wkhtmltopdf/bin/wkhtmltoimage.exe"
    7. # WORD导出目录
    8. create:
    9. report_createFilePath_50_folder: E://JianBao/downloadPath/
    10. spring.freemarker.template-loader-path=classpath:/templates/

    FtlToHtmlToImg

    1. package com.cei.utils;
    2. import freemarker.template.Configuration;
    3. import freemarker.template.Template;
    4. import freemarker.template.TemplateException;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.beans.factory.annotation.Value;
    7. import org.springframework.core.io.ClassPathResource;
    8. import org.springframework.core.io.Resource;
    9. import org.springframework.stereotype.Component;
    10. import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
    11. import sun.misc.BASE64Encoder;
    12. import javax.annotation.PostConstruct;
    13. import javax.imageio.ImageIO;
    14. import java.awt.*;
    15. import java.awt.image.BufferedImage;
    16. import java.io.*;
    17. import java.math.BigInteger;
    18. import java.nio.charset.StandardCharsets;
    19. import java.util.*;
    20. import java.util.List;
    21. @Component
    22. public class FtlToHtmlToImg {
    23. private static FtlToHtmlToImg staticInstance = new FtlToHtmlToImg();
    24. // 资源绝对路径
    25. @Value("${ftlToImg.resourcePath}")
    26. private String resourcePath;
    27. // 文件ftl模板名称
    28. @Value("${ftlToImg.modelName}")
    29. private String modelName;
    30. // 文件输出路径
    31. @Value("${ftlToImg.inputFileName}")
    32. private String inputFileName;
    33. // wkhtmltopdf平台路径(安装本地的绝对路径,到bin里面的exe)
    34. @Value("${ftlToImg.wkhtmltopdf}")
    35. private String wkhtmltopdf;
    36. @Autowired
    37. FreeMarkerConfigurer freeMarkerConfigurer;
    38. @PostConstruct
    39. public void init() {
    40. staticInstance.resourcePath = resourcePath;
    41. staticInstance.modelName = modelName;
    42. staticInstance.inputFileName = inputFileName;
    43. staticInstance.wkhtmltopdf = wkhtmltopdf;
    44. staticInstance.freeMarkerConfigurer = freeMarkerConfigurer;
    45. }
    46. /**
    47. * 获取模板转img
    48. *
    49. * @param
    50. * @return
    51. */
    52. public List> createImgBase64(List> dataList, BigInteger companyId, String companyName) {
    53. File file = new File(staticInstance.inputFileName + companyId + ".html");
    54. // 如果文件夹不存在则创建文件夹
    55. if (!file.exists() && !file.isDirectory()) {
    56. //创建上级目录
    57. file.getParentFile().mkdirs();
    58. }
    59. //ftl转html
    60. String ftlFile = staticInstance.modelName;
    61. String html = null;
    62. try {
    63. html = ftlToString(dataList, ftlFile, companyId, companyName);
    64. } catch (IOException e) {
    65. throw new RuntimeException(e);
    66. } catch (TemplateException e) {
    67. throw new RuntimeException(e);
    68. }
    69. // html转图片转base64
    70. List> ImageBase64 = new ArrayList<>();
    71. try {
    72. ImageBase64 = ImageRender(html, companyId, companyName);
    73. } catch (IOException e) {
    74. e.printStackTrace();
    75. }
    76. return ImageBase64;
    77. }
    78. /**
    79. * @param map 需要填充的数据集合
    80. * @param templateName 被填充的ftl文件
    81. * @return html数据
    82. * @throws IOException
    83. * @throws TemplateException
    84. * @Description: 将ftl文件转html文件
    85. * @Author:
    86. * @Date:
    87. */
    88. public String ftlToString(List> dataList, String templateName, BigInteger companyId, String companyName) throws IOException, TemplateException {
    89. LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
    90. String value = "";
    91. // Configuration configuration = new Configuration();
    92. // Resource resource = new ClassPathResource(staticInstance.resourcePath);
    93. // File sourceFile = resource.getFile();
    94. // String ftlPath = sourceFile.getAbsolutePath();
    95. String filName = templateName;
    96. String encoding = "UTF-8";
    97. StringWriter out = new StringWriter();
    98. // configuration.setDirectoryForTemplateLoading(new File(ftlPath));
    99. Template template = staticInstance.freeMarkerConfigurer.getConfiguration().getTemplate(filName, encoding);
    100. template.setEncoding(encoding);
    101. Map map = new HashMap<>();
    102. map.put("historyList", dataList);
    103. template.process(map, out);
    104. out.flush();
    105. out.close();
    106. value = out.getBuffer().toString();
    107. LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
    108. return value;
    109. }
    110. /**
    111. * @param html html代码
    112. * @return base64图片
    113. * @throws IOException
    114. * @Description: 将html转换成图片并切割
    115. * @Author:
    116. * @Date:
    117. */
    118. public List> ImageRender(String html, BigInteger companyId, String companyName) throws IOException {
    119. LoggerUtil.info("开始输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
    120. // 将html输出为文件
    121. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(staticInstance.inputFileName + companyId + ".html"), StandardCharsets.UTF_8));
    122. bufferedWriter.write(html);
    123. bufferedWriter.newLine();
    124. File source = new File(staticInstance.inputFileName + companyId + ".html");
    125. bufferedWriter.flush();
    126. bufferedWriter.close();
    127. LoggerUtil.info("结束输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
    128. // 将输出的html文件转化为图片
    129. ProcessBuilder pb = new ProcessBuilder(staticInstance.wkhtmltopdf, staticInstance.inputFileName + companyId + ".html", staticInstance.inputFileName + companyId + ".png");
    130. Process process;
    131. try {
    132. LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
    133. process = pb.start();
    134. //注意,调用process.getErrorStream()而不是process.getInputStream()
    135. BufferedReader errStreamReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    136. String line = null;
    137. line = errStreamReader.readLine();
    138. while (line != null) {
    139. line = errStreamReader.readLine();
    140. }
    141. errStreamReader.close();
    142. process.destroy();
    143. LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
    144. LoggerUtil.info("开始切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
    145. // 读入大图
    146. File file = new File(staticInstance.inputFileName + companyId + ".png");
    147. FileInputStream fis = new FileInputStream(file);
    148. BufferedImage image = ImageIO.read(fis);
    149. fis.close();
    150. // 如果图片太大将切割图片
    151. // word里一页需要图片多高
    152. // 平均高度1726是算出来的,在word里一页大小是 height:697.4pt width:413.7pt
    153. // 工具生成的图片宽度是1024,所以缩放最终得的结果是word里一页高度需要图片高度1726
    154. int picHeight = 1726;
    155. int picHeightTemp = 0;
    156. // 计算每个小图的宽度和高度
    157. int chunkWidth = image.getWidth();
    158. int chunkHeight = picHeight;
    159. int rows = 1;
    160. // 图片竖着分几块
    161. if (image != null && image.getHeight() > 1726) {
    162. rows = (image.getHeight() / 1726) + 1;
    163. picHeightTemp = image.getHeight() % 1726;
    164. } else {
    165. picHeightTemp = image.getHeight();
    166. }
    167. // 图片被切割成几块
    168. int chunks = rows;
    169. //大图中的一部分
    170. int count = 0;
    171. BufferedImage imgs[] = new BufferedImage[chunks];
    172. for (int x = 0; x < rows; x++) {
    173. // if判断,最后一张如果固定写死高度,会导致图片有上一张图内容拼接
    174. // 所以最后一张切割的高度,需要取余判断图片按平均分配高度之后,最后一张是剩余了多高
    175. if ((x + 1) == rows) {
    176. //设置小图的大小和类型
    177. imgs[count] = new BufferedImage(chunkWidth, picHeightTemp, image.getType());
    178. //写入图像内容
    179. // drawImage的参数,自己点进去源码里看官方注释
    180. Graphics2D gr = imgs[count++].createGraphics();
    181. gr.drawImage(image, 0, 0, chunkWidth, picHeightTemp, 0, chunkHeight * x, chunkWidth, image.getHeight(), null);
    182. gr.dispose();
    183. } else {
    184. imgs[count] = new BufferedImage(chunkWidth, chunkHeight, image.getType());
    185. Graphics2D gr = imgs[count++].createGraphics();
    186. gr.drawImage(image, 0, 0, chunkWidth, chunkHeight, 0, chunkHeight * x, chunkWidth, chunkHeight * x + chunkHeight, null);
    187. gr.dispose();
    188. }
    189. }
    190. // 图片被切割的base64
    191. List> base64List = new ArrayList<>();
    192. // 输出小图
    193. for (int i = 0; i < imgs.length; i++) {
    194. //ImageIO.write(imgs[i], "jpg", new File("C:\\img\\split\\img" + i + ".jpg"));
    195. // ImageIO.write(imgs[i], "png", new File("C:\\Users\\87151\\Desktop\\ceshi\\" + i + ".png"));
    196. Map map = new HashMap<>();
    197. // 图片base64
    198. map.put("url", imageToBase64(imgs[i]));
    199. // 图片文件名,不能重复,要不word生成的图片也重复
    200. map.put("name", 10000 + i);
    201. // 动态高度,防止最后一张拉伸
    202. if (i + 1 == imgs.length) {
    203. map.put("myheight", picHeightTemp / 2.475);
    204. } else {
    205. map.put("myheight", "697.4");
    206. }
    207. base64List.add(map);
    208. }
    209. LoggerUtil.info("结束切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
    210. // 删除临时文件缓存
    211. deleteTempFile(companyId);
    212. return base64List;
    213. } catch (IOException e) {
    214. e.printStackTrace();
    215. }
    216. return new ArrayList<>();
    217. }
    218. /**
    219. * 文件BufferedImage类型转BASE64
    220. *
    221. * @param bufferedImage
    222. * @return
    223. */
    224. public String imageToBase64(BufferedImage bufferedImage) {
    225. ByteArrayOutputStream baos = new ByteArrayOutputStream();//io流
    226. try {
    227. ImageIO.write(bufferedImage, "png", baos);//写入流中
    228. baos.flush();
    229. baos.close();
    230. } catch (IOException e) {
    231. e.printStackTrace();
    232. }
    233. byte[] bytes = baos.toByteArray();//转换成字节
    234. BASE64Encoder encoder = new BASE64Encoder();
    235. String png_base64 = encoder.encodeBuffer(bytes).trim();//转换成base64串
    236. png_base64 = png_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n
    237. return png_base64;
    238. }
    239. /**
    240. * 删除临时文件缓存
    241. */
    242. public void deleteTempFile(BigInteger companyId) {
    243. LoggerUtil.info("开始清除企业ID【" + companyId + "】的临时缓存文件成功");
    244. // 读取临时文件
    245. File file1 = new File(staticInstance.inputFileName + companyId + ".html");
    246. File file2 = new File(staticInstance.inputFileName + companyId + ".png");
    247. // 删除文件
    248. if (file1 != null) {
    249. file1.delete();
    250. }
    251. if (file2 != null) {
    252. file2.delete();
    253. }
    254. LoggerUtil.info("结束清除企业ID【" + companyId + "】的临时缓存文件成功");
    255. }
    256. }

    需要调用的类

    1. // 发展历程数据写进FTL,再把FTL转成HTML,再把HTML转成IMG,把IMG转成BASE64返回来
    2. FtlToHtmlToImg ftlToHtmlToImg = new FtlToHtmlToImg();
    3. List> devolopBase64List = ftlToHtmlToImg.createImgBase64(devolopEsResult, companyId, companyName);

            注意我们这边的参数,如果扒用的话需要酌情修改,我们参数是1.数据参数 2.企业id 3.企业名,企业ID和企业名字单纯就是为了传过去打印Log4j日志的,工具类里面的LoggerUtil是我们自己封装的Log4j的类,自己删了就行。

            工具类里面的方法大多数都是直接网上找的,不过找的帖子不是一个,太多没法发转发链接了,部分我们自己业务后补的是我自己写的。

    spring.freemarker.template-loader-path=classpath:/templates/ 

     这句配置对应工具类里的

    1. @Autowired
    2. FreeMarkerConfigurer freeMarkerConfigurer;

            必加,因为如果单纯的用 new ClassPathResource 的话打包成jar包运行会获取不到resource的资源

            这里可以参考我另一个文章 https://blog.csdn.net/qq_37241221/article/details/126048613

     

            导出word的话,里面图片太大超过了word一页内容,word可能要么压缩图片高度,要么就直接后面内容不显示了,我这里自己写了个切割方案,会根据图片的高度自己判断该不该切割,然后导入到word,1726那个数字是我根据word一页多高自己算的,亲测好用

    最后附上切割效果图 

  • 相关阅读:
    扩散模型实战(十):Stable Diffusion文本条件生成图像大模型
    bitbucket.org 用法
    视频汇聚/视频云存储/视频监控管理平台EasyCVR部署后无法正常启用是什么问题?该如何解决?
    【鸟哥杂谈】Linux环境下解决端口占用问题 Error: listen EADDRINUSE: address already in use :::8266
    深入分析:浏览器中输入一个URL会发生什么事情呢?
    vite创建vue3项目页面引用public下js文件失败解决
    ARMday01(计算机理论、ARM理论)
    前端科举八股文-REACT篇
    FastAPI 学习之路(七)字符串的校验
    Java多并发(四)| 锁(Lock接口 & AQS & ReentrantLock)
  • 原文地址:https://blog.csdn.net/qq_37241221/article/details/126103193