java 使用 poi 更新 ppt 中图表的数据

时间:2024-02-29 17:52:03

 

本文源码:    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

  1. XSLFSlide.getRelationParts();
  2. 遍历上面的数组
  3. 检查XSLFSlide.getRelationParts().get(n).getDocumentPart()的类型 instanceof XSLFChart

3.5.3.2            Chart关联的excel

 

  1. 读取:XSSFWookbook workbook = XSLFChart.getWorkBook()
  2. 修改:使用XSSFWookbook, XSSFSheet的相关接口
  3. 保存:步骤1返回的workbook.write(chart.getPackagepart().getOutputStream())

 

3.5.3.3            chart的缓存数据

 

  1. 通过 3.5.3.1 找到XSLFChart
  2. 找到绘图区域(xml中c:plotArea):XSLFChart.getCTChart().getPlotArea()
  3. 根据类型找到图表实例(可能是:CTPieChart, CTBarChart等):XSLFChart.getCTChart().getPlotArea().getXXXChartList()不为空的。
  4. 每个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.    流程及源码

  1. 获取 SlideShow
  2. 遍历 XSLFSlide
  3. 遍历 XSLFSlide的依赖部分
  4. 找到依赖部分为图表 (XSLFChart)的
  5. 根据图表标题、类型找到对应图表
  6. 更新图表关联的excel
  7. 更新图表的界面缓存数据
  8. 更新图表与关联excel的关系
  9. 保存新文件

 代码如下:调用 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.    运行示例

 

 

相关文章