且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

使用Apache POI从PowerPoint幻灯片中删除图表

更新时间:2023-02-14 09:14:56

由于XSLFChart处于@Beta状态,因此到目前为止,图表没有明确的Shape.因此,使用apache poi只能获取包含图表的XSLFGraphicFrame.但是从幻灯片中删除XSLFGraphicFrame不会同时删除所有相关的图表部分.因此,自上而下删除相关图表部分意味着从POIXMLDocumentPart级别降低到PackagePart级别直到现在都没有实现.而且,由于POIXMLDocumentPart中的所有相关方法都受到保护,并且XSLFChart本身是最终的,因此解决此问题的可能性并不大.

Since XSLFChart is in @Beta state, there is not a explicit Shape for a chart until now. So using apache poi we can only get XSLFGraphicFrames which are containing charts. But removing a XSLFGraphicFrame from the slide will not removing all related chart parts too. So the removing the related chart parts top-down, means from POIXMLDocumentPart level down to PackagePart level is not implemented until now. And since all relevant methods in POIXMLDocumentPart are protected and the XSLFChart itself is final there is not really a easy possibility to work around.

以下代码显示了问题.这样评论.

The folowing code shows the problem. It is commented as such.

该代码从第一张幻灯片中删除了所有图表,并删除了所有的关系和相关部分,例如:/ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx/ppt/charts/colorsN.xml/ppt/charts/styleN.xml.如注释所示,仅/ppt/charts/chartN.xml无法删除.

The code removes all charts from the first slide and removes all relationships and related parts that would be: /ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx, /ppt/charts/colorsN.xml and /ppt/charts/styleN.xml. Only /ppt/charts/chartN.xml cannot be removed, as it is commented.

import java.io.FileInputStream;
import java.io.FileOutputStream;

import org.apache.poi.xslf.usermodel.*;
import org.apache.poi.sl.usermodel.*;

import org.apache.poi.POIXMLDocumentPart;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackageRelationship;

import org.apache.xmlbeans.XmlObject;

import java.util.Map;
import java.util.HashMap;

import java.util.regex.Pattern;

public class ReadPPTRemoveChart {

 public static void main(String[] args) throws Exception {

  XMLSlideShow slideShow = new XMLSlideShow(new FileInputStream("PPTWithCharts.pptx"));

  XSLFSlide slide = slideShow.getSlides().get(0);

  Map<String, XSLFGraphicFrame> chartFramesToRemove = new HashMap<>();

  for (XSLFShape shape : slide.getShapes()) {
   if (shape instanceof XSLFGraphicFrame) {
    XSLFGraphicFrame graphicframe = (XSLFGraphicFrame)shape;
    XmlObject xmlobject = graphicframe.getXmlObject();
    XmlObject[] graphics = xmlobject.selectPath(
                            "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " +
                            ".//a:graphic");
    if (graphics.length > 0) { //we have a XSLFGraphicFrame containing a:graphic
     XmlObject graphic = graphics[0];
     XmlObject[] charts = graphic.selectPath(
                           "declare namespace c='http://schemas.openxmlformats.org/drawingml/2006/chart' " +
                           ".//c:chart");
     if (charts.length > 0) { //we have a XSLFGraphicFrame containing c:chart
      XmlObject chart = charts[0];
      String rid = chart.selectAttribute(
                          "http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id")
                          .newCursor().getTextValue();
      chartFramesToRemove.put(rid, graphicframe);
     }
    }
   }
  }

  PackagePart slidepart = slide.getPackagePart();
  OPCPackage opcpackage = slideShow.getPackage();

  for (String rid : chartFramesToRemove.keySet()) {
   //at frist remove the XSLFGraphicFrame
   XSLFGraphicFrame chartFrame = chartFramesToRemove.get(rid);
   slide.removeShape(chartFrame);
   //Here is the problem in my opinion. This **should** remove all related parts too.
   //But since XSLFChart is @Beta, it does not.

   //So we try doing removing the related parts manually.
   //we get the PackagePart of the chart
   PackageRelationship relship = slidepart.getRelationships().getRelationshipByID(rid);
   PackagePart chartpart = slidepart.getRelatedPart(relship);

   //now we get and remove all the relations and related PackageParts from this chartpart
   //this are /ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx, /ppt/charts/colorsN.xml 
   //and /ppt/charts/styleN.xml
   for (PackageRelationship chartrelship : chartpart.getRelationships()) {
    String partname = chartrelship.getTargetURI().toString();
    PackagePart part = opcpackage.getPartsByName(Pattern.compile(partname)).get(0);
    opcpackage.removePart(part);
    chartpart.removeRelationship(chartrelship.getId());
   }
   //this works

   //now we **should** be able removing the relationship to the chartpart from the slide too
   //but this seems not to be possible
   //doing this on PackagePart level works:
   slidepart.removeRelationship(rid);
   for (PackageRelationship sliderelship : slidepart.getRelationships()) {
    System.out.println("rel PP level: " + sliderelship.getTargetURI().toString());
   }
   //all relationships to /ppt/charts/chartN.xml are removed

   //but on POIXMLDocumentPart level this has no effect
   for (POIXMLDocumentPart sliderelpart : slide.getRelations()) {
    System.out.println("rel POIXML level: " + sliderelpart.getPackagePart().getPartName());
   }
   //relationships to /ppt/charts/chartN.xml are **not** removed

   //So we cannot remove the chartpart.
   //If we would do this, then while slideShow.write the 
   //org.apache.poi.xslf.usermodel.XSLFChart.commit in XSLFChart.java fails 
   //because after removing the PackagePart is absent but the relation is still there.
   //opcpackage.removePart(chartpart);

  }


  slideShow.write(new FileOutputStream("PPTWithChartsNew.pptx"));
  slideShow.close();
 }
}

使用PowerPoint打开PPTWithChartsNew.pptx并保存后,由于不需要的/ppt/charts/styleN.xml部分也不再存在,因此也将其删除.

After opening the PPTWithChartsNew.pptx using PowerPoint and saving it then, the unnecessary /ppt/charts/styleN.xml parts are removed too since there are no more relations to them.

编辑2017年9月24日:

Edit Sep 24 2017:

找到了使用反射的解决方案.如前所述,删除相关图表部分需要自上而下,即从POIXMLDocumentPart级别降至PackagePart级别.并且由于POIXMLDocumentPart.removeRelation受保护,我们需要使用反射来完成此操作.

Found a solution using reflection. As said, the removing the related chart parts needs to be top-down, means from POIXMLDocumentPart level down to PackagePart level. And since POIXMLDocumentPart.removeRelation is protected, we need doing this using reflection.

import java.io.FileInputStream;
import java.io.FileOutputStream;

import org.apache.poi.xslf.usermodel.*;
import org.apache.poi.sl.usermodel.*;

import org.apache.poi.POIXMLDocumentPart;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackageRelationship;

import org.apache.xmlbeans.XmlObject;

import java.util.Map;
import java.util.HashMap;

import java.util.regex.Pattern;

import java.lang.reflect.Method;

public class ReadPPTRemoveChart {

 public static void main(String[] args) throws Exception {

  XMLSlideShow slideShow = new XMLSlideShow(new FileInputStream("PPTWithCharts.pptx"));

  XSLFSlide slide = slideShow.getSlides().get(0);

  Map<String, XSLFGraphicFrame> chartFramesToRemove = new HashMap<>();

  for (XSLFShape shape : slide.getShapes()) {
   if (shape instanceof XSLFGraphicFrame) {
    XSLFGraphicFrame graphicframe = (XSLFGraphicFrame)shape;
    XmlObject xmlobject = graphicframe.getXmlObject();
    XmlObject[] graphics = xmlobject.selectPath(
                            "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " +
                            ".//a:graphic");
    if (graphics.length > 0) { //we have a XSLFGraphicFrame containing a:graphic
     XmlObject graphic = graphics[0];
     XmlObject[] charts = graphic.selectPath(
                           "declare namespace c='http://schemas.openxmlformats.org/drawingml/2006/chart' " +
                           ".//c:chart");
     if (charts.length > 0) { //we have a XSLFGraphicFrame containing c:chart
      XmlObject chart = charts[0];
      String rid = chart.selectAttribute(
                          "http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id")
                          .newCursor().getTextValue();
      chartFramesToRemove.put(rid, graphicframe);
     }
    }
   }
  }

  PackagePart slidepart = slide.getPackagePart();
  OPCPackage opcpackage = slideShow.getPackage();

  for (String rid : chartFramesToRemove.keySet()) {
   //at frist remove the XSLFGraphicFrame
   XSLFGraphicFrame chartFrame = chartFramesToRemove.get(rid);
   slide.removeShape(chartFrame);
   //Here is the problem in my opinion. This **should** remove all related parts too.
   //But since XSLFChart is @Beta, it does not.

   //So we try doing removing the related parts manually.

   //we get the PackagePart of the chart
   PackageRelationship relship = slidepart.getRelationships().getRelationshipByID(rid);
   PackagePart chartpart = slidepart.getRelatedPart(relship);

   //now we get and remove all the relations and related PackageParts from this chartpart
   //this are /ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx, /ppt/charts/colorsN.xml 
   //and /ppt/charts/styleN.xml
   for (PackageRelationship chartrelship : chartpart.getRelationships()) {
    String partname = chartrelship.getTargetURI().toString();
    PackagePart part = opcpackage.getPartsByName(Pattern.compile(partname)).get(0);
    opcpackage.removePart(part);
    chartpart.removeRelationship(chartrelship.getId());
   }

   //now we remove the chart part from the slide part
   //We need doing this on POIXMLDocumentPart level. 
   //Since POIXMLDocumentPart.removeRelation is protected, we need doing this using reflection
   XSLFChart chart = (XSLFChart)slide.getRelationById(rid);
   Method removeRelation = POIXMLDocumentPart.class.getDeclaredMethod("removeRelation", POIXMLDocumentPart.class); 
   removeRelation.setAccessible(true); 
   removeRelation.invoke(slide, chart);

  }

  slideShow.write(new FileOutputStream("PPTWithChartsNew.pptx"));
  slideShow.close();
 }
}