本文源码: 1. https://github.com/zhongchengyi/zhongcy.demos/tree/master/apoi-ppt-chart
2. 在第5节也有核心源码
1. apoi简介
Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。
其中:
HSSF - 提供读写Microsoft Excel格式档案的功能。
XSSF - 提供读写Microsoft Excel OOXML格式档案的功能。
HWPF - 提供读写Microsoft Word格式档案的功能。
HSLF - 提供读写Microsoft PowerPoint格式档案的功能。
HDGF - 提供读写Microsoft Visio格式档案的功能。
这里主要用到 HSLF
2. POI PPT特点
- 比较原始,与 XSSF 不同,没有对ppt做太好的封装,基本全是操作xml的方法。
- 关于poi ppt的文档比较少
- 关于open-xml的文档也比较少
- 为数不多的可以操作ppt的库
3. PPT文档结构简介
由于文档稀少,推荐自己创建简单的PPT,了解里面xml的结构,再根据其结构,通过代码读取,修改。
如:我自己创建了一个简单的ppt,只有一页,里面两个图表,我想找到图表数据所在的位置。
3.1 新建1.pptx内容如下
3.2 将1.pptx修改为1.zip
3.3 用解压工具对1.zip解压
3.4 ppt\slides 幻灯片
- 里面是幻灯片的xml,每一个文件代表一页幻灯片
- 一般是按照 slide1.xml , slide2.xml 命名的,后面的数字是页号
- 每个xml都是压缩结构的文档(即内容只有两行)
使用idea打开slide1.xml,格式化后,如图:
slide.xml 是记录幻灯片的结构:其中 Shape会记录里面的文本,批注,图表,备注都是记录rid, 这些信息都是记录在p:spTree节点下。
3.5 ppt\charts 图表数据
- 此目录记录以chartxx.xml图表信息
- 每个图表一个文件
- 所有幻灯片的图表都在这个目录,没有子目录了。
打开 chart1.xml
再打开1.pptx,找到第一张图表关联的数据,下图标注了系列具体的位置,其中,ser2代表A列和C列(c:cat部分与第一个c:ser共用)
3.5.1 c:ser / c:cat
- c:f 图表与excel 的关联关系,Sheet1!$A$2:$A$4 代表是sheet1的A列2行,到A列4行
- c:strCache 图表的缓存数据,是一个数组,c:ptCount是数组的长度,c:pt是数组里面的数据(如果更新图表时数据行与ppt原图表的长度不一样,需要更新 c:f, c:ptCount, c:pt)
3.5.2 c:ser / c:num
- 结构上与 c:cat 是一样的。
- c:numRef代表excel中的这一列是数字类型,
- c:strRef代表excel中的这一列是字符类型。
- 需要注意的是:c:cat和c:val下都有可能是c:numRef 或 c:strRef(我的源码这里没有判断)
3.5.3 相关接口
3.5.3.1 获取幻灯片的Chart
- XSLFSlide.getRelationParts();
- 遍历上面的数组
- 检查XSLFSlide.getRelationParts().get(n).getDocumentPart()的类型 instanceof XSLFChart
3.5.3.2 Chart关联的excel
- 读取:XSSFWookbook workbook = XSLFChart.getWorkBook()
- 修改:使用XSSFWookbook, XSSFSheet的相关接口
- 保存:步骤1返回的workbook.write(chart.getPackagepart().getOutputStream())
3.5.3.3 chart的缓存数据
- 通过 3.5.3.1 找到XSLFChart
- 找到绘图区域(xml中c:plotArea):XSLFChart.getCTChart().getPlotArea()
- 根据类型找到图表实例(可能是:CTPieChart, CTBarChart等):XSLFChart.getCTChart().getPlotArea().getXXXChartList()不为空的。
- 每个Chart实例都是同样的结构,以CTPieChart为例:CTPieChart.getCat获取c:cat, CTPieChart.getVal获取c:val
3.6 ppt\embeddings 嵌入的文档
4. 准备
- 使用IDEA新建一个java 控制台程序
- 新建一个 pom.xml 文件
- 在 pom.xml 中增加 apache poi 的依赖
- 使用 maven 安装依赖
4.1 poi的依赖如下
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
安装完成后,在idea的 libraies 里会增加以下:
5.
流程及源码
- 获取 SlideShow
- 遍历 XSLFSlide
- 遍历 XSLFSlide的依赖部分
- 找到依赖部分为图表 (XSLFChart)的
- 根据图表标题、类型找到对应图表
- 更新图表关联的excel
- 更新图表的界面缓存数据
- 更新图表与关联excel的关系
- 保存新文件
代码如下:调用 run 方法
package zhongcy.demos; import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.sl.usermodel.SlideShow;
import org.apache.poi.sl.usermodel.SlideShowFactory;
import org.apache.poi.xslf.usermodel.XSLFChart;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.openxmlformats.schemas.drawingml.x2006.chart.*; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern; public class PPTDemo { public void run() {
try {
SlideShow slideShow = SlideShowFactory.create(new File("./res/1.pptx")); for (Object o : slideShow.getSlides()) {
XSLFSlide slider = (XSLFSlide) o; // 第一页
if (slider.getSlideNumber() == 1) {
for (POIXMLDocumentPart.RelationPart part : slider.getRelationParts()) {
POIXMLDocumentPart documentPart = part.getDocumentPart();
// 是图表
if (documentPart instanceof XSLFChart) {
XSLFChart chart = (XSLFChart) documentPart; // 查看里面的图表数据,才能知道是什么图表
CTPlotArea plot = chart.getCTChart().getPlotArea(); // 测试数据
List<SeriesData> seriesDatas = Arrays.asList(
new SeriesData("", Arrays.asList(
new NameDouble("行1", Math.random() * 100),
new NameDouble("行2", Math.random() * 100),
new NameDouble("行3", Math.random() * 100),
new NameDouble("行4", Math.random() * 100),
new NameDouble("行5", Math.random() * 100)
)),
new SeriesData("", Arrays.asList(
new NameDouble("行1", Math.random() * 100),
new NameDouble("行2", Math.random() * 100),
new NameDouble("行3", Math.random() * 100),
new NameDouble("行4", Math.random() * 100),
new NameDouble("行5", Math.random() * 100)
))
);
XSSFWorkbook workbook = chart.getWorkbook();
XSSFSheet sheet = workbook.getSheetAt(0); // 柱状图
if (!plot.getBarChartList().isEmpty()) {
CTBarChart barChart = plot.getBarChartArray(0);
updateChartExcelV(seriesDatas, workbook, sheet);
workbook.write(chart.getPackagePart().getOutputStream()); int i = 0;
for (CTBarSer ser : barChart.getSerList()) {
updateChartCatAndNum(seriesDatas.get(i), ser.getTx(), ser.getCat(), ser.getVal());
++i;
}
} // 饼图
else if (!plot.getPieChartList().isEmpty()) {
// 示例饼图只有一列数据
updateChartExcelV(Arrays.asList(seriesDatas.get(0)), workbook, sheet);
workbook.write(chart.getPackagePart().getOutputStream()); CTPieChart pieChart = plot.getPieChartArray(0);
int i = 0;
for (CTPieSer ser : pieChart.getSerList()) {
updateChartCatAndNum(seriesDatas.get(i), ser.getTx(), ser.getCat(), ser.getVal());
++i;
}
}
}
}
} } try {
try (FileOutputStream out = new FileOutputStream("./res/o1.pptx")) {
slideShow.write(out);
}
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
} } catch (IOException e) {
e.printStackTrace();
} catch (InvalidFormatException e) {
e.printStackTrace();
}
} /**
* 更新图表的关联 excel, 值是纵向的
*
* @param param
* @param workbook
* @param sheet
*/
protected void updateChartExcelV(List<SeriesData> seriesDatas, XSSFWorkbook workbook, XSSFSheet sheet) {
XSSFRow title = sheet.getRow(0);
for (int i = 0; i < seriesDatas.size(); i++) {
SeriesData data = seriesDatas.get(i);
if (data.name != null && !data.name.isEmpty()) {
// 系列名称,不能修改,修改后无法打开 excel
// title.getCell(i + 1).setCellValue(data.name);
}
int size = data.value.size();
for (int j = 0; j < size; j++) {
XSSFRow row = sheet.getRow(j + 1);
if (row == null) {
row = sheet.createRow(j + 1);
}
NameDouble cellValu = data.value.get(j);
XSSFCell cell = row.getCell(0);
if (cell == null) {
cell = row.createCell(0);
}
cell.setCellValue(cellValu.name); cell = row.getCell(i + 1);
if (cell == null) {
cell = row.createCell(i + 1);
}
cell.setCellValue(cellValu.value);
}
int lastRowNum = sheet.getLastRowNum();
if (lastRowNum > size) {
for (int idx = lastRowNum; idx > size; idx--) {
sheet.removeRow(sheet.getRow(idx));
}
}
}
} /**
* 更新 chart 的缓存数据
*
* @param data 数据
* @param serTitle 系列的标题缓存
* @param catDataSource 条目的数据缓存
* @param numDataSource 数据的缓存
*/
protected void updateChartCatAndNum(SeriesData data, CTSerTx serTitle, CTAxDataSource catDataSource,
CTNumDataSource numDataSource) { // 更新系列标题
// serTitle.getStrRef().setF(serTitle.getStrRef().getF()); //
// serTitle.getStrRef().getStrCache().getPtArray(0).setV(data.name); // TODO cat 也可能是 numRef
long ptCatCnt = catDataSource.getStrRef().getStrCache().getPtCount().getVal();
long ptNumCnt = numDataSource.getNumRef().getNumCache().getPtCount().getVal();
int dataSize = data.value.size();
for (int i = 0; i < dataSize; i++) {
NameDouble cellValu = data.value.get(i);
CTStrVal cat = ptCatCnt > i ? catDataSource.getStrRef().getStrCache().getPtArray(i)
: catDataSource.getStrRef().getStrCache().addNewPt();
cat.setIdx(i);
cat.setV(cellValu.name); CTNumVal val = ptNumCnt > i ? numDataSource.getNumRef().getNumCache().getPtArray(i)
: numDataSource.getNumRef().getNumCache().addNewPt();
val.setIdx(i);
val.setV(String.format("%.2f", cellValu.value)); } // 更新对应 excel 的range
catDataSource.getStrRef().setF(
replaceRowEnd(catDataSource.getStrRef().getF(),
ptCatCnt,
dataSize));
numDataSource.getNumRef().setF(
replaceRowEnd(numDataSource.getNumRef().getF(),
ptNumCnt,
dataSize)); // 删除多的
if (ptNumCnt > dataSize) {
for (int idx = dataSize; idx < ptNumCnt; idx++) {
catDataSource.getStrRef().getStrCache().removePt(dataSize);
numDataSource.getNumRef().getNumCache().removePt(dataSize);
}
}
// 更新个数
catDataSource.getStrRef().getStrCache().getPtCount().setVal(dataSize);
numDataSource.getNumRef().getNumCache().getPtCount().setVal(dataSize);
} /**
* 替换 形如: Sheet1!$A$2:$A$4 的字符
*
* @param range
* @return
*/
public static String replaceRowEnd(String range, long oldSize, long newSize) {
Pattern pattern = Pattern.compile("(:\\$[A-Z]+\\$)(\\d+)");
Matcher matcher = pattern.matcher(range);
if (matcher.find()) {
long old = Long.parseLong(matcher.group(2));
return range.replaceAll("(:\\$[A-Z]+\\$)(\\d+)", "$1" + Long.toString(old - oldSize + newSize));
}
return range;
} /**
* 一个系列的数据
*/
public static class SeriesData { /**
* value 系列的名字
*/
public String name; public List<NameDouble> value; public SeriesData(java.util.List<NameDouble> value) {
this.value = value;
} public SeriesData(String name, List<NameDouble> value) {
this.name = name;
this.value = value;
} public SeriesData() {
}
} /**
*
*/
public class NameDouble { public String name; /**
*/
public double value; public NameDouble(String name, double value) {
this.name = name;
this.value = value;
} @SuppressWarnings("unused")
public NameDouble() {
} }
}