Spring Boot - 后台生成EChart報表圖片並插入到Word文件中


后台生成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 "{\n" +
                "        color: [\"#f8732c\", \"#0094c8\"],\n" +
                "        title: {\n" +
                // 名字+流量
                "          text: " + "\"" + title + "\"" + ",\n" +
                "        },\n" +
                "        tooltip: {\n" +
                "          trigger: \"axis\",\n" +
                "        },\n" +
                "        legend: {\n" +
                "          data: [\"line1\", \"line2\"],\n" +
                "        },\n" +
                "        toolbox: {\n" +
                "          show: true,\n" +
                "          feature: {\n" +
                "            saveAsImage: { show: true },\n" +
                "          },\n" +
                "        },\n" +
                "        calculable: true,\n" +
                "        xAxis: {\n" +
                "          type: \"category\",\n" +
                "          data: " +
                // X軸數據
                xAxis +
                ",\n" +
                "          axisLabel: { interval: 0 },\n" +
                "        },\n" +
                "        yAxis: [\n" +
                "          {\n" +
                "            type: \"value\",\n" +
                "            axisLabel: {\n" +
                "              formatter: function (value) {\n" +
                "                return value.toFixed(1) + \"MB\";\n" +
                "              },\n" +
                "            },\n" +
                "          },\n" +
                "        ],\n" +
                "        series: [\n" +
                "          {\n" +
                "            name: \"line1\",\n" +
                "            type: \"bar\",\n" +
                "            symbol: \"circle\",\n" +
                "            barMaxWidth: 40,\n" +
                "            data: " +
                // line1數據
                line1 +
                ",\n" +
                "            itemStyle: {\n" +
                "              //上方顯示數值\n" +
                "              normal: {\n" +
                "                label: {\n" +
                "                  show: true, //開啟顯示\n" +
                "                  position: \"top\", //在上方顯示\n" +
                "                  textStyle: {\n" +
                "                    //數值樣式\n" +
                "                    color: \"black\",\n" +
                "                    // fontSize: 14\n" +
                "                  },\n" +
                "                },\n" +
                "                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
                "                  { offset: 0, color: \"#f8732c\" },\n" +
                "                  { offset: 1, color: \"#FFCEBF\" },\n" +
                "                  { offset: 1, color: \"#FFCEBF\" },\n" +
                "                ]),\n" +
                "              },\n" +
                "              emphasis: {\n" +
                "                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
                "                  { offset: 1, color: \"#f8732c\" },\n" +
                "                  { offset: 0, color: \"#FFCEBF\" },\n" +
                "                  { offset: 0, color: \"#FFCEBF\" },\n" +
                "                ]),\n" +
                "              },\n" +
                "            },\n" +
                "          },\n" +
                "          {\n" +
                "            name: \"line2\",\n" +
                "            type: \"bar\",\n" +
                "            symbol: \"circle\",\n" +
                "            barMaxWidth: 40,\n" +
                "            data: " +
                // line2數據
                line2 +
                ",\n" +
                "            itemStyle: {\n" +
                "              //上方顯示數值\n" +
                "              normal: {\n" +
                "                label: {\n" +
                "                  show: true, //開啟顯示\n" +
                "                  position: \"top\", //在上方顯示\n" +
                "                  textStyle: {\n" +
                "                    //數值樣式\n" +
                "                    color: \"black\",\n" +
                "                    // fontSize: 14\n" +
                "                  },\n" +
                "                },\n" +
                "                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
                "                  { offset: 0, color: \"#0094C8\" },\n" +
                "                  { offset: 1, color: \"#CEF2FF\" },\n" +
                "                  { offset: 1, color: \"#CEF2FF\" },\n" +
                "                ]),\n" +
                "              },\n" +
                "              emphasis: {\n" +
                "                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
                "                  { offset: 1, color: \"#0094C8\" },\n" +
                "                  { offset: 0, color: \"#CEF2FF\" },\n" +
                "                  { offset: 0, color: \"#CEF2FF\" },\n" +
                "                ]),\n" +
                "              },\n" +
                "            },\n" +
                "          },\n" +
                "        ],\n" +
                "      }";
    }
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\n";
        // doc文件命名
        String docName = UUID.randomUUID().toString().substring(0, 8) + ".docx";
        String docPath = eChartTempPath + docName;

        StringBuffer content = new StringBuffer();
        ...加一大堆生成word內容的邏輯
        content.append("\n");

        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在運行之后會把生成的文件都刪掉,如果想看看生成的臨時文件長成啥樣,把代碼中的文件刪除操作都注釋掉就可以了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM