一、需求
没有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
是一个基于webkit
的JavaScript 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.js
、echarts.min.js
、echarts-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
这个工具比较好,不失真,而且不依赖软件,但是要使用破解版,不然会有水印
- 添加依赖
<dependencies>
...
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>15.8.0</version>
</dependency>
</dependencies>
这个依赖可能下载不下来,可以自己去网上找一个15.8.0
版本的jar
包,传到私服,或者放在工程的lib
目录下。
注意,这里版本最好用我这一个,最新版本的aspose-words
不好破解
- 在
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>
- 编写工具类
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;
}
}
- 编写测试代码
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);
}
- 结果
四、常见问题
至此,这个需求简单的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 死活都是报错
- 先检查下自己的服务器版本,如果是完整的 linux 版本,下载官网对应的 linux 版本,基本不会有问题。
- 我这里因为是
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 的方法(而这个方法,有很多成熟的案例)