接着我上一篇文章去写,Java EasyWord导出word文档。上一篇文章中图片是固定的(图片路径固定),实际中可能需要根据数据做一些统计图出来,并和文档一起导出(只要我们知道生成的图片路径就可以动态的导出了)。就是相当于,把前端Echarts生成的图片直接在后端生成。
准备工作
- 下载并解压PhantomJs,这是官网地址 https://phantomjs.org/download.html,有两个包,Windows和Linux。我展示使用的版本是phantomjs-2.1.1。
- jquery-xxx.min.js,任意版本都可以,我使用的是jquery-3.6.0.min.js
- echarts.min.js。下载地址:https://echarts.baidu.com/download.html
- echarts-convert.js
(function () { var system = require('system'); var fs = require('fs'); var config = { // define the location of js files JQUERY: 'jquery-3.6.0.min.js', ECHARTS: 'echarts.min.js', // default container width and height DEFAULT_WIDTH: '600', DEFAULT_HEIGHT: '700' }, parseParams, render, pick, usage; // 提示:命令格式 usage = function () { console.log("\n" + "Usage: phantomjs echarts-convert.js -infile URL -width width -height height" + "\n"); }; // 选择是否存在设置长宽,否使用默认长宽 pick = function () { var args = arguments, i, arg, length = args.length; for (i = 0; i < length; i += 1) { arg = args[i]; if (arg !== undefined && arg !== null && arg !== 'null' && arg != '0') { return arg; } } }; // 处理参数 parseParams = function () { var map = {}, i, key; if (system.args.length < 2) { usage(); phantom.exit(); } for (i = 0; i < system.args.length; i += 1) { if (system.args[i].charAt(0) === '-') { key = system.args[i].substr(1, i.length); if (key === 'infile') { // get string from file // force translate the key from infile to options. key = 'options'; try { map[key] = fs.read(system.args[i + 1]).replace(/^\s+/, ''); } catch (e) { console.log('Error: cannot find file, ' + system.args[i + 1]); phantom.exit(); } } else { map[key] = system.args[i + 1].replace(/^\s+/, ''); } } } return map; }; render = function (params) { var page = require('webpage').create(), createChart; page.onConsoleMessage = function (msg) { console.log(msg); }; page.onAlert = function (msg) { console.log(msg); }; createChart = function (inputOption, width, height) { var counter = 0; function decrementImgCounter() { counter -= 1; if (counter < 1) { console.log("The images load error"); } } function loadScript(varStr, codeStr) { var script = $('<script>').attr('type', 'text/javascript'); script.html('var ' + varStr + ' = ' + codeStr); document.getElementsByTagName("head")[0].appendChild(script[0]); if (window[varStr] !== undefined) { console.log('Echarts.' + varStr + ' has been parsed'); } } function loadImages() { var images = $('image'), i, img; if (images.length > 0) { counter = images.length; for (i = 0; i < images.length; i += 1) { img = new Image(); img.onload = img.onerror = decrementImgCounter; img.src = images[i].getAttribute('href'); } } else { console.log('The images have been loaded'); } } // load opitons if (inputOption != 'undefined') { // parse the options loadScript('options', inputOption); // disable the animation options.animation = false; } // we render the image, so we need set background to white. $(document.body).css('backgroundColor', 'white'); var container = $("<div>").appendTo(document.body); container.attr('id', 'container'); container.css({ width: width, height: height }); // render the chart var myChart = echarts.init(container[0]); myChart.setOption(options); // load images loadImages(); return myChart.getDataURL(); }; // parse the params page.open("about:blank", function (status) { // inject the dependency js page.injectJs(config.JQUERY); page.injectJs(config.ECHARTS); var width = pick(params.width, config.DEFAULT_WIDTH); var height = pick(params.height, config.DEFAULT_HEIGHT); // create the chart var base64 = page.evaluate(createChart, params.options, width, height); console.log(base64); // define the clip-rectangle console.log('\nbase64 complete'); // exit phantom.exit(); }); }; // get the args var params = parseParams(); // validate the params if (params.options === undefined || params.options.length === 0) { console.log("ERROR: No options or infile found."); usage(); phantom.exit(); } // render the image render(params); }());
后端测试代码
这里我以生成基础雷达图为例,图表数据找的是Echarts官网例子。
import lombok.Data;
import java.io.Serializable;
/**
* @author qing
*/
@Data
public class TestPhantomjs implements Serializable {
private static final long serialVersionUID = 1L;
/**
* echarts 数据生成文件路径
*/
private String optionPath;
/**
* 生成图片所在的路径
*/
private String picturePath;
}
import lombok.Data;
import net.sf.json.JSONObject;
import java.util.List;
/**
* 从官网中弄到的示例
* option = {
* title: {
* text: 'Basic Radar Chart'
* },
* legend: {
* data: ['Allocated Budget', 'Actual Spending']
* },
* radar: {
* // shape: 'circle',
* indicator: [
* { name: 'Sales', max: 6500 },
* { name: 'Administration', max: 16000 },
* { name: 'Information Technology', max: 30000 },
* { name: 'Customer Support', max: 38000 },
* { name: 'Development', max: 52000 },
* { name: 'Marketing', max: 25000 }
* ]
* },
* series: [
* {
* name: 'Budget vs spending',
* type: 'radar',
* data: [
* {
* value: [4200, 3000, 20000, 35000, 50000, 18000],
* name: 'Allocated Budget'
* },
* {
* value: [5000, 14000, 28000, 26000, 42000, 21000],
* name: 'Actual Spending'
* }
* ]
* }
* ]
* }
*
* 雷达图属性
* @author qing
*/
@Data
public class TestRadar {
/**
* 雷达图的标题(描述)
*/
private String titleText;
/**
* 按钮
*/
private List<String> legendData;
/**
* 雷达图形状,默认是有棱角 polygon
*/
private String radarShape;
/**
* 雷达图指标
*/
private List<JSONObject> radarIndicator;
private String seriesName;
/**
* 基础数据
*/
private List<JSONObject> seriesData;
}
import com.sushengren.easyword.EasyWord;
import com.sushengren.easyword.annotation.WordProperty;
import com.sushengren.easyword.converters.PictureConverter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class TestWord {
/**
* 标题
*/
@WordProperty("title")
private String title;
/**
* 水果列表
*/
@WordProperty("水果列表")
private List<FruitTable> fruitTables;
/**
* 商品列表
*/
@WordProperty("商品列表")
private List<CommodityTable> commodityTables;
/**
* 图片
*/
@WordProperty(value = "logo", converter = PictureConverter.class)
private InputStream logo;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class FruitTable {
/**
* 名称
*/
@WordProperty("名称")
private String name;
/**
* 来源
*/
@WordProperty("来源")
private String source;
/**
* 单价
*/
@WordProperty("单价")
private Double price;
/**
* 描述
*/
@WordProperty("描述")
private String desc;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CommodityTable {
/**
* 商品类别
*/
@WordProperty("商品类别")
private String type;
/**
* 商品说明
*/
@WordProperty("商品说明")
private String desc;
/**
* 类别列表
*/
@WordProperty("类别列表")
private List<CategoryTable> categoryTable;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CategoryTable {
/**
* 名称
*/
@WordProperty("名称")
private String name;
/**
* 来源
*/
@WordProperty("来源")
private String source;
/**
* 单价
*/
@WordProperty("单价")
private Double price;
/**
* 描述
*/
@WordProperty("描述")
private String desc;
}
public static void main(String[] args) throws Exception {
// 这是表格的数据 start
List<FruitTable> fruitTables = new ArrayList<>();
FruitTable f1 = new FruitTable("苹果", "烟台", 5.0, "又红又大的苹果。");
fruitTables.add(f1);
FruitTable f2 = new FruitTable("芒果", "海南", 8.0, "好吃不贵的芒果。");
fruitTables.add(f2);
FruitTable f3 = new FruitTable("柑橘", "宜昌", 2.5, "好一个柑橘。");
fruitTables.add(f3);
List<CategoryTable> categoryTables1 = new ArrayList<>();
CategoryTable a1 = new CategoryTable("芭蕉", "火焰山", 6.5, "烈日炎炎,芭蕉冉冉。");
categoryTables1.add(a1);
CategoryTable a2 = new CategoryTable("樱桃", "泰安", 25.0, "红了樱桃,绿了芭蕉。");
categoryTables1.add(a2);
List<CommodityTable> commodityTables = new ArrayList<>();
CommodityTable c1 = new CommodityTable("水果", "这是一批水果", categoryTables1);
commodityTables.add(c1);
List<CategoryTable> categoryTables2 = new ArrayList<>();
CategoryTable a3 = new CategoryTable("空调", "小行星", 2800.0, "全靠空调续命。");
categoryTables2.add(a3);
CategoryTable a4 = new CategoryTable("热水器", "哈哈哈", 1525.0, "零冷水。");
categoryTables2.add(a4);
CommodityTable c2 = new CommodityTable("家电", "这是一堆家电", categoryTables2);
commodityTables.add(c2);
// 这是表格的数据 end
// 先组装雷达图数据
TestRadar testRadar = radarDataAssembly();
// 生成图片,并返回图片路径
TestPhantomjs radarPicturePath = getRadarPicturePath(testRadar);
TestWord testWord = TestWord.builder()
.title("这是一个word导出测试")
.fruitTables(fruitTables)
.commodityTables(commodityTables)
.logo(new FileInputStream(radarPicturePath.getPicturePath()))
.build();
File file = new File("D:\\Temp\\title.docx");
FileOutputStream out = new FileOutputStream("D:\\Temp\\测试.docx");
EasyWord.of(file).doWrite(testWord).toOutputStream(out);
}
/**
* 雷达图数据拼接
* @return
* @throws Exception
*/
private static TestRadar radarDataAssembly() throws Exception {
TestRadar radar = new TestRadar();
radar.setTitleText("''");
List<net.sf.json.JSONObject> radarIndicators = new LinkedList<>();
net.sf.json.JSONObject j1 = new net.sf.json.JSONObject();
j1.put("name", "Sales");
j1.put("max", 6500);
radarIndicators.add(j1);
net.sf.json.JSONObject j2 = new net.sf.json.JSONObject();
j2.put("name", "Administration");
j2.put("max", 16000);
radarIndicators.add(j2);
net.sf.json.JSONObject j3 = new net.sf.json.JSONObject();
j3.put("name", "Information Technology");
j3.put("max", 30000);
radarIndicators.add(j3);
net.sf.json.JSONObject j4 = new net.sf.json.JSONObject();
j4.put("name", "Customer Support");
j4.put("max", 38000);
radarIndicators.add(j4);
net.sf.json.JSONObject j5 = new net.sf.json.JSONObject();
j5.put("name", "Development");
j5.put("max", 52000);
radarIndicators.add(j5);
net.sf.json.JSONObject j6 = new net.sf.json.JSONObject();
j6.put("name", "Marketing");
j6.put("max", 25000);
radarIndicators.add(j6);
radar.setRadarIndicator(radarIndicators);
radar.setSeriesName("'Budget vs spending'");
List<String> legendData = new LinkedList<>();
legendData.add("'Allocated Budget'");
legendData.add("'Actual Spending'");
radar.setLegendData(legendData);
// 最主要的数据 series 数据的拼接
List<net.sf.json.JSONObject> seriesData = new LinkedList<>();
net.sf.json.JSONObject json1 = new net.sf.json.JSONObject();
json1.put("name", "'Allocated Budget'");
List<Integer> list1 = new LinkedList<>();
list1.add(4200);list1.add(3000);list1.add(20000);list1.add(35000);list1.add(50000);list1.add(18000);
json1.put("value", list1);
seriesData.add(json1);
net.sf.json.JSONObject json2 = new net.sf.json.JSONObject();
json2.put("name", "'Actual Spending'");
List<Integer> list2 = new LinkedList<>();
list2.add(5000);list2.add(14000);list2.add(28000);list2.add(26000);list2.add(42000);list2.add(21000);
json2.put("value", list2);
seriesData.add(json2);
radar.setRadarShape("'polygon'");
radar.setSeriesData(seriesData);
return radar;
}
/**
* 生成图片,并返回图片路径
* @param radar
* @return
* @throws Exception
*/
private static TestPhantomjs getRadarPicturePath(TestRadar radar) throws Exception {
TestPhantomjsUtil testPhantomjsUtil = new TestPhantomjsUtil();
return testPhantomjsUtil.getRadarPicturePath(radar);
}
}
import org.springframework.stereotype.Component;
import sun.misc.BASE64Decoder;
import java.io.*;
import java.util.UUID;
/**
* 测试Echarts后台渲染生成图片
* @author qing
*/
@Component
public class TestPhantomjsUtil {
/**
* phantomjs 路径
*/
private static final String PHANTOMJS_PATH = "D:/phantomjs-2.1.1-windows/bin/phantomjs.exe";
private static final String UPLOAD_FILE_PATH = "D:/Temp/echartjs";
/**
* echarts获取图片base64编码URL头
*/
private static final String BASE64FORMAT = "data:image/png;base64,";
/**
* 生成图片(默认图片宽高),返回图片路径
* @param option echarts 图表
* @throws Exception
* @return
*/
public TestPhantomjs getPicturePath(String option) throws Exception {
// 得到option 的 js文件路径
String optionPath = writeFile(option);
// 得到图片处理好后的base64码
String pictureBase64 = getPictureBase64(optionPath);
// 生成图片,并返回图片路径
String picturePath = getPicturePathFromBase64(pictureBase64);
TestPhantomjs phantomjs = new TestPhantomjs();
phantomjs.setOptionPath(optionPath);
phantomjs.setPicturePath(picturePath);
return phantomjs;
}
/**
* 命令格式:
* phantomjs echarts-convert.js -infile optionURl -width width -height height
* 可选参数:-width width -height height
* 备注:
* phantomjs添加到环境变量中后可以直接使用,这里防止环境变量配置问题所以直接使用绝对路径
*/
public String getPictureBase64(String dataPath) throws Exception {
String base64 = "";
String echartsPath = UPLOAD_FILE_PATH + File.separator + "echarts-convert.js";
String cmd = PHANTOMJS_PATH + " " + echartsPath + " -infile " + dataPath
// + " -width " + pictureWidth + " -height " + pictureHeight;
;
System.out.println("PhantomjsUtil.getPictureBase64() 执行的命令是:"+ cmd);
Process process = Runtime.getRuntime().exec(cmd);
try (
// 将快照图片生成字节数组
BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))
)
{
String line;
while ((line = input.readLine()) != null) {
if (line.startsWith(BASE64FORMAT)) {
base64 = line;
break;
}
}
return base64;
}
}
/**
* options生成文件存储
* @param options
* @return
*/
public String writeFile(String options) throws Exception {
String dataPath = UPLOAD_FILE_PATH + File.separator + "file" +File.separator+ UUID.randomUUID().toString().substring(0, 8) +".js";
/* option写入文本文件 用于执行命令*/
try (BufferedWriter out = new BufferedWriter(new FileWriter(new File(dataPath))))
{
out.write(options);
// 把缓存区内容压入文件
out.flush();
}
return dataPath;
}
public String getPicturePathFromBase64(String base) throws Exception {
String base64Pic = base;
// 图像数据为空
if (base64Pic == null) {
throw new Exception("图片base64码不存在。");
}
String picturePath = UPLOAD_FILE_PATH + File.separator + "file" +File.separator + System.currentTimeMillis()+".png";
// 如果要返回file文件这边return就可以了,存到临时文件中
try(OutputStream out = new FileOutputStream(picturePath))
{
BASE64Decoder decoder = new BASE64Decoder();
//前台在用Ajax传base64值的时候会把base64中的+换成空格,所以需要替换回来。
String baseValue = base64Pic.replaceAll(" ", "+");
//去除base64中无用的部分
byte[] b = decoder.decodeBuffer(baseValue.replace(BASE64FORMAT, ""));
for (int i = 0; i < b.length; ++i) {
// 调整异常数据
if (b[i] < 0) {
b[i] += 256;
}
}
out.write(b);
out.flush();
}
return picturePath;
}
/**
* 基本雷达图
* @param radar
* @throws Exception
*/
public TestPhantomjs getRadarPicturePath(TestRadar radar) throws Exception {
String options = "option = {\n" +
" title: {\n" +
" text: "+radar.getTitleText()+"\n" +
" },\n" +
" legend: {\n" +
" data: "+radar.getLegendData()+"\n" +
" },\n" +
" radar: {\n" +
" shape: "+radar.getRadarShape()+",\n" +
" indicator: "+radar.getRadarIndicator()+"\n" +
" },\n" +
" series: [\n" +
" {\n" +
" name: "+radar.getSeriesName()+",\n" +
" type: 'radar',\n" +
" data: "+radar.getSeriesData()+"\n" +
" }\n" +
" ]\n" +
"}";
TestPhantomjs phantomjs = new TestPhantomjsUtil().getPicturePath(options);
return phantomjs;
}
}
在本地生成的js以及图片
在我们本地D:\Temp\echartjs\file文件夹中会生成两个文件,这是示例。
导出word最终展示效果