后台生成EChart报表图片并插入到Word文件中
前期准备
PhantomJS
https://phantomjs.org/download.html
官方介绍:
PhantomJS是一个基于 WebKit 的服务器端JavaScript API。它全面支持web而不需浏览器支持,支持各种Web标准:DOM处理,CSS选择器, JSON,Canvas,和SVG。
PhantomJS常用于页面自动化,网络监测,网页截屏,以及无界面测试等。
通常我们使用PhantomJS作为爬虫工具。传统的爬虫只能单纯地爬取html的代码,对于js渲染的页面,就无法爬取,如ECharts统计图。而PhantomJS正可以解决此类问题。
echarts-convert
https://gitee.com/saintlee/echartsconvert
一个配合phantomjs,在服务端生成EChart图片的工具包。
ECharts-2.2.7.jar
https://github.com/abel533/ECharts
一个供Java开发使用的ECharts的开发包,主要目的是方便在Java中构造ECharts中可能用到的全部数据结构,如完整的结构Option。
注:我的自用版本资源在这里
链接: https://pan.baidu.com/s/1Y4hQZXtKglWq7LnISQtiUw 密码: 8alt
生成EChart图片
新建一个测试类 EChartWordDemo,后续的方法都加在这个类中
/** * 后台生成EChart图片并插入Word测试类 * * @Author FanZhen * @Date 2021/3/12 */ public class EChartWordDemo { // ============== 这里要改成自己电脑对应的文件位置,服务器部署时可以通过环境变量等方式来动态改变它们的值 ================= /** * echart-convert包的路径 */ private String eChartJSPath = "/Users/helios_fz/IdeaProjects/websocket/src/main/resources/echart/echarts-convert/echarts-convert1.js"; /** * echart临时文件存储路径 */ private String eChartTempPath = "/Users/helios_fz/Desktop/a/b/"; /** * phantomjs命令路径 */ private String phantomjsPath = "/Users/helios_fz/IdeaProjects/rbac/rbac-admin/src/main/resources/phantomjs/phantomjs-2.1.1-macosx/bin/phantomjs"; }
在pom文件中引入 ECharts.jar 的依赖。因为这个包在中心库没有,阿里云的资源我试了几次又没拉下来,所以我就配置静态引入了(这里也是要根据自己放置jar包的位置来调整配置):
<!-- echart依赖 --> <dependency> <groupId>com.github.abel533</groupId> <artifactId>echarts</artifactId> <version>2.2.7</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/echart/ECharts-2.2.7.jar</systemPath> </dependency>
拼接EChart初始化json的方法:
/** * 生成EChart初始化json * * @param title 图片标题 * @param xAxis x轴 * @param line1 柱状图1 * @param line2 柱状图2 * @return json字符串 */ private String getEChartOption(String title, String xAxis, String line1, String line2) { return "{ " + " color: ["#f8732c", "#0094c8"], " + " title: { " + // 名字+流量 " text: " + """ + title + """ + ", " + " }, " + " tooltip: { " + " trigger: "axis", " + " }, " + " legend: { " + " data: ["line1", "line2"], " + " }, " + " toolbox: { " + " show: true, " + " feature: { " + " saveAsImage: { show: true }, " + " }, " + " }, " + " calculable: true, " + " xAxis: { " + " type: "category", " + " data: " + // X轴数据 xAxis + ", " + " axisLabel: { interval: 0 }, " + " }, " + " yAxis: [ " + " { " + " type: "value", " + " axisLabel: { " + " formatter: function (value) { " + " return value.toFixed(1) + "MB"; " + " }, " + " }, " + " }, " + " ], " + " series: [ " + " { " + " name: "line1", " + " type: "bar", " + " symbol: "circle", " + " barMaxWidth: 40, " + " data: " + // line1数据 line1 + ", " + " itemStyle: { " + " //上方显示数值 " + " normal: { " + " label: { " + " show: true, //开启显示 " + " position: "top", //在上方显示 " + " textStyle: { " + " //数值样式 " + " color: "black", " + " // fontSize: 14 " + " }, " + " }, " + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ " + " { offset: 0, color: "#f8732c" }, " + " { offset: 1, color: "#FFCEBF" }, " + " { offset: 1, color: "#FFCEBF" }, " + " ]), " + " }, " + " emphasis: { " + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ " + " { offset: 1, color: "#f8732c" }, " + " { offset: 0, color: "#FFCEBF" }, " + " { offset: 0, color: "#FFCEBF" }, " + " ]), " + " }, " + " }, " + " }, " + " { " + " name: "line2", " + " type: "bar", " + " symbol: "circle", " + " barMaxWidth: 40, " + " data: " + // line2数据 line2 + ", " + " itemStyle: { " + " //上方显示数值 " + " normal: { " + " label: { " + " show: true, //开启显示 " + " position: "top", //在上方显示 " + " textStyle: { " + " //数值样式 " + " color: "black", " + " // fontSize: 14 " + " }, " + " }, " + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ " + " { offset: 0, color: "#0094C8" }, " + " { offset: 1, color: "#CEF2FF" }, " + " { offset: 1, color: "#CEF2FF" }, " + " ]), " + " }, " + " emphasis: { " + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ " + " { offset: 1, color: "#0094C8" }, " + " { offset: 0, color: "#CEF2FF" }, " + " { offset: 0, color: "#CEF2FF" }, " + " ]), " + " }, " + " }, " + " }, " + " ], " + " }"; }
上边的代码折叠起来的原因有两点:
- 这个json我是写死的,因为我这边的需求只是生成一个柱状图,其他类型也可以生成,只要把前端的json复制过来就行了
- 这里也可以写一个动态的拼接方法,EChart.jar那个包就是做这部分工作的,可是我懒得写 = =
生成EChart图片的代码:
/** * 生成EChart图 * * @param options EChart初始化json * @param tmpPath 临时文件存放处 * @param echartJsPath 第三方工具路径 * @return */ private String generateEChart(String options, String tmpPath, String echartJsPath) { // 生成Echart的初始化json文件 String dataPath = writeFile(options, tmpPath); // 生成随机文件名 String fileName = UUID.randomUUID().toString().substring(0, 8) + ".png"; String path = tmpPath + fileName; try { // 文件路径(路径+文件名) File file = new File(path); // 文件不存在则创建文件,先创建目录 if (!file.exists()) { File dir = new File(file.getParent()); dir.mkdirs(); file.createNewFile(); } // 这里只能写绝对路径,因为要执行系统命令行 String cmd = phantomjsPath + " " + echartJsPath + " -infile " + dataPath + " -outfile " + path; Process process = Runtime.getRuntime().exec(cmd); BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream())); String line = ""; while ((line = input.readLine()) != null) { System.out.println(line); } input.close(); // 删除生成的临时json文件 File jsonFile = new File(dataPath); jsonFile.delete(); return path; } catch (IOException e) { e.printStackTrace(); return path; } } /** * 保存EChart临时json * * @param options echart初始化js * @param tmpPath 临时文件保存路径 * @return 文件完整路径 */ private String writeFile(String options, String tmpPath) { String dataPath = tmpPath + UUID.randomUUID().toString().substring(0, 8) + ".json"; try { /* 写入Txt文件 */ // 相对路径,如果没有则要建立一个新的output.txt文件 File writeName = new File(dataPath); // 文件不存在则创建文件,先创建目录 if (!writeName.exists()) { File dir = new File(writeName.getParent()); dir.mkdirs(); // 创建新文件 writeName.createNewFile(); } BufferedWriter out = new BufferedWriter(new FileWriter(writeName)); out.write(options); // 把缓存区内容压入文件 out.flush(); // 最后记得关闭文件 out.close(); } catch (IOException e) { e.printStackTrace(); } return dataPath; }
生成Word文件并插入EChart图片
在pom文件中引入生成word需要的依赖:
<!-- word依赖 --> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext</artifactId> <version>2.1.5</version> </dependency> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext-rtf</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency>
生成word并插入图片:
/** * 生成word文件 * * @param title 标题 * @param content 正文 * @param imagePaths 待插入的图片地址列表 * @param docPath word文件保存地址 * @throws IOException * @throws com.lowagie.text.DocumentException */ private void writeWord(String title, String content, List<String> imagePaths, String docPath) throws IOException, com.lowagie.text.DocumentException { // 创建文件 File file = new File(docPath); Document document = new Document(PageSize.A4); RtfWriter2.getInstance(document, new FileOutputStream(file)); document.open(); // 初始化字体 BaseFont bfChinese = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED); // 标题字体 Font titleFont = new Font(bfChinese, 30, Font.BOLD); // 小标题字体 // Font littleTitleFont = new Font(bfChinese, 20, Font.BOLD); // 正文字体 Font contextFont = new Font(bfChinese, 15, Font.NORMAL); // 设置大标题 Paragraph titlePar = new Paragraph(title); titlePar.setFont(titleFont); titlePar.setAlignment(Element.ALIGN_CENTER); document.add(titlePar); // 正文 Paragraph context = new Paragraph(content); context.setAlignment(Element.HEADER); context.setFont(contextFont); document.add(context); // 图片 imagePaths.forEach(imagePath -> { Image img = null; try { img = Image.getInstance(imagePath); img.setAbsolutePosition(0, 0); img.scalePercent(50f); // 设置图片显示位置 img.setAlignment(Image.LEFT); document.add(img); } catch (IOException | DocumentException e) { e.printStackTrace(); } }); document.close(); }
调用方法生成图片并插入word中
/** * 导出docx文件 * * @param params 参数列表 * @param response response 返回的文件流 */ public void getWordWithEchart(Map<String, Object> params, HttpServletResponse response) throws IOException { // 这一部分代码和业务逻辑强相关了,需要自己根据现实情况去实现 List<String> imagePaths = new ArrayList<>(); 一个业务数组.forEach(id -> { // 这里的X轴其实有一个问题,就是用"MM-dd"形式传入其中的话,生成工具会默认这是一个减法运算。 // 这个时候需要遍历一下X轴数据,在每一个数据外面加上一对转义的双引号,like this:""" String eChartOption = getEChartOption( 标题, x轴, 柱状图1, 柱状图2); // 生成图片 imagePaths.add(generateEChart(eChartOption, eChartTempPath, eChartJSPath)); }); // 根据EChart图片生成Word文档 String title = "Word文档Title "; // doc文件命名 String docName = UUID.randomUUID().toString().substring(0, 8) + ".docx"; String docPath = eChartTempPath + docName; StringBuffer content = new StringBuffer(); ...加一大堆生成word内容的逻辑 content.append(" "); try { // 生成word文件 writeWord(title, content.toString(), imagePaths, docPath); } catch (IOException | DocumentException e) { e.printStackTrace(); } //删除临时文件 imagePaths.forEach(imagePath -> { File file = new File(imagePath); file.delete(); }); // 返回文件流 File file = new File(docPath); // 八进制输出流 response.setContentType("application/octet-stream"); response.setHeader("content-type", "application/octet-stream"); // 设置导出Word的名称 response.setHeader("Content-disposition", "attachment;filename=" + "Word文档.docx"); // 刷新缓冲 response.flushBuffer(); // 将doc文件流写入到返回 // 根据路径获取要下载的文件输入流 InputStream inputStream = new FileInputStream(file); OutputStream out = response.getOutputStream(); //创建数据缓冲区 byte[] b = new byte[1024]; int length; while ((length = inputStream.read(b)) > 0) { //把文件流写到缓冲区里 out.write(b, 0, length); } out.flush(); out.close(); inputStream.close(); // 传输过后把doc文件删除 file.delete(); }
注:这个demo在运行之后会把生成的文件都删掉,如果想看看生成的临时文件长成啥样,把代码中的文件删除操作都注释掉就可以了。