本文源碼: 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() { } } }
6. 運行示例