TestXSSFDrawing.java

/* ====================================================================
   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.xssf.usermodel;

import static org.apache.poi.xssf.usermodel.XSSFRelation.NS_DRAWINGML;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.awt.Color;
import java.io.IOException;
import java.util.List;

import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.FontUnderline;
import org.apache.poi.ss.usermodel.ShapeTypes;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.XSSFTestDataSamples;
import org.junit.jupiter.api.Test;
import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupTransform2D;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph;
import org.openxmlformats.schemas.drawingml.x2006.main.STTextUnderlineType;
import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTDrawing;

class TestXSSFDrawing {
    @Test
    void bug54803() throws Exception {
        try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("bug54803.xlsx")) {
            XSSFSheet sheet = wb.getSheetAt(0);
            sheet.createDrawingPatriarch();
            try (XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb)) {
                XSSFSheet sheet2 = wb2.getSheetAt(0);
                assertNotNull(sheet2.getDrawingPatriarch());
            }
        }
    }

    @Test
    void testRead() throws IOException {
        XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithDrawing.xlsx");
        XSSFSheet sheet = wb.getSheetAt(0);
        //the sheet has one relationship and it is XSSFDrawing
        List<RelationPart> rels = sheet.getRelationParts();
        assertEquals(1, rels.size());
        RelationPart rp = rels.get(0);
        assertTrue(rp.getDocumentPart() instanceof XSSFDrawing);

        XSSFDrawing drawing = rp.getDocumentPart();
        //sheet.createDrawingPatriarch() should return the same instance of XSSFDrawing
        assertSame(drawing, sheet.createDrawingPatriarch());
        String drawingId = rp.getRelationship().getId();

        //there should be a relation to this drawing in the worksheet
        assertTrue(sheet.getCTWorksheet().isSetDrawing());
        assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId());

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(6, shapes.size());

        assertTrue(shapes.get(0) instanceof XSSFPicture);
        assertTrue(shapes.get(1) instanceof XSSFPicture);
        assertTrue(shapes.get(2) instanceof XSSFPicture);
        assertTrue(shapes.get(3) instanceof XSSFPicture);
        assertTrue(shapes.get(4) instanceof XSSFSimpleShape);
        assertTrue(shapes.get(5) instanceof XSSFPicture);

        for(XSSFShape sh : shapes) assertNotNull(sh.getAnchor());

        checkRewrite(wb);
        wb.close();
    }

    @Test
    void testNew() throws IOException {
        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        //multiple calls of createDrawingPatriarch should return the same instance of XSSFDrawing
        XSSFDrawing dr1 = sheet.createDrawingPatriarch();
        XSSFDrawing dr2 = sheet.createDrawingPatriarch();
        assertSame(dr1, dr2);

        List<RelationPart> rels = sheet.getRelationParts();
        assertEquals(1, rels.size());
        RelationPart rp = rels.get(0);
        assertTrue(rp.getDocumentPart() instanceof XSSFDrawing);

        XSSFDrawing drawing = rp.getDocumentPart();
        String drawingId = rp.getRelationship().getId();

        //there should be a relation to this drawing in the worksheet
        assertTrue(sheet.getCTWorksheet().isSetDrawing());
        assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId());

        XSSFConnector c1= drawing.createConnector(new XSSFClientAnchor(0,0,0,0,0,0,2,2));
        c1.setLineWidth(2.5);
        c1.setLineStyle(1);

        XSSFShapeGroup c2 = drawing.createGroup(new XSSFClientAnchor(0,0,0,0,0,0,5,5));
        assertNotNull(c2);

        XSSFSimpleShape c3 = drawing.createSimpleShape(new XSSFClientAnchor(0,0,0,0,2,2,3,4));
        c3.setText(new XSSFRichTextString("Test String"));
        c3.setFillColor(128, 128, 128);

        XSSFTextBox c4 = drawing.createTextbox(new XSSFClientAnchor(0,0,0,0,4,4,5,6));
        XSSFRichTextString rt = new XSSFRichTextString("Test String");
        rt.applyFont(0, 5, wb1.createFont());
        rt.applyFont(5, 6, wb1.createFont());
        c4.setText(rt);

        c4.setNoFill(true);
        assertEquals(4, drawing.getCTDrawing().sizeOfTwoCellAnchorArray());

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(4, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFConnector);
        assertTrue(shapes.get(1) instanceof XSSFShapeGroup);
        assertTrue(shapes.get(2) instanceof XSSFSimpleShape);
        assertTrue(shapes.get(3) instanceof XSSFSimpleShape); //

        // Save and re-load it
        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);

        // Check
        dr1 = sheet.createDrawingPatriarch();
        CTDrawing ctDrawing = dr1.getCTDrawing();

        // Connector, shapes and text boxes are all two cell anchors
        assertEquals(0, ctDrawing.sizeOfAbsoluteAnchorArray());
        assertEquals(0, ctDrawing.sizeOfOneCellAnchorArray());
        assertEquals(4, ctDrawing.sizeOfTwoCellAnchorArray());

        shapes = dr1.getShapes();
        assertEquals(4, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFConnector);
        assertTrue(shapes.get(1) instanceof XSSFShapeGroup);
        assertTrue(shapes.get(2) instanceof XSSFSimpleShape);
        assertTrue(shapes.get(3) instanceof XSSFSimpleShape); //

        // Ensure it got the right namespaces
        String xml = ctDrawing.toString();
        assertTrue(xml.contains("xmlns:xdr=\"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing\""));
        assertTrue(xml.contains("xmlns:a=\"" + NS_DRAWINGML + '\"'));

        checkRewrite(wb2);
        wb2.close();
    }

    @Test
    void testMultipleDrawings() throws IOException{
        XSSFWorkbook wb = new XSSFWorkbook();
        for (int i = 0; i < 3; i++) {
            XSSFSheet sheet = wb.createSheet();
            XSSFDrawing drawing = sheet.createDrawingPatriarch();
            assertNotNull(drawing);
        }
        try (OPCPackage pkg = wb.getPackage()) {
            assertEquals(3, pkg.getPartsByContentType(XSSFRelation.DRAWINGS.getContentType()).size());
            checkRewrite(wb);
        }
        wb.close();
    }

    @Test
    void testClone() throws Exception{
        XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithDrawing.xlsx");
        XSSFSheet sheet1 = wb.getSheetAt(0);

        XSSFSheet sheet2 = wb.cloneSheet(0);

        //the source sheet has one relationship and it is XSSFDrawing
        List<POIXMLDocumentPart> rels1 = sheet1.getRelations();
        assertEquals(1, rels1.size());
        assertTrue(rels1.get(0) instanceof XSSFDrawing);

        List<POIXMLDocumentPart> rels2 = sheet2.getRelations();
        assertEquals(1, rels2.size());
        assertTrue(rels2.get(0) instanceof XSSFDrawing);

        XSSFDrawing drawing1 = (XSSFDrawing)rels1.get(0);
        XSSFDrawing drawing2 = (XSSFDrawing)rels2.get(0);
        assertNotSame(drawing1, drawing2);  // drawing2 is a clone of drawing1

        List<XSSFShape> shapes1 = drawing1.getShapes();
        List<XSSFShape> shapes2 = drawing2.getShapes();
        assertEquals(shapes1.size(), shapes2.size());

        for(int i = 0; i < shapes1.size(); i++){
            XSSFShape sh1 = shapes1.get(i);
            XSSFShape sh2 = shapes2.get(i);

            assertSame(sh1.getClass(), sh2.getClass());
            assertEquals(sh1.getShapeProperties().toString(), sh2.getShapeProperties().toString());
        }

        checkRewrite(wb);
        wb.close();
    }

    /**
     * ensure that rich text attributes defined in a XSSFRichTextString
     * are passed to XSSFSimpleShape.
     *
     * See Bugzilla 52219.
     */
    @Test
    void testRichText() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));
        XSSFRichTextString rt = new XSSFRichTextString("Test String");

        XSSFFont font = wb.createFont();
        font.setColor(new XSSFColor(new Color(0, 128, 128), wb.getStylesSource().getIndexedColors()));
        font.setItalic(true);
        font.setBold(true);
        font.setUnderline(FontUnderline.SINGLE);
        rt.applyFont(font);

        shape.setText(rt);

        CTTextParagraph pr = shape.getCTShape().getTxBody().getPArray(0);
        assertEquals(1, pr.sizeOfRArray());

        CTTextCharacterProperties rPr = pr.getRArray(0).getRPr();
        assertTrue(rPr.getB());
        assertTrue(rPr.getI());
        assertEquals(STTextUnderlineType.SNG, rPr.getU());
        assertArrayEquals(
                new byte[]{-1, 0, (byte)128, (byte)128} ,
                rPr.getSolidFill().getSrgbClr().getVal());

        checkRewrite(wb);
        wb.close();
    }

    /**
     *  test that anchor is not null when reading shapes from existing drawings
     */
    @Test
    void testReadAnchors() throws IOException {
        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFClientAnchor anchor1 = new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4);
        XSSFShape shape1 = drawing.createTextbox(anchor1);
        assertNotNull(shape1);

        XSSFClientAnchor anchor2 = new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 5);
        XSSFShape shape2 = drawing.createTextbox(anchor2);
        assertNotNull(shape2);

        int pictureIndex= wb1.addPicture(new byte[]{}, XSSFWorkbook.PICTURE_TYPE_PNG);
        XSSFClientAnchor anchor3 = new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 6);
        XSSFShape shape3 = drawing.createPicture(anchor3, pictureIndex);
        assertNotNull(shape3);

        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);
        drawing = sheet.createDrawingPatriarch();
        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(shapes.get(0).getAnchor(), anchor1);
        assertEquals(shapes.get(1).getAnchor(), anchor2);
        assertEquals(shapes.get(2).getAnchor(), anchor3);

        checkRewrite(wb2);
        wb2.close();
    }

    /**
     * ensure that font and color rich text attributes defined in a XSSFRichTextString
     * are passed to XSSFSimpleShape.
     *
     * See Bugzilla 54969.
     */
    @Test
    void testRichTextFontAndColor() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));
        XSSFRichTextString rt = new XSSFRichTextString("Test String");

        XSSFFont font = wb.createFont();
        font.setColor(new XSSFColor(new Color(0, 128, 128), wb.getStylesSource().getIndexedColors()));
        font.setFontName("Arial");
        rt.applyFont(font);

        shape.setText(rt);

        CTTextParagraph pr = shape.getCTShape().getTxBody().getPArray(0);
        assertEquals(1, pr.sizeOfRArray());

        CTTextCharacterProperties rPr = pr.getRArray(0).getRPr();
        assertEquals("Arial", rPr.getLatin().getTypeface());
        assertArrayEquals(
                new byte[]{-1, 0, (byte)128, (byte)128} ,
                rPr.getSolidFill().getSrgbClr().getVal());
        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test setText single paragraph to ensure backwards compatibility
     */
    @Test
    void testSetTextSingleParagraph() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));
        XSSFRichTextString rt = new XSSFRichTextString("Test String");

        XSSFFont font = wb.createFont();
        font.setColor(new XSSFColor(new Color(0, 255, 255), wb.getStylesSource().getIndexedColors()));
        font.setFontName("Arial");
        rt.applyFont(font);

        shape.setText(rt);

        List<XSSFTextParagraph> paras = shape.getTextParagraphs();
        assertEquals(1, paras.size());
        assertEquals("Test String", paras.get(0).getText());

        List<XSSFTextRun> runs = paras.get(0).getTextRuns();
        assertEquals(1, runs.size());
        assertEquals("Arial", runs.get(0).getFontFamily());

        Color clr = runs.get(0).getFontColor();
        assertArrayEquals(
                new int[] { 0, 255, 255 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test addNewTextParagraph
     */
    @Test
    void testAddNewTextParagraph() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));

        XSSFTextParagraph para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 1");

        List<XSSFTextParagraph> paras = shape.getTextParagraphs();
        assertEquals(2, paras.size());  // this should be 2 as XSSFSimpleShape creates a default paragraph (no text), and then we add a string to that.

        List<XSSFTextRun> runs = para.getTextRuns();
        assertEquals(1, runs.size());
        assertEquals("Line 1", runs.get(0).getText());

        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test addNewTextParagraph using RichTextString
     */
    @Test
    void testAddNewTextParagraphWithRTS() throws IOException {
        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));
        XSSFRichTextString rt = new XSSFRichTextString("Test Rich Text String");

        XSSFFont font = wb1.createFont();
        font.setColor(new XSSFColor(new Color(0, 255, 255), wb1.getStylesSource().getIndexedColors()));
        font.setFontName("Arial");
        rt.applyFont(font);

        XSSFFont midfont = wb1.createFont();
        midfont.setColor(new XSSFColor(new Color(0, 255, 0), wb1.getStylesSource().getIndexedColors()));
        rt.applyFont(5, 14, midfont);   // set the text "Rich Text" to be green and the default font

        XSSFTextParagraph para = shape.addNewTextParagraph(rt);

        // Save and re-load it
        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);

        // Check
        drawing = sheet.createDrawingPatriarch();

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(1, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);

        XSSFSimpleShape sshape = (XSSFSimpleShape) shapes.get(0);

        List<XSSFTextParagraph> paras = sshape.getTextParagraphs();
        assertEquals(2, paras.size());  // this should be 2 as XSSFSimpleShape creates a default paragraph (no text), and then we add a string to that.

        List<XSSFTextRun> runs = para.getTextRuns();
        assertEquals(3, runs.size());

        // first run properties
        assertEquals("Test ", runs.get(0).getText());
        assertEquals("Arial", runs.get(0).getFontFamily());

        Color clr = runs.get(0).getFontColor();
        assertArrayEquals(
                new int[] { 0, 255, 255 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        // second run properties
        assertEquals("Rich Text", runs.get(1).getText());
        assertEquals(XSSFFont.DEFAULT_FONT_NAME, runs.get(1).getFontFamily());

        clr = runs.get(1).getFontColor();
        assertArrayEquals(
                new int[] { 0, 255, 0 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        // third run properties
        assertEquals(" String", runs.get(2).getText());
        assertEquals("Arial", runs.get(2).getFontFamily());
        clr = runs.get(2).getFontColor();
        assertArrayEquals(
                new int[] { 0, 255, 255 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        checkRewrite(wb2);
        wb2.close();
    }

    /**
     * Test add multiple paragraphs and retrieve text
     */
    @Test
    void testAddMultipleParagraphs() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));

        XSSFTextParagraph para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 1");

        para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 2");

        para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 3");

        List<XSSFTextParagraph> paras = shape.getTextParagraphs();
        assertEquals(4, paras.size());  // this should be 4 as XSSFSimpleShape creates a default paragraph (no text), and then we added 3 paragraphs
        assertEquals("Line 1\nLine 2\nLine 3", shape.getText());

        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test setting the text, then adding multiple paragraphs and retrieve text
     */
    @Test
    void testSetAddMultipleParagraphs() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 3, 4));

        shape.setText("Line 1");

        XSSFTextParagraph para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 2");

        para = shape.addNewTextParagraph();
        para.addNewTextRun().setText("Line 3");

        List<XSSFTextParagraph> paras = shape.getTextParagraphs();
        assertEquals(3, paras.size());  // this should be 3 as we overwrote the default paragraph with setText, then added 2 new paragraphs
        assertEquals("Line 1\nLine 2\nLine 3", shape.getText());

        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test reading text from a textbox in an existing file
     */
    @Test
    void testReadTextBox() throws IOException {
        XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithDrawing.xlsx");
        XSSFSheet sheet = wb.getSheetAt(0);
        //the sheet has one relationship and it is XSSFDrawing
        List<RelationPart> rels = sheet.getRelationParts();
        assertEquals(1, rels.size());
        RelationPart rp = rels.get(0);
        assertTrue(rp.getDocumentPart() instanceof XSSFDrawing);

        XSSFDrawing drawing = rp.getDocumentPart();
        //sheet.createDrawingPatriarch() should return the same instance of XSSFDrawing
        assertSame(drawing, sheet.createDrawingPatriarch());
        String drawingId = rp.getRelationship().getId();

        //there should be a relation to this drawing in the worksheet
        assertTrue(sheet.getCTWorksheet().isSetDrawing());
        assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId());

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(6, shapes.size());

        assertTrue(shapes.get(4) instanceof XSSFSimpleShape);

        XSSFSimpleShape textbox = (XSSFSimpleShape) shapes.get(4);
        assertEquals("Sheet with various pictures\n(jpeg, png, wmf, emf and pict)", textbox.getText());

        checkRewrite(wb);
        wb.close();
    }


    /**
     * Test reading multiple paragraphs from a textbox in an existing file
     */
    @Test
    void testReadTextBoxParagraphs() throws IOException {
        XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithTextBox.xlsx");
        XSSFSheet sheet = wb.getSheetAt(0);
        //the sheet has one relationship and it is XSSFDrawing
        List<RelationPart> rels = sheet.getRelationParts();
        assertEquals(1, rels.size());
        RelationPart rp = rels.get(0);

        assertTrue(rp.getDocumentPart() instanceof XSSFDrawing);

        XSSFDrawing drawing = rp.getDocumentPart();

        //sheet.createDrawingPatriarch() should return the same instance of XSSFDrawing
        assertSame(drawing, sheet.createDrawingPatriarch());
        String drawingId = rp.getRelationship().getId();

        //there should be a relation to this drawing in the worksheet
        assertTrue(sheet.getCTWorksheet().isSetDrawing());
        assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId());

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(1, shapes.size());

        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);

        XSSFSimpleShape textbox = (XSSFSimpleShape) shapes.get(0);

        List<XSSFTextParagraph> paras = textbox.getTextParagraphs();
        assertEquals(3, paras.size());

        assertEquals("Line 2", paras.get(1).getText()); // check content of second paragraph

        assertEquals("Line 1\nLine 2\nLine 3", textbox.getText());  // check content of entire textbox

        // check attributes of paragraphs
        assertEquals(TextAlign.LEFT, paras.get(0).getTextAlign());
        assertEquals(TextAlign.CENTER, paras.get(1).getTextAlign());
        assertEquals(TextAlign.RIGHT, paras.get(2).getTextAlign());

        Color clr = paras.get(0).getTextRuns().get(0).getFontColor();
        assertArrayEquals(
                new int[] { 255, 0, 0 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        clr = paras.get(1).getTextRuns().get(0).getFontColor();
        assertArrayEquals(
                new int[] { 0, 255, 0 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        clr = paras.get(2).getTextRuns().get(0).getFontColor();
        assertArrayEquals(
                new int[] { 0, 0, 255 } ,
                new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() });

        checkRewrite(wb);
        wb.close();
    }

    /**
     * Test adding and reading back paragraphs as bullet points
     */
    @Test
    void testAddBulletParagraphs() throws IOException {

        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFTextBox shape = drawing.createTextbox(new XSSFClientAnchor(0, 0, 0, 0, 2, 2, 10, 20));

        String paraString1 = "A normal paragraph";
        String paraString2 = "First bullet";
        String paraString3 = "Second bullet (level 1)";
        String paraString4 = "Third bullet";
        String paraString5 = "Another normal paragraph";
        String paraString6 = "First numbered bullet";
        String paraString7 = "Second bullet (level 1)";
        String paraString8 = "Third bullet (level 1)";
        String paraString9 = "Fourth bullet (level 1)";
        String paraString10 = "Fifth Bullet";

        XSSFTextParagraph para = shape.addNewTextParagraph(paraString1);
        assertNotNull(para);
        para = shape.addNewTextParagraph(paraString2);
        para.setBullet(true);

        para = shape.addNewTextParagraph(paraString3);
        para.setBullet(true);
        para.setLevel(1);

        para = shape.addNewTextParagraph(paraString4);
        para.setBullet(true);

        para = shape.addNewTextParagraph(paraString5);
        assertNotNull(para);
        para = shape.addNewTextParagraph(paraString6);
        para.setBullet(ListAutoNumber.ARABIC_PERIOD);

        para = shape.addNewTextParagraph(paraString7);
        para.setBullet(ListAutoNumber.ARABIC_PERIOD, 3);
        para.setLevel(1);

        para = shape.addNewTextParagraph(paraString8);
        para.setBullet(ListAutoNumber.ARABIC_PERIOD, 3);
        para.setLevel(1);

        para = shape.addNewTextParagraph("");
        para.setBullet(ListAutoNumber.ARABIC_PERIOD, 3);
        para.setLevel(1);

        para = shape.addNewTextParagraph(paraString9);
        para.setBullet(ListAutoNumber.ARABIC_PERIOD, 3);
        para.setLevel(1);

        para = shape.addNewTextParagraph(paraString10);
        para.setBullet(ListAutoNumber.ARABIC_PERIOD);

        // Save and re-load it
        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);

        // Check
        drawing = sheet.createDrawingPatriarch();

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(1, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);

        XSSFSimpleShape sshape = (XSSFSimpleShape) shapes.get(0);

        List<XSSFTextParagraph> paras = sshape.getTextParagraphs();
        assertEquals(12, paras.size());  // this should be 12 as XSSFSimpleShape creates a default paragraph (no text), and then we added to that

        String builder =
                paraString1 +
                "\n" +
                "\u2022 " +
                paraString2 +
                "\n" +
                "\t\u2022 " +
                paraString3 +
                "\n" +
                "\u2022 " +
                paraString4 +
                "\n" +
                paraString5 +
                "\n" +
                "1. " +
                paraString6 +
                "\n" +
                "\t3. " +
                paraString7 +
                "\n" +
                "\t4. " +
                paraString8 +
                "\n" +
                "\t" +   // should be empty
                "\n" +
                "\t5. " +
                paraString9 +
                "\n" +
                "2. " +
                paraString10;

        assertEquals(builder, sshape.getText());

        checkRewrite(wb2);
        wb2.close();
    }

    /**
     * Test reading bullet numbering from a textbox in an existing file
     */
    @Test
    void testReadTextBox2() throws IOException {
        XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithTextBox2.xlsx");
        XSSFSheet sheet = wb.getSheetAt(0);
        XSSFDrawing drawing = sheet.createDrawingPatriarch();
        List<XSSFShape> shapes = drawing.getShapes();
        XSSFSimpleShape textbox = (XSSFSimpleShape) shapes.get(0);
        String extracted = textbox.getText();
        String sb =
                "1. content1A\n" +
                "\t1. content1B\n" +
                "\t2. content2B\n" +
                "\t3. content3B\n" +
                "2. content2A\n" +
                "\t3. content2BStartAt3\n" +
                "\t\n\t\n\t" +
                "4. content2BStartAt3Incremented\n" +
                "\t\n\t\n\t\n\t";

        assertEquals(sb, extracted);
        checkRewrite(wb);
        wb.close();
    }

    @Test
    void testXSSFSimpleShapeCausesNPE56514() throws IOException {
        XSSFWorkbook wb1 = XSSFTestDataSamples.openSampleWorkbook("56514.xlsx");
        XSSFSheet sheet = wb1.getSheetAt(0);
        XSSFDrawing drawing = sheet.createDrawingPatriarch();
        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(4, shapes.size());

        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);
        drawing = sheet.createDrawingPatriarch();

        shapes = drawing.getShapes();
        assertEquals(4, shapes.size());
        wb2.close();
        }

    @Test
    void testXSSFSAddPicture() throws Exception {
        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        //multiple calls of createDrawingPatriarch should return the same instance of XSSFDrawing
        XSSFDrawing dr1 = sheet.createDrawingPatriarch();
        XSSFDrawing dr2 = sheet.createDrawingPatriarch();
        assertSame(dr1, dr2);

        List<RelationPart> rels = sheet.getRelationParts();
        assertEquals(1, rels.size());
        RelationPart rp = rels.get(0);
        assertTrue(rp.getDocumentPart() instanceof XSSFDrawing);

        assertEquals(0, rp.getDocumentPart().getRelations().size());

        XSSFDrawing drawing = rp.getDocumentPart();
        String drawingId = rp.getRelationship().getId();

        //there should be a relation to this drawing in the worksheet
        assertTrue(sheet.getCTWorksheet().isSetDrawing());
        assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId());

        byte[] pictureData = HSSFTestDataSamples.getTestDataFileContent("45829.png");

        ClientAnchor anchor = wb1.getCreationHelper().createClientAnchor();
        anchor.setCol1(1);
        anchor.setRow1(1);

        drawing.createPicture(anchor, wb1.addPicture(pictureData, Workbook.PICTURE_TYPE_JPEG));
        final int pictureIndex = wb1.addPicture(pictureData, Workbook.PICTURE_TYPE_JPEG);
        drawing.createPicture(anchor, pictureIndex);
        drawing.createPicture(anchor, pictureIndex);

        // repeated additions of same share package relationship
        assertEquals(2, rp.getDocumentPart().getPackagePart().getRelationships().size());

        List<XSSFShape> shapes = drawing.getShapes();
        assertEquals(3, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFPicture);
        assertTrue(shapes.get(1) instanceof XSSFPicture);

        // Save and re-load it
        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();
        sheet = wb2.getSheetAt(0);

        // Check
        dr1 = sheet.createDrawingPatriarch();
        CTDrawing ctDrawing = dr1.getCTDrawing();

        // Connector, shapes and text boxes are all two cell anchors
        assertEquals(0, ctDrawing.sizeOfAbsoluteAnchorArray());
        assertEquals(0, ctDrawing.sizeOfOneCellAnchorArray());
        assertEquals(3, ctDrawing.sizeOfTwoCellAnchorArray());

        shapes = dr1.getShapes();
        assertEquals(3, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFPicture);
        assertTrue(shapes.get(1) instanceof XSSFPicture);

        checkRewrite(wb2);
        wb2.close();
    }

    @Test
    void testBug56835CellComment() throws IOException {
        try (XSSFWorkbook wb = new XSSFWorkbook()) {
            XSSFSheet sheet = wb.createSheet();
            XSSFDrawing drawing = sheet.createDrawingPatriarch();

            // first comment works
            ClientAnchor anchor = new XSSFClientAnchor(1, 1, 2, 2, 3, 3, 4, 4);
            XSSFComment comment = drawing.createCellComment(anchor);
            assertNotNull(comment);

            // Should fail if we try to add the same comment for the same cell
            assertThrows(IllegalArgumentException.class, () -> drawing.createCellComment(anchor));
        }
    }

    @Test
    void testGroupShape() throws Exception {
        XSSFWorkbook wb1 = new XSSFWorkbook();
        XSSFSheet sheet = wb1.createSheet();
        XSSFDrawing drawing = sheet.createDrawingPatriarch();

        XSSFSimpleShape s0 = drawing.createSimpleShape(drawing.createAnchor(0, 0, Units.pixelToEMU(30), Units.pixelToEMU(30), 1, 1, 10, 10));
        s0.setShapeType(ShapeTypes.RECT);
        s0.setLineStyleColor(100, 0, 0);

        XSSFShapeGroup g1 = drawing.createGroup(drawing.createAnchor(0, 0, 300, 300, 1, 1, 10, 10));
        CTGroupTransform2D xfrmG1 = g1.getCTGroupShape().getGrpSpPr().getXfrm();

        XSSFSimpleShape s1 = g1.createSimpleShape(new XSSFChildAnchor(
            (int)(xfrmG1.getChExt().getCx()*0.1),
            (int)(xfrmG1.getChExt().getCy()*0.1),
            (int)(xfrmG1.getChExt().getCx()*0.9),
            (int)(xfrmG1.getChExt().getCy()*0.9)
        ));
        s1.setShapeType(ShapeTypes.RECT);
        s1.setLineStyleColor(0, 100, 0);

        XSSFShapeGroup g2 = g1.createGroup(new XSSFChildAnchor(
            (int)(xfrmG1.getChExt().getCx()*0.2),
            (int)(xfrmG1.getChExt().getCy()*0.2),
            (int)(xfrmG1.getChExt().getCx()*0.8),
            (int)(xfrmG1.getChExt().getCy()*0.8)
        ));
        CTGroupTransform2D xfrmG2 = g2.getCTGroupShape().getGrpSpPr().getXfrm();

        XSSFSimpleShape s2 = g2.createSimpleShape(new XSSFChildAnchor(
            (int)(xfrmG2.getChExt().getCx()*0.1),
            (int)(xfrmG2.getChExt().getCy()*0.1),
            (int)(xfrmG2.getChExt().getCx()*0.9),
            (int)(xfrmG2.getChExt().getCy()*0.9)
        ));
        s2.setShapeType(ShapeTypes.RECT);
        s2.setLineStyleColor(0, 0, 100);

        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb1);
        wb1.close();

        XSSFDrawing draw = wb2.getSheetAt(0).getDrawingPatriarch();
        List<XSSFShape> shapes = draw.getShapes();
        assertEquals(2, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);
        assertTrue(shapes.get(1) instanceof XSSFShapeGroup);
        shapes = draw.getShapes((XSSFShapeGroup)shapes.get(1));
        assertEquals(2, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);
        assertTrue(shapes.get(1) instanceof XSSFShapeGroup);
        shapes = draw.getShapes((XSSFShapeGroup)shapes.get(1));
        assertEquals(1, shapes.size());
        assertTrue(shapes.get(0) instanceof XSSFSimpleShape);

        wb2.close();
    }

    @Test
    void testBug63901() throws IOException {
        try (XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("chartTitle_withTitle.xlsx")) {
            XSSFSheet sheet = workbook.getSheetAt(0);
            XSSFDrawing drawing = sheet.getDrawingPatriarch();
            assertEquals(1, drawing.getCharts().size());

            XSSFWorkbook workbook2 = new XSSFWorkbook();
            XSSFSheet sheet2 = workbook2.createSheet();
            XSSFDrawing drawing2 = sheet2.createDrawingPatriarch();

            drawing.getCharts().forEach(drawing2::importChart);
            assertEquals(1, drawing2.getCharts().size());
        }
    }

    private static void checkRewrite(XSSFWorkbook wb) throws IOException {
        XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb);
        assertNotNull(wb2);
        wb2.close();
    }
}