本文源码: 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.poigroupId>
<artifactId>poi-ooxmlartifactId>
<version>4.1.1version>
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(); // 测试数据 ListseriesDatas = 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 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 value; public SeriesData(java.util.List value) { this.value = value; } public SeriesData(String name, List 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() { } } }
6. 运行示例