diff options
author | PJ Fanning <fanningpj@apache.org> | 2022-07-22 09:55:10 +0000 |
---|---|---|
committer | PJ Fanning <fanningpj@apache.org> | 2022-07-22 09:55:10 +0000 |
commit | cc7fcdfab3aad498c568ac6a562a48bd82df4030 (patch) | |
tree | 1395a4d7c9bacea57020905caeca50b3357d1892 | |
parent | aa7eee178ca62d1006b6ccc771bc1814b9c90acd (diff) | |
download | poi-cc7fcdfab3aad498c568ac6a562a48bd82df4030.tar.gz poi-cc7fcdfab3aad498c568ac6a562a48bd82df4030.zip |
[bug-66176] Integrate SmartArt diagrams from powerpoint presentations. Thanks to Yaseen.
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1902934 13f79535-47bb-0310-9956-ffa450edef68
7 files changed, 578 insertions, 4 deletions
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 index 0000000000..95f9f9add4 --- /dev/null +++ b/poi-examples/src/main/java/org/apache/poi/examples/xslf/SmartArtConversionDemo.java @@ -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 index 0000000000..ee248c0f69 --- /dev/null +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagram.java @@ -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 index 0000000000..014d7f26c3 --- /dev/null +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFDiagramDrawing.java @@ -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; + } +} diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java index 745db2bb16..0ab72a074c 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java @@ -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(); diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFRelation.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFRelation.java index 19d0c3d10f..c60905b235 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFRelation.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFRelation.java @@ -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 index 0000000000..c3ae9c4e0f --- /dev/null +++ b/poi-ooxml/src/test/java/org/apache/poi/xslf/usermodel/TestXSLFDiagram.java @@ -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 Binary files differnew file mode 100644 index 0000000000..22992d8e86 --- /dev/null +++ b/test-data/slideshow/smartart-simple.pptx |