--- /dev/null
+/* ====================================================================
+ 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());
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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;
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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;
+ }
+}
@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){
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);
}
*/
public boolean hasDiagram() {
String uri = getGraphicalData().getUri();
- return uri.equals(DRAWINGML_DIAGRAM_URI);
+ return uri.equals(XSLFDiagram.DRAWINGML_DIAGRAM_URI);
}
private CTGraphicalObjectData getGraphicalData() {
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);
// 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();
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,
--- /dev/null
+/* ====================================================================
+ 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());
+ }
+}