纯java后台生成带ECharts的PDF文档(phantomjs)

一、需求

没有HTML预览页面,要求前台提交数据后,在服务端生成一份带ECharts图表的PDF文件

二、分析

存在以下几个难点:
1、没有浏览器,怎么渲染生成ECharts图并且放到PDF中
2、后台生成PDF的方案比较少,怎么保证样式,同时要有可维护性

解决方案:
1、直接生成PDF局限性较大,可以先生成word,再转PDF
2、寻找一个工具,可以把ECharts转成图片,插入到word中

三、实现

这里我以生成一个简历PDF为例

1、word的生成

采用POI-TL转为模板

官网地址:http://deepoove.com/poi-tl/1.9.x/
在这里插入图片描述
这个工具直接采用word模板引擎,直接创建.docx模板,定义好模板,编写变量,然后直接可以生成

优点如下:

  • 直接采用.docx模板,所见即所得,可维护性高
  • 跨平台,使用简单
  • 官方文档丰富,支持很多种替换图片的方法,比较方便

缺点如下:

  • 使用word模板,样式无法100%还原UI设计图
  • 需要花时间拼接模板,包括背景图,字体的间距,需要耐心去微调
  • 因为字数的占位原因,实际的效果可能会和自己的模板有所出入
  • 运行在服务器上,需要安装中文字体,否则会乱码
  • 采用引用标签的方式替换图片,需要提前设计好原图的尺寸,否则会变形
  • 采用引用标签的方式替换图片,必须使用 office 打开,否则没有可选文字选项

我这里创建了一个word模板,如下:
在这里插入图片描述
图片我使用的官方API的引用标签,具体用法参照官方文档

模板生成word示例代码
public static void main(String[] args) throws Exception{
    //组装学生基础数据
    Student student = new Student();
    student.setName("张三");
    student.setSex("男");
    student.setBirth("1995/2/3");
    student.setNation("汉族");
    student.setPolitical("群众");
    student.setMarry("否");
    student.setPhone("13812345678");
    student.setEmail("test@email.com");
    student.setJob("程序员");
    //图片支持多种方式,这里采用InputStream流,具体查看官方文档
    InputStream is = Test.class.getClassLoader().getResourceAsStream("static/myAvatar.png");
    student.setAvatar(Pictures.ofStream(is, PictureType.PNG).create());
    //compile参数类型有很多种,可以自己查阅API选择合适自己的
    XWPFTemplate template = XWPFTemplate.compile(Test.class.getClassLoader().getResourceAsStream("static/template.docx"));
    template.render(student);
    template.writeAndClose(new FileOutputStream("D:\\test\\output.docx"));
}

效果图
在这里插入图片描述
可以看到变量都被替换了,头像也被我们自己的图片替换了,但是样式有错乱,所以在编写模板的时候,需要考虑到字数问题

2、ECharts图片的生成

采用Phantomjs(无界面的浏览器)+ ehcarts-convert.js

PhantomJS是一个基于webkitJavaScript API。它使用QtWebKit作为它核心浏览器的功能,使用webkit来编译解释执行JavaScript代码。任何你可以在基于webkit浏览器做的事情,它都能做到。它不仅是个隐形的浏览器,提供了诸如CSS选择器、支持 Web 标准、DOM操作、JSON、HTML5、Canvas、SVG等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。PhantomJS的用处可谓非常广泛,诸如网络监测、网页截屏、无需浏览器的 Web 测试、页面访问自动化等

第一步:下载PhantomJS

PhantomJS官方地址:http://phantomjs.org/
PhantomJS官方API:http://phantomjs.org/api/
PhantomJS官方示例:http://phantomjs.org/examples/
PhantomJS GitHub:https://github.com/ariya/phantomjs/

去官网下载对应版本的压缩包,我这里是 windows 机器,就下载 windows 版本的做演示,解压到本地一个目录,我这里放在D盘

第二步:准备ehcarts-convert.js脚本

需要三个文件:jquery-3.2.1.min.jsecharts.min.jsecharts-convert.js
我这里有打包好的,可以直接下载:https://mp.csdn.net/mp_download/manage/download/UpDetailed

第三步:编写生成ECharts图片工具类
package com.test.util;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.*;

public class PhantomJsUtils {

    static Log log = LogFactory.getLog(PhantomJsUtils.class);

    //echarts-convert.js所在目录,实际使用可以写在.yml文件里,然后使用@value动态获取
    private static final String echartConverJsPath = "D:\\echartconvert\\echarts-convert.js";
    //phantomjs所在目录,到bin那一层,实际使用可以写在.yml文件里,然后使用@value动态获取
    private static final String phantomJsPath = "D:\\phantomjs-2.1.1-windows\\bin\\";

    /**
     * 生成echarts图片的方法
     * @param options echarts的option
     * @param tmpDir 存放图片的临时目录
     * @param width 图片的宽,多少px
     * @param height 图片的高,多少px
     * @return
     */
    public static String genEChart(String options, String tmpDir, int width, int height) throws Exception {
        log.info("########## 开始生成EChart图片 #########");
        //生成初始化json文件
        String jsonPath = tmpDir + System.currentTimeMillis() + ".json";
        writeFile(options, jsonPath);
        //生成png文件
        String imgPath = tmpDir + System.currentTimeMillis() + ".png";
        createDirFile(imgPath);
        //拼接phantomjs命令行
        String cmd = phantomJsPath + "phantomjs " + echartConverJsPath +
                " -infile " + jsonPath +
                " -outfile " + imgPath +
                " -width " + width +
                " -height " + height;
        log.info("####### 开始执行command命令: " + cmd);
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(cmd);
            readProcess(process);
            // 关闭进程资源对象
            process.waitFor();
        } finally {
            if (null != process) {
                process.destroy();
            }
        }
        // 删除生成的临时json文件
        File jsonFile = new File(jsonPath);
        jsonFile.delete();
        return imgPath;
    }

    public static void readProcess(Process process) throws Exception{
        InputStream fis = null;
        InputStreamReader isr = null;
        BufferedReader br = null;
        try {
            //取得命令结果的输出流
            fis=process.getInputStream();
            //用一个读输出流类去读
            isr=new InputStreamReader(fis);
            //用缓冲器读行
            br=new BufferedReader(isr);
            String line=null;
            //直到读完为止
            while((line=br.readLine())!=null) {
                System.out.println(line);
            }
        } finally {
            if (null != fis) {
                fis.close();
            }
            if (null != isr) {
                isr.close();
            }
            if (null != br) {
                br.close();

            }
        }
    }

    public static String writeFile(String options, String dataPath) throws IOException{
        log.info("########## 内容写入文件 #########");
        File writeName = createDirFile(dataPath);
        BufferedWriter out = null;
        try {
            out = new BufferedWriter(new FileWriter(writeName));
        } finally {
            if (null != out) {
                out.write(options);
                // 把缓存区内容压入文件
                out.flush();
                // 最后记得关闭文件
                out.close();
            }
        }
        return dataPath;
    }

    public static File createDirFile(String path) throws IOException {
        log.info("########## 创建文件 #########");
        // 文件路径(路径+文件名)
        File file = new File(path);
        // 文件不存在则创建文件,先创建目录
        if (!file.exists()) {
            File dir = new File(file.getParent());
            dir.mkdirs();
            file.createNewFile();
        }
        return file;
    }
}

编写一段测试代码测试

public static void main(String[] args) throws Exception{
    //组装学生基础数据
    Student student = new Student();
    student.setName("张三");
    student.setSex("男");
    student.setBirth("1995/2/3");
    student.setNation("汉族");
    student.setPolitical("群众");
    student.setMarry("否");
    student.setPhone("13812345678");
    student.setEmail("test@email.com");
    student.setJob("程序员");
    //图片支持多种方式,这里采用InputStream流,具体查看官方文档
    InputStream is = Test.class.getClassLoader().getResourceAsStream("static/myAvatar.png");
    student.setAvatar(Pictures.ofStream(is, PictureType.PNG).create());
    //准备生成ECharts图片,然后放到对象属性里
    String options = "{xAxis:{type:'category',data:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']},yAxis:{type:'value'},series:[{data:[150,230,224,218,135,147,260],type:'line'}]}";
    String imgFile = PhantomJsUtils.genEChart(options, "D:\\test\\", 800, 400);
    student.setEchartImg(Pictures.ofStream(new FileInputStream(imgFile), PictureType.PNG).create());
    //compile参数类型有很多种,可以自己查阅API选择合适自己的
    XWPFTemplate template = XWPFTemplate.compile(Test.class.getClassLoader().getResourceAsStream("static/template.docx"));
    template.render(student);
    template.writeAndClose(new FileOutputStream("D:\\test\\output.docx"));
}

查看最终生成的文档结果
在这里插入图片描述
可以看到word文档里面已经插入了ECharts图片

我这里的options是从官网复制的代码,实际开发可能没这么简单,需要自己写。要么让前端写好传给你,要么自己在后端写。
后端这里有一个工具类,可以很好的帮助你组装options

ECharts-java类库

gitee地址:https://gitee.com/free/ECharts?_from=gitee_search
以我这里的options为例

{
    xAxis: {
        type: 'category',
        data: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [
        {
            data: [150,230,224,218,135,147,260],
            type: 'line'
        }
    ]
}

添加依赖

<dependency>
    <groupId>com.github.abel533</groupId>
    <artifactId>ECharts</artifactId>
    <version>3.0.0.2</version>
</dependency>

编写生成options的方法

static String getLine() {
    GsonOption option = new GsonOption();

    CategoryAxis categoryAxis = new CategoryAxis();
    categoryAxis.data("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");
    option.xAxis(categoryAxis);

    ValueAxis valueAxis = new ValueAxis();
    option.yAxis(valueAxis);

    Line line = new Line();
    line.data(150, 230, 224, 218, 135, 147, 260);
    option.series(line);

    return option.toString();
}

写法不难,参考官网的示例,自己动手写几个就会了

但是这个API不更新了,新的一些属性可能不支持,所以实际应用如果有不支持的,可以自己写一个名字一样的类,继承它的类,然后扩展属性

3、word转pdf

使用aspose-words工具实现word转pdf

这个工具比较好,不失真,而且不依赖软件,但是要使用破解版,不然会有水印

  1. 添加依赖
<dependencies>
	...
	<dependency>
	    <groupId>com.aspose</groupId>
	    <artifactId>aspose-words</artifactId>
	    <version>15.8.0</version>
	</dependency>
</dependencies>

这个依赖可能下载不下来,可以自己去网上找一个15.8.0版本的jar包,传到私服,或者放在工程的lib目录下。
注意,这里版本最好用我这一个,最新版本的aspose-words不好破解

  1. reousrces目录新建一个文件License.xml,内容如下,用于破解:
<License>
    <Data>
        <Products>
            <Product>Aspose.Total for Java</Product>
        </Products>
        <EditionType>Enterprise</EditionType>
        <SubscriptionExpiry>20991231</SubscriptionExpiry>
        <LicenseExpiry>20991231</LicenseExpiry>
        <SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
    </Data>
    <Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>
</License>
  1. 编写工具类
package com.test.util;

import com.aspose.words.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.Logger;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

/**
 * @Description:
 * @Author: lcwei2
 */
public class WordUtil {

    static Log log = LogFactory.getLog(WordUtil.class);

    public static void word2Pdf(String wordPath, String pdfPath, String fontsFolder) throws Exception{
        if (!getLicense()) {
            log.info("######## License获取失败,退出 #######");
            return;
        }
        log.info("word转pdf开始, wordpath: " + wordPath + ", pdfpath: " + pdfPath);
        File pdfFile = new File(pdfPath);
        try (FileOutputStream os = new FileOutputStream(pdfFile)){
            Document doc = new Document(wordPath);
            doc.save(os, SaveFormat.PDF);
        }
    }

    private static boolean getLicense() throws Exception{
        boolean result = false;
        InputStream is = null;
        try {
            is = WordUtil.class.getClassLoader().getResourceAsStream("License.xml");
            License license = new License();
            license.setLicense(is);
            result = true;
        }  finally {
            if (is != null) {
                is.close();
            }
        }
        return result;
    }
}
  1. 编写测试代码
public static void main(String[] args) throws Exception{
    //组装学生基础数据
    Student student = new Student();
    student.setName("张三");
    student.setSex("男");
    student.setBirth("1995/2/3");
    student.setNation("汉族");
    student.setPolitical("群众");
    student.setMarry("否");
    student.setPhone("13812345678");
    student.setEmail("test@email.com");
    student.setJob("程序员");
    //图片支持多种方式,这里采用InputStream流,具体查看官方文档
    InputStream is = Test.class.getClassLoader().getResourceAsStream("static/myAvatar.png");
    student.setAvatar(Pictures.ofStream(is, PictureType.PNG).create());
    //准备生成ECharts图片,然后放到对象属性里
//    String options = "{xAxis:{type:'category',data:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']},yAxis:{type:'value'},series:[{data:[150,230,224,218,135,147,260],type:'line'}]}";
    String options = getLine();
    String imgFile = PhantomJsUtils.genEChart(options, "D:\\test\\", 800, 400);
    student.setEchartImg(Pictures.ofStream(new FileInputStream(imgFile), PictureType.PNG).create());
    //compile参数类型有很多种,可以自己查阅API选择合适自己的
    XWPFTemplate template = XWPFTemplate.compile(Test.class.getClassLoader().getResourceAsStream("static/template.docx"));
    template.render(student);
    String wordPath = "D:\\test\\output.docx";
    String pdfPath = "D:\\test\\output.pdf";
    template.writeAndClose(new FileOutputStream(wordPath));
    WordUtil.word2Pdf(wordPath, pdfPath);
}
  1. 结果
    在这里插入图片描述

四、常见问题

至此,这个需求简单的Demo已经完成了,实际在开发过程中,还会有很多问题,需要自己一步一步调试,这里总结几个自己遇到的几个问题,希望可以帮到需要的人

1. 生成 ECharts 图片失败,全黑等

拷贝PhantomJsUtils类中的cmd命令,手动去控制台执行一下试试,看看报不报错,windows环境下可能要改权限之类的

2. 生成 options 字符串,不要使用 new Gson(options).toJson() 方法

网上有的文章在生成options字符串的时候,使用的这个方法,这个方法有一个问题,就是识别不了脚本里的func自定义函数。直接使用自带的options.toString就行了

3. Dockerfile 部署问题

如果是 docker 部署,需要编写dockerfile,可以参考这篇文章:https://blog.csdn.net/qq_34898847/article/details/120045897

4. 部署到服务器上,PhantomJs 死活都是报错

  1. 先检查下自己的服务器版本,如果是完整的 linux 版本,下载官网对应的 linux 版本,基本不会有问题。
  2. 我这里因为是alpine版本,官网没找到对应版本的PhantomJs,最后在这篇文章找到了:https://blog.csdn.net/teyietry/article/details/113648740。 这个问题困扰了我一夜

5. 中文乱码问题

需要提前下载对应字体的 ttf 文件,到服务器上安装。然后在word2Pdf方法里,需要加一行代码FontSettings.setFontsFolder("D:\\xxx", true);,指定字体目录

五、总结

最后。遇到这种需求,建议还是从产品层面优化。改成从前端渲染,因为这种方案,技术成本太大

因为…

  • 你需要编写word模板,这对后端来说简直是噩梦,而且无法100%还原UI设计
  • 你需要oss对象存储,放pdf文件
  • 你需要后端先生成word,再转pdf
  • 你需要去服务器安装PhantomJs插件
  • 你需要准备echarts-convert.js脚本
  • 你需要去服务器安装字体
  • 如果你是docker部署,你需要修改dockerfile文件

  • 纯后端做这个需要考虑的因素太多了,而且在调试过程中会遇到很多奇怪的问题,参考资料也很少

如果需求改成前端提供一个 HTML 预览页,然后再提供一个按钮,点击生成 PDF 文档,你只需要

  • 提供接口,给前端拼接options
  • 前端寻找一个 HTML 转 PDF 的方法(而这个方法,有很多成熟的案例)
  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
图表图片的步骤是什么? 回答: 生成echarts图表图片的步骤如下: 1. 首先,需要安装部署Phantomjs,可以将下载好的Phantomjs放在任意位置。 2. 找到echarts-convert.js文件,并将其放在与Phantomjs相同的目录下。 3. 在命令行中执行命令,例如在Linux系统中可以执行nohup phantomjs echarts-convert.js -s -p 50130 > echarts.log 2>&1 &,而在Windows系统中可以执行C:\Users\Administrator\Desktop\phantomjs-2.1.1-windows\bin>phantomjs C:\Users\Administrator\Desktop\echartsconvert\echarts-convert.js -s -p 9090。\[2\]\[3\] 4. 调用接口生成图片,可以通过发送请求来调用接口生成echarts图表的图片。 #### 引用[.reference_title] - *1* *2* [java后台生成echarts图表图片](https://blog.csdn.net/zixuanyankai/article/details/130702369)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [java 实现后台生成echarts 图片](https://blog.csdn.net/weixin_43831289/article/details/119645323)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值