Java输出PPT文件(三) - 饼图数据替换
0. 前言
在这次的开发中,也遇到了PPT文件存在饼图的情况,需要对饼图的数据进行替换,一并记录下。
1. 依赖
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-full -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-full</artifactId>
<version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-schemas -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/ooxml-schemas -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
注意:poi-ooxml、poi-ooxml-full目前最高版本是5.2.3,但需要Apache的commons-io也为高版本,所以这里使用了5.0.0,想试用5.2.3的朋友先解决下依赖问题,笔者遇到的报错如下:
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream
2. 代码
PowerPoint工具测试类:
import org.apache.poi.xslf.usermodel.*;
import org.springframework.util.CollectionUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
/**
* Copyright: Horizon
*
* @ClassName PowerPointUtilTest
* @Description PowerPoint工具测试类
* @Author Nile (QQEmail:576109623)
* @Date 15:48 2022/11/5
* @Version 1.0.0
*/
/*@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j*/
public class PowerPointUtilTest {
public static void main(String[] args) throws IOException {
// 文件路径及文件名称
String rootDir = "src/main/resources/ppt/";
String[] pptArray = {"Title.pptx", "Foreword.pptx", "Dependency.pptx"};
// 参数map
Map<String, String> paramMap = new HashMap<>();
paramMap.put("${date}", "2022年11月13日");
paramMap.put("${book}", "《马普尔小姐最后的案件》");
paramMap.put("${thought}", "不想出去");
paramMap.put("${drink}", "恩施绿茶");
paramMap.put("${doing}", "写写blog");
paramMap.put("${rent}", "25");
paramMap.put("${dining}", "15");
paramMap.put("${shopping}", "10");
paramMap.put("${debt}", "49");
paramMap.put("${saving}", "1");
// 合并
mergePPT(rootDir, Arrays.asList(pptArray), paramMap);
}
/**
* 合并PPT
* @Author Nile (QQEmail:576109623)
* @Date 22:18 2022/11/13
* @param rootDir 文件路径
* @param fileNameList 文件名称列表
* @param paramMap 参数map
* @return void
*/
private static void mergePPT(String rootDir, List<String> fileNameList, Map<String, String> paramMap) throws IOException {
if (CollectionUtils.isEmpty(fileNameList)) {
return;
}
// 1. 使用第1个PPT作为基础文件
XMLSlideShow ppt = new XMLSlideShow(new FileInputStream(rootDir + fileNameList.get(0)));
// 饼图处理(因为代码太占篇幅了,所以写文章时把通用处理移除了)
pptPieHandle(ppt, paramMap);
// 2. 从第2个文件开始遍历,合并
for (int i = 1; i < fileNameList.size(); i++) {
FileInputStream inputstream = new FileInputStream(rootDir + fileNameList.get(i));
XMLSlideShow src = new XMLSlideShow(inputstream);
// 饼图处理(因为代码太占篇幅了,所以写文章时把通用处理移除了)
pptPieHandle(src, paramMap);
// 遍历每张幻灯片
for (XSLFSlide srcSlide : src.getSlides()) {
// 合并
ppt.createSlide().importContent(srcSlide);
}
}
// 3. 输出
String resultName = "Result.pptx";
FileOutputStream out = new FileOutputStream(rootDir + resultName);
ppt.write(out);
out.close();
}
/**
* PPT饼图处理
* @Author Nile (QQEmail:576109623)
* @Date 0:28 2022/11/16
* @param pptx PPT
* @param paramMap 参数map
* @return void
*/
private static void pptPieHandle(XMLSlideShow pptx, Map<String, String> paramMap) {
PowerPointUtil powerPointUtil = new PowerPointUtil(pptx);
// 遍历幻灯片
List<XSLFSlide> slideList = pptx.getSlides();
for (XSLFSlide slide : slideList) {
List<XSLFChart> charts = powerPointUtil.getAllChartFromSlide(slide);
if (CollectionUtils.isEmpty(charts)) {
continue;
}
// 替换饼图数据
XSLFChart chart = charts.get(0);
CalculateUtil calculateUtil = new CalculateUtil();
// 这里印象中开发的时候不需要处理的,忘记了。
// 但为了输出效果处理下,用的是javax.script.ScriptEngine
List<String> tempData = Arrays.asList(calculateUtil.calExpression(paramMap.get("${rent}") + "/100"),
calculateUtil.calExpression(paramMap.get("${dining}") + "/100"),
calculateUtil.calExpression(paramMap.get("${shopping}") + "/100"),
calculateUtil.calExpression(paramMap.get("${debt}") + "/100"),
calculateUtil.calExpression(paramMap.get("${saving}") + "/100"));
if (!tempData.contains(null)) {
powerPointUtil.updatePieDataCache(powerPointUtil.getPieChartFromChart(chart).get(0), 0, tempData);
}
}
}
}
计算工具类:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
/**
* Copyright: Horizon
*
* @ClassName CalculateUtil
* @Description 计算工具类
* @Author Nile (QQEmail:576109623)
* @Date 16:07 2022/11/19
* @Version 1.0.0
*/
public class CalculateUtil {
/**
* 计算公式结果
* @Author Nile (QQEmail:576109623)
* @Date 16:10 2022/11/19
* @param expression 公式
* @return 计算结果,异常返回null
*/
public String calExpression(String expression) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
try {
return engine.eval(expression).toString();
} catch (Exception e) {
return null;
}
}
}
PowerPoint工具类:
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFChart;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.openxmlformats.schemas.drawingml.x2006.chart.*;
import java.util.ArrayList;
import java.util.List;
/**
* Copyright: Horizon
*
* @ClassName PowerPointUtil
* @Description PowerPoint工具类
* @Author Nile (QQEmail:576109623)
* @Date 15:23 2022/11/5
* @Version 1.0.0
*/
@Data
@Slf4j
public class PowerPointUtil {
/**
* PPT文件
*/
private XMLSlideShow pptx;
public PowerPointUtil(XMLSlideShow pptx) {
this.pptx = pptx;
}
/**
* 从当前图表中获取饼状图
* @Author Nile (QQEmail:576109623)
* @Date 16:57 2022/11/5
* @param chart 图表
* @return 饼图列表
*/
public List<CTPieChart> getPieChartFromChart(XSLFChart chart) {
CTPlotArea plotArea = getChartPlotArea(chart);
return plotArea.getPieChartList();
}
/**
* 更新饼图的缓存数据
* @Author Nile (QQEmail:576109623)
* @Date 17:04 2022/11/5
* @param pieChart 饼图
* @param serIndex 索引
* @param data 数据
* @return void
*/
public void updatePieDataCache(CTPieChart pieChart, int serIndex, List<String> data) {
CTPieSer ctPieSer = pieChart.getSerList().get(serIndex);
CTNumRef numRef = ctPieSer.getVal().getNumRef();
replaceVal(numRef, data);
// 设置饼图显示精度
numRef.getNumCache().setFormatCode("0.00%");
}
/**
* 替换数据
* @Author Nile (QQEmail:576109623)
* @Date 17:05 2022/11/5
* @param numRef 数字引用
* @param data 数据
* @return void
*/
private void replaceVal(CTNumRef numRef, List<String> data) {
numRef.unsetNumCache();
CTNumData ctNumData = numRef.addNewNumCache();
ctNumData.addNewPtCount().setVal(data.size());
for (int i = 0; i < data.size(); i++) {
CTNumVal ctNumVal = ctNumData.addNewPt();
ctNumVal.setIdx(i);
ctNumVal.setV(data.get(i));
}
}
/**
* 获取plotArea
* @Author Nile (QQEmail:576109623)
* @Date 17:15 2022/11/5
* @param chart 图表
* @return plotArea
*/
private CTPlotArea getChartPlotArea(XSLFChart chart) {
return chart.getCTChart().getPlotArea();
}
/**
* 从幻灯片中获取图表列表
* @Author Nile (QQEmail:576109623)
* @Date 16:35 2022/11/13
* @param slide 幻灯片
* @return 图表列表
*/
public List<XSLFChart> getAllChartFromSlide(XSLFSlide slide) {
List<XSLFChart> charts = new ArrayList<>();
for (POIXMLDocumentPart relation : slide.getRelations()) {
if (relation instanceof XSLFChart) {
charts.add((XSLFChart) relation);
}
}
return charts;
}
}
3. 测试
3.1 饼图数据
需要使用excel准备好饼图的基础数据,并插入饼图,调整好样式、标签等
3.2 模板准备
复制饼图,粘贴到PPT中,粘贴选项选择“使用目标主题和嵌入工作簿”。
3.3 替换结果
4. 问题
4.1 数据
上面的代码是写死的!!! 换句话说,上面这个代码只能用来处理这个特定的饼图。
没太多时间来写这个了,笔者初步的想法是,写一个策略模式,根据不同的饼图,获取到需要的占位符,通过paramMap获取实际值,然后构建tempData,作为pptPieHandle方法的入参。当然这就需要有一个管理功能来维护 PPT文件 - 饼图 - 数据 的关系了。
其实,在开发过程中,也查到了另外一种维护饼图数据的方式,就是在PPT中创建一个excel表格(内嵌),然后将excel表格数据同这个饼图绑定,通过更新excel表格的数据来更新饼图。但实际测试过程,要嘛样式有问题,要嘛数据有问题,要嘛替换失败。虽然也有成功的,最后还是使用了本文这种方式。
4.2 数据校验
这点就比较繁琐了,需要使用正则校验入参,必须保证每个数值的取值范围为:[0, 1],且总和要为1。
其实笔者也试过传入负数,负号会被忽略,当成正数切割饼图;传入字符,按0处理;总和不为1,根据各个数占总和的占比切割饼图…所以这里需要根据需要进行数据校验和处理。