Spring Boot

后台生成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" },
" +
                "                ]),
" +
                "              },
" +
                "            },
" +
                "          },
" +
                "        ],
" +
                "      }";
    }
View Code

上边的代码折叠起来的原因有两点:

  • 这个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在运行之后会把生成的文件都删掉,如果想看看生成的临时文件长成啥样,把代码中的文件删除操作都注释掉就可以了。

原文地址:https://www.cnblogs.com/helios-fz/p/14524566.html