]> source.dussan.org Git - poi.git/commitdiff
[bug-66176] Integrate SmartArt diagrams from powerpoint presentations. Thanks to...
authorPJ Fanning <fanningpj@apache.org>
Fri, 22 Jul 2022 09:55:10 +0000 (09:55 +0000)
committerPJ Fanning <fanningpj@apache.org>
Fri, 22 Jul 2022 09:55:10 +0000 (09:55 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1902934 13f79535-47bb-0310-9956-ffa450edef68

poi-examples/src/main/java/org/apache/poi/examples/xslf/SmartArtConversionDemo.java [new file with mode: 0644]
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagram.java [new file with mode: 0644]
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagramDrawing.java [new file with mode: 0644]
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFRelation.java
poi-ooxml/src/test/java/org/apache/poi/xslf/usermodel/TestXSLFDiagram.java [new file with mode: 0644]
test-data/slideshow/smartart-simple.pptx [new file with mode: 0644]

diff --git a/poi-examples/src/main/java/org/apache/poi/examples/xslf/SmartArtConversionDemo.java b/poi-examples/src/main/java/org/apache/poi/examples/xslf/SmartArtConversionDemo.java
new file mode 100644 (file)
index 0000000..95f9f9a
--- /dev/null
@@ -0,0 +1,137 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.examples.xslf;
+
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.xslf.usermodel.*;
+import org.openxmlformats.schemas.drawingml.x2006.main.CTBlip;
+import org.openxmlformats.schemas.drawingml.x2006.main.CTBlipFillProperties;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Converts SmartArt to openxml shapes and saves the result to the specified output path.
+ */
+public class SmartArtConversionDemo {
+
+    private final XMLSlideShow inputPptx;
+    private final XMLSlideShow outputPptx;
+
+    SmartArtConversionDemo(XMLSlideShow inputPptx, XMLSlideShow outputPptx) {
+        this.inputPptx = inputPptx;
+        this.outputPptx = outputPptx;
+    }
+
+    public static void main(String[] args) throws IOException {
+        if (args.length != 2) {
+            System.out.println("Expected arguments: <inputPath> <outputPath>");
+            System.exit(1);
+        }
+
+        File inputFile = new File(args[0]);
+        if (!inputFile.exists()) {
+            System.out.printf("Unable to find input file at path: %s", args[0]);
+            System.exit(1);
+        }
+
+        try (
+                FileInputStream inputPptxStream = new FileInputStream(inputFile);
+                FileOutputStream outputPptxStream = new FileOutputStream(args[1])
+        ) {
+            XMLSlideShow inputPptx = new XMLSlideShow(inputPptxStream);
+            XMLSlideShow outputPptx = new XMLSlideShow();
+            SmartArtConversionDemo demo = new SmartArtConversionDemo(inputPptx, outputPptx);
+            demo.convertSmartArt();
+            outputPptx.write(outputPptxStream);
+        }
+    }
+
+    private static void copyAndUpdateImageRelations(XSLFDiagram diagram, XSLFSlide outputSlide) throws IOException {
+        XSLFGroupShape inputGroupShape = diagram.getGroupShape();
+        for (XSLFShape shape : inputGroupShape.getShapes()) {
+            org.openxmlformats.schemas.presentationml.x2006.main.CTShape ctShape
+                    = (org.openxmlformats.schemas.presentationml.x2006.main.CTShape) shape.getXmlObject();
+
+            if (ctShape.getSpPr().getBlipFill() == null) {
+                continue;
+            }
+
+            CTBlipFillProperties blipFillProps = ctShape.getSpPr().getBlipFill();
+            CTBlip blip = blipFillProps.getBlip();
+            // In SmartArt diagrams, the references to images/embeds are stored in drawing#.xml.rels. When read by
+            // POI it copies this relationship to the parent slide to allow POI to correctly resolve the images.
+            POIXMLDocumentPart inputPicturePart = diagram.getSheet().getRelationById(blip.getEmbed());
+
+            if (inputPicturePart == null || inputPicturePart.getPackagePart() == null) {
+                continue;
+            }
+
+            XSLFPictureData inputPictureData = new XSLFPictureData(inputPicturePart.getPackagePart());
+
+            // Copy the input image to the output slides and update the shape to reference the copied image
+            XMLSlideShow outputPptx = outputSlide.getSlideShow();
+            XSLFPictureData outputPictureData = outputPptx.addPicture(
+                    inputPicturePart.getPackagePart().getInputStream(), inputPictureData.getType());
+            POIXMLDocumentPart.RelationPart outputRelation = outputSlide.addRelation(null, XSLFRelation.IMAGES, outputPictureData);
+            ctShape.getSpPr().getBlipFill().getBlip().setEmbed(outputRelation.getRelationship().getId());
+        }
+    }
+
+    private static XSLFTheme extractTheme(XMLSlideShow slideShow) {
+        if (!slideShow.getSlideMasters().isEmpty()) {
+            return slideShow.getSlideMasters().get(0).getTheme();
+        }
+        return null;
+    }
+
+    private void convertSmartArt() throws IOException {
+        // Copy page size and theme
+        outputPptx.setPageSize(inputPptx.getPageSize());
+        XSLFTheme theme = extractTheme(inputPptx);
+        if (theme != null) {
+            outputPptx.getSlideMasters().get(0).getTheme().getXmlObject().set(theme.getXmlObject());
+        }
+
+        for (XSLFSlide inputSlide : inputPptx.getSlides()) {
+            XSLFSlide outputSlide = outputPptx.createSlide();
+
+            List<XSLFShape> inputShapes = inputSlide.getShapes();
+            for (XSLFShape shape : inputShapes) {
+                if (shape instanceof XSLFDiagram) {
+                    copyDiagramToOutput((XSLFDiagram) shape, outputSlide);
+                } else {
+                    XSLFAutoShape autoShape = outputSlide.createAutoShape();
+                    // Hacky hack. Reassign xml to copy the content over.
+                    autoShape.getXmlObject().set(shape.getXmlObject());
+                }
+            }
+        }
+    }
+
+    private void copyDiagramToOutput(XSLFDiagram inputDiagram, XSLFSlide outputSlide) throws IOException {
+        // This method modifies the underlying xml of the input shapes. We modify the xml structure first, then
+        // assign that to our output shape.
+        copyAndUpdateImageRelations(inputDiagram, outputSlide);
+        XSLFGroupShape inputGroupShape = inputDiagram.getGroupShape();
+        XSLFGroupShape outputGroupShape = outputSlide.createGroup();
+        outputGroupShape.getXmlObject().set(inputGroupShape.getXmlObject());
+    }
+}
diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagram.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagram.java
new file mode 100644 (file)
index 0000000..ee248c0
--- /dev/null
@@ -0,0 +1,243 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.xslf.usermodel;
+
+import com.microsoft.schemas.office.drawing.x2008.diagram.CTGroupShape;
+import com.microsoft.schemas.office.drawing.x2008.diagram.CTShape;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.util.Beta;
+import org.apache.xmlbeans.XmlObject;
+import org.openxmlformats.schemas.drawingml.x2006.diagram.CTRelIds;
+import org.openxmlformats.schemas.drawingml.x2006.main.CTGraphicalObjectData;
+import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupShapeProperties;
+import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph;
+import org.openxmlformats.schemas.presentationml.x2006.main.CTApplicationNonVisualDrawingProps;
+import org.openxmlformats.schemas.presentationml.x2006.main.CTGraphicalObjectFrame;
+import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShapeNonVisual;
+import org.openxmlformats.schemas.presentationml.x2006.main.CTShapeNonVisual;
+
+import javax.xml.namespace.QName;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Representation of a DrawingML Diagram
+ * <p>
+ * This class converts the diagram to an {@link XSLFGroupShape} accessible via {@link #getGroupShape()}. The underlying
+ * {@link XSLFDiagramDrawing} used to create the group shape is accessible via {@link #getDiagramDrawing()}.
+ * <p>
+ * In pptx files, these diagrams are generated by creating SmartArt. When a pptx has SmartArt, a directory with the
+ * following structure is created:
+ *
+ * <pre>
+ * ppt/
+ *   diagrams/
+ *     data#.xml
+ *     drawing#.xml^
+ *     colors#.xml
+ *     quickStyle#.xml
+ *     layout#.xml
+ *     rels/
+ *       data#.xml.rels
+ *       drawing#.xml.rels
+ * </pre>
+ * <p>
+ * ^The `drawing#.xml` file is not in the OpenXML spec. It was added as an extension by Microsoft, namespace:
+ * http://schemas.microsoft.com/office/drawing/2008/diagram
+ * <p>
+ * The drawing#.xml file contains the rendered output of the diagram. This class reads the underlying drawing#.xml and
+ * converts it to a {@link XSLFGroupShape}.
+ * <p>
+ * The data, drawing, colors, and quickStyle files are in the OpenXML spec. These contain the instructions that define
+ * how to render the diagram. Rendering diagrams from these files is not trivial, they support for loops, if/elses, etc.
+ * Integrating such a change into POI would be quite sophisticated and challenging.
+ *
+ * @since POI 5.2.3
+ */
+@Beta
+public class XSLFDiagram extends XSLFGraphicFrame {
+
+    public static final String DRAWINGML_DIAGRAM_URI = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
+    private final XSLFDiagramDrawing _drawing;
+    private final XSLFGroupShape _groupShape;
+
+    /* package protected */ XSLFDiagram(CTGraphicalObjectFrame shape, XSLFSheet sheet) {
+        super(shape, sheet);
+        _drawing = readDiagramDrawing(shape, sheet);
+        _groupShape = initGroupShape(sheet);
+    }
+
+    private static boolean hasTextContent(CTShape msShapeCt) {
+        if (msShapeCt.getTxBody() == null || msShapeCt.getTxXfrm() == null) {
+            return false;
+        }
+        // A shape has text content when there is at least 1 paragraph with 1 paragraph run list
+        List<CTTextParagraph> paragraphs = msShapeCt.getTxBody().getPList();
+        return paragraphs.stream()
+                .flatMap(p -> p.getRList().stream())
+                .anyMatch(run -> run.getT() != null && !run.getT().trim().isEmpty());
+    }
+
+    private static boolean hasBlipEmbed(CTShape msShapeCt) {
+        return msShapeCt != null
+                && msShapeCt.getSpPr() != null
+                && msShapeCt.getSpPr().getBlipFill() != null
+                && msShapeCt.getSpPr().getBlipFill().getBlip() != null
+                && msShapeCt.getSpPr().getBlipFill().getBlip().getEmbed() != null;
+    }
+
+    private static XSLFDiagramDrawing readDiagramDrawing(CTGraphicalObjectFrame shape, XSLFSheet sheet) {
+        CTGraphicalObjectData graphicData = shape.getGraphic().getGraphicData();
+        XmlObject[] children = graphicData.selectChildren(new QName(DRAWINGML_DIAGRAM_URI, "relIds"));
+
+        if (children.length == 0) {
+            return null;
+        }
+
+        // CTRelIds doesn't contain a relationship to the drawing#.xml
+        // But it has the same name as the other data#.xml, layout#.xml, etc.
+        CTRelIds relIds = (CTRelIds) children[0];
+        POIXMLDocumentPart dataModelPart = sheet.getRelationById(relIds.getDm());
+        if (dataModelPart == null) {
+            return null;
+        }
+        String dataPartName = dataModelPart.getPackagePart().getPartName().getName();
+        String drawingPartName = dataPartName.replace("data", "drawing");
+        for (POIXMLDocumentPart.RelationPart rp : sheet.getRelationParts()) {
+            if (drawingPartName.equals(rp.getDocumentPart().getPackagePart().getPartName().getName())) {
+                if (rp.getDocumentPart() instanceof XSLFDiagramDrawing) {
+                    return rp.getDocumentPart();
+                }
+            }
+        }
+        return null;
+    }
+
+    // If the shape has text, two XSLFShapes are created. One shape element and one textbox element.
+    public List<org.openxmlformats.schemas.presentationml.x2006.main.CTShape> convertShape(CTShape msShapeCt, XSLFSheet sheet) {
+        org.openxmlformats.schemas.presentationml.x2006.main.CTShape shapeCt
+                = org.openxmlformats.schemas.presentationml.x2006.main.CTShape.Factory.newInstance();
+
+        // The fields on MS CTShape largely re-use the underlying openxml classes.
+        // We just copy the fields from the MS CTShape to the openxml CTShape
+        shapeCt.setStyle(msShapeCt.getStyle());
+        shapeCt.setSpPr(msShapeCt.getSpPr());
+
+        CTShapeNonVisual nonVisualCt = shapeCt.addNewNvSpPr();
+        nonVisualCt.setCNvPr(msShapeCt.getNvSpPr().getCNvPr());
+        nonVisualCt.setCNvSpPr(msShapeCt.getNvSpPr().getCNvSpPr());
+        nonVisualCt.setNvPr(CTApplicationNonVisualDrawingProps.Factory.newInstance());
+        shapeCt.setNvSpPr(nonVisualCt);
+
+        ArrayList<org.openxmlformats.schemas.presentationml.x2006.main.CTShape> shapes = new ArrayList<>();
+        shapes.add(shapeCt);
+
+        if (hasTextContent(msShapeCt)) {
+            org.openxmlformats.schemas.presentationml.x2006.main.CTShape textShapeCT = convertText(msShapeCt, nonVisualCt);
+            shapes.add(textShapeCT);
+        }
+
+        if (hasBlipEmbed(msShapeCt)) {
+            String embedId = msShapeCt.getSpPr().getBlipFill().getBlip().getEmbed();
+            POIXMLDocumentPart part = _drawing.getRelationById(embedId);
+            if (part != null) {
+                // When reading the blip, POI looks into the `slide#.xml.rels` file. However, the blip relationship is
+                // defined inside `drawing#.xml.rels`. Copy this relationship to the parent.
+                POIXMLDocumentPart.RelationPart updatedRelation = sheet.addRelation(null, XSLFRelation.IMAGES, part);
+                shapeCt.getSpPr().getBlipFill().getBlip().setEmbed(updatedRelation.getRelationship().getId());
+            }
+        }
+
+        return shapes;
+    }
+
+    private org.openxmlformats.schemas.presentationml.x2006.main.CTShape convertText(CTShape msShapeCt, CTShapeNonVisual nonVisualCt) {
+        org.openxmlformats.schemas.presentationml.x2006.main.CTShape textShapeCT
+                = org.openxmlformats.schemas.presentationml.x2006.main.CTShape.Factory.newInstance();
+
+        // SmartArt shapes define a separate `txXfrm` property for the placement of text inside the shape
+        // We can't easily (is it even possible?) set a separate xfrm for the text on the openxml CTShape.
+        // Instead, we create a separate textbox shape with the same xfrm.
+        org.openxmlformats.schemas.drawingml.x2006.main.CTShapeProperties textShapeProps = textShapeCT.addNewSpPr();
+        textShapeProps.setXfrm(msShapeCt.getTxXfrm());
+
+        textShapeCT.setTxBody(msShapeCt.getTxBody());
+        textShapeCT.setStyle(msShapeCt.getStyle());
+        // Create a copy of the nonVisualCt when setting it for the text box.
+        // If we shared the one object, a consumer may be surprised that updating the text shape properties
+        // also updates the parent shape.
+        textShapeCT.setNvSpPr((CTShapeNonVisual) nonVisualCt.copy());
+
+        return textShapeCT;
+    }
+
+    /**
+     * Returns the underlying {@link XSLFDiagramDrawing} used to create this diagram.
+     * <p>
+     * NOTE: Modifying this drawing will not update the groupShape returned from {@link #getGroupShape()}.
+     */
+    public XSLFDiagramDrawing getDiagramDrawing() {
+        return _drawing;
+    }
+
+    private XSLFGroupShape initGroupShape(XSLFSheet sheet) {
+        XSLFDiagramDrawing drawing = getDiagramDrawing();
+        if (drawing == null || drawing.getDrawingDocument() == null) {
+            return null;
+        }
+
+        CTGroupShape msGroupShapeCt = drawing.getDrawingDocument().getDrawing().getSpTree();
+        if (msGroupShapeCt == null || msGroupShapeCt.getSpList().isEmpty()) {
+            return null;
+        }
+        return convertMsGroupToGroupShape(msGroupShapeCt, sheet);
+    }
+
+    /**
+     * Returns the diagram represented as a grouped shape.
+     */
+    public XSLFGroupShape getGroupShape() {
+        return _groupShape;
+    }
+
+    private XSLFGroupShape convertMsGroupToGroupShape(CTGroupShape msGroupShapeCt, XSLFSheet sheet) {
+        org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape groupShapeCt
+                = org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape.Factory.newInstance();
+
+        CTGroupShapeProperties groupShapePropsCt = groupShapeCt.addNewGrpSpPr();
+
+        CTGroupShapeNonVisual groupShapeNonVisualCt = groupShapeCt.addNewNvGrpSpPr();
+        groupShapeNonVisualCt.setCNvPr(msGroupShapeCt.getNvGrpSpPr().getCNvPr());
+        groupShapeNonVisualCt.setCNvGrpSpPr(msGroupShapeCt.getNvGrpSpPr().getCNvGrpSpPr());
+        groupShapeNonVisualCt.setNvPr(CTApplicationNonVisualDrawingProps.Factory.newInstance());
+
+        for (CTShape msShapeCt : msGroupShapeCt.getSpList()) {
+            List<org.openxmlformats.schemas.presentationml.x2006.main.CTShape> shapes = convertShape(msShapeCt, sheet);
+            groupShapeCt.getSpList().addAll(shapes);
+        }
+
+        Rectangle2D anchor = super.getAnchor();
+        Rectangle2D interiorAnchor = new Rectangle2D.Double(0, 0, anchor.getWidth(), anchor.getHeight());
+
+        XSLFGroupShape groupShape = new XSLFGroupShape(groupShapeCt, getSheet());
+        groupShape.setAnchor(anchor);
+        groupShape.setInteriorAnchor(interiorAnchor);
+        groupShape.setRotation(super.getRotation());
+        return groupShape;
+    }
+}
diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagramDrawing.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagramDrawing.java
new file mode 100644 (file)
index 0000000..014d7f2
--- /dev/null
@@ -0,0 +1,55 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.xslf.usermodel;
+
+import com.microsoft.schemas.office.drawing.x2008.diagram.DrawingDocument;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.util.Beta;
+import org.apache.xmlbeans.XmlException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Drawing representation of a SmartArt diagram.
+ */
+@Beta
+public class XSLFDiagramDrawing extends POIXMLDocumentPart {
+
+    private final DrawingDocument _drawingDoc;
+
+    /* package protected */ XSLFDiagramDrawing() {
+        super();
+        _drawingDoc = DrawingDocument.Factory.newInstance();
+    }
+
+    /* package protected */ XSLFDiagramDrawing(PackagePart part) throws XmlException, IOException {
+        super(part);
+        _drawingDoc = readPackagePart(part);
+    }
+
+    private static DrawingDocument readPackagePart(PackagePart part) throws IOException, XmlException {
+        try (InputStream is = part.getInputStream()) {
+            return DrawingDocument.Factory.parse(is);
+        }
+    }
+
+    public DrawingDocument getDrawingDocument() {
+        return _drawingDoc;
+    }
+}
index 745db2bb16e4ec5fa4e7bfef4bf72435bb15449e..0ab72a074c30776282c28d37b41de5071fdb4021 100644 (file)
@@ -53,7 +53,6 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape;
 @Beta
 public class XSLFGraphicFrame extends XSLFShape implements GraphicalFrame<XSLFShape, XSLFTextParagraph> {
     private static final String DRAWINGML_CHART_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart";
-    private static final String DRAWINGML_DIAGRAM_URI = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
     private static final Logger LOG = LogManager.getLogger(XSLFGraphicFrame.class);
 
     /*package*/ XSLFGraphicFrame(CTGraphicalObjectFrame shape, XSLFSheet sheet){
@@ -100,6 +99,8 @@ public class XSLFGraphicFrame extends XSLFShape implements GraphicalFrame<XSLFSh
             return new XSLFTable(shape, sheet);
         case XSLFObjectShape.OLE_URI:
             return new XSLFObjectShape(shape, sheet);
+        case XSLFDiagram.DRAWINGML_DIAGRAM_URI:
+            return new XSLFDiagram(shape, sheet);
         default:
             return new XSLFGraphicFrame(shape, sheet);
         }
@@ -177,7 +178,7 @@ public class XSLFGraphicFrame extends XSLFShape implements GraphicalFrame<XSLFSh
      */
     public boolean hasDiagram() {
         String uri = getGraphicalData().getUri();
-        return uri.equals(DRAWINGML_DIAGRAM_URI);
+        return uri.equals(XSLFDiagram.DRAWINGML_DIAGRAM_URI);
     }
 
     private CTGraphicalObjectData getGraphicalData() {
@@ -211,7 +212,7 @@ public class XSLFGraphicFrame extends XSLFShape implements GraphicalFrame<XSLFSh
 
         CTGraphicalObjectData data = getGraphicalData();
         String uri = data.getUri();
-        if(uri.equals(DRAWINGML_DIAGRAM_URI)){
+        if(uri.equals(XSLFDiagram.DRAWINGML_DIAGRAM_URI)){
             copyDiagram(data, (XSLFGraphicFrame)sh);
         } if(uri.equals(DRAWINGML_CHART_URI)){
             copyChart(data, (XSLFGraphicFrame)sh);
@@ -256,7 +257,7 @@ public class XSLFGraphicFrame extends XSLFShape implements GraphicalFrame<XSLFSh
 
     // TODO should be moved to a sub-class
     private void copyDiagram(CTGraphicalObjectData objData, XSLFGraphicFrame srcShape){
-        String xpath = "declare namespace dgm='" + DRAWINGML_DIAGRAM_URI + "' $this//dgm:relIds";
+        String xpath = "declare namespace dgm='" + XSLFDiagram.DRAWINGML_DIAGRAM_URI + "' $this//dgm:relIds";
         XmlObject[] obj = objData.selectPath(xpath);
         if(obj != null && obj.length == 1) {
             XSLFSheet sheet = srcShape.getSheet();
index 19d0c3d10f66f4cbd40bf4a4ff4e60a544296cbd..c60905b23579d33077cb052055e80fc2ee440ed8 100644 (file)
@@ -150,6 +150,13 @@ public final class XSLFRelation extends POIXMLRelation {
         XSLFChart::new, XSLFChart::new
     );
 
+    public static final XSLFRelation DIAGRAM_DRAWING = new XSLFRelation(
+            "application/vnd.ms-office.drawingml.diagramDrawing+xml",
+            "http://schemas.microsoft.com/office/2007/relationships/diagramDrawing",
+            "/ppt/diagrams/drawing#.xml",
+            XSLFDiagramDrawing::new, XSLFDiagramDrawing::new
+    );
+
     public static final XSLFRelation IMAGE_EMF = new XSLFRelation(
         PictureType.EMF.contentType,
         IMAGE_PART,
diff --git a/poi-ooxml/src/test/java/org/apache/poi/xslf/usermodel/TestXSLFDiagram.java b/poi-ooxml/src/test/java/org/apache/poi/xslf/usermodel/TestXSLFDiagram.java
new file mode 100644 (file)
index 0000000..c3ae9c4
--- /dev/null
@@ -0,0 +1,131 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.xslf.usermodel;
+
+import org.apache.poi.openxml4j.opc.ContentTypes;
+import org.apache.poi.sl.usermodel.ColorStyle;
+import org.apache.poi.sl.usermodel.PaintStyle;
+import org.apache.poi.sl.usermodel.TextParagraph.TextAlign;
+import org.apache.poi.xslf.XSLFTestDataSamples;
+import org.junit.jupiter.api.Test;
+
+import java.awt.Color;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestXSLFDiagram {
+
+    private static final String SIMPLE_DIAGRAM = "smartart-simple.pptx";
+
+    private static List<XSLFDiagram> extractDiagrams(XMLSlideShow slideShow) {
+        return slideShow.getSlides()
+                .stream()
+                .flatMap(s -> s.getShapes().stream())
+                .filter(s -> s instanceof XSLFDiagram)
+                .map(s -> (XSLFDiagram) s)
+                .collect(Collectors.toList());
+    }
+
+    // https://stackoverflow.com/a/3607942
+    private static String colorToHex(Color color) {
+        return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue());
+    }
+
+    private static Color hexToColor(String hex) {
+        return Color.decode(hex);
+    }
+
+    @Test
+    public void testHasDiagram() {
+        XMLSlideShow inputPptx = XSLFTestDataSamples.openSampleDocument(SIMPLE_DIAGRAM);
+        List<XSLFDiagram> diagrams = extractDiagrams(inputPptx);
+        assertEquals(1, diagrams.size());
+
+        XSLFDiagram diagram = diagrams.get(0);
+        assertTrue(diagram.hasDiagram());
+    }
+
+    @Test
+    public void testDiagramContainsShapes() {
+        XMLSlideShow inputPptx = XSLFTestDataSamples.openSampleDocument(SIMPLE_DIAGRAM);
+        List<XSLFDiagram> diagrams = extractDiagrams(inputPptx);
+        assertEquals(1, diagrams.size());
+
+        XSLFDiagram diagram = diagrams.get(0);
+        XSLFGroupShape groupShape = diagram.getGroupShape();
+        assertNotNull(groupShape);
+
+        // The Group gets the same positioning as the SmartArt. This can be much wider/taller than the content inside.
+        assertEquals(groupShape.getAnchor().getWidth(), 113.375, 1E-4);
+        assertEquals(groupShape.getAnchor().getHeight(), 74, 1E-4);
+        assertEquals(groupShape.getAnchor().getX(), -16.75, 1E-4);
+        assertEquals(groupShape.getAnchor().getY(), 5.5, 1E-4);
+
+        List<XSLFShape> shapes = groupShape.getShapes();
+        // 4 shapes, 3 text boxes, one shape does not have any text inside it
+        assertEquals(7, shapes.size());
+
+        // Shape 1 - Yellow Circle - "abc" center aligned
+        String accent4Hex = "#ffc000"; // yellow
+        XSLFAutoShape yellowCircle = (XSLFAutoShape) shapes.get(0);
+        assertTrue(yellowCircle.getText().isEmpty());
+        assertEquals(accent4Hex, colorToHex(yellowCircle.getFillColor()));
+
+        XSLFAutoShape yellowCircleText = (XSLFAutoShape) shapes.get(1);
+        assertEquals(yellowCircleText.getText(), "abc");
+        assertEquals(TextAlign.CENTER, yellowCircleText.getTextParagraphs().get(0).getTextAlign());
+
+        // Shape 2 - Gradient Blue & Purple - "def" left aligned
+        XSLFAutoShape gradientCircle = (XSLFAutoShape) shapes.get(2);
+        assertTrue(gradientCircle.getFillPaint() instanceof PaintStyle.GradientPaint);
+        assertTrue(gradientCircle.getText().isEmpty());
+
+        XSLFAutoShape gradientCircleText = (XSLFAutoShape) shapes.get(3);
+        assertEquals(gradientCircleText.getText(), "def");
+        // Even with left justification, the text is rendered on the right side of the circle because SmartArt defines
+        // a better visual placement for the textbox inside the txXfrm property.
+        assertEquals(1, gradientCircleText.getTextParagraphs().size());
+        XSLFTextParagraph paragraph = gradientCircleText.getTextParagraphs().get(0);
+        assertEquals(TextAlign.LEFT, paragraph.getTextAlign());
+        assertEquals(1, paragraph.getTextRuns().size());
+        XSLFTextRun textRun = paragraph.getTextRuns().get(0);
+        assertTrue(textRun.isBold());
+        assertTrue(textRun.isItalic());
+
+        // Shape 3 - Green Circle with theme color - "ghi" right aligned
+        XSLFAutoShape greenCircle = (XSLFAutoShape) shapes.get(4);
+        ColorStyle greenCircleColorStyle = ((PaintStyle.SolidPaint) greenCircle.getFillPaint()).getSolidColor();
+        // The circle uses the yellow accent4 color but has HSL adjustments that make it green
+        assertEquals(hexToColor(accent4Hex), greenCircleColorStyle.getColor());
+        assertEquals(50004, greenCircleColorStyle.getAlpha()); // 50% transparency
+        assertEquals(6533927, greenCircleColorStyle.getHueOff());
+        assertEquals(6405, greenCircleColorStyle.getLumOff());
+        assertEquals(-27185, greenCircleColorStyle.getSatOff());
+
+        XSLFAutoShape greenCircleText = (XSLFAutoShape) shapes.get(5);
+        assertEquals(greenCircleText.getText(), "ghi");
+        assertEquals(TextAlign.RIGHT, greenCircleText.getTextParagraphs().get(0).getTextAlign());
+
+        // Shape 4 - Circle with Picture Fill - no text
+        XSLFAutoShape pictureShape = (XSLFAutoShape) shapes.get(6);
+        assertTrue(pictureShape.getText().isEmpty());
+        XSLFTexturePaint texturePaint = (XSLFTexturePaint) pictureShape.getFillPaint();
+        assertEquals(ContentTypes.IMAGE_JPEG, texturePaint.getContentType());
+    }
+}
diff --git a/test-data/slideshow/smartart-simple.pptx b/test-data/slideshow/smartart-simple.pptx
new file mode 100644 (file)
index 0000000..22992d8
Binary files /dev/null and b/test-data/slideshow/smartart-simple.pptx differ