From 09555c446b384e3117432bf645f54832e91c9fb7 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Wed, 31 Jul 2013 00:19:07 +0000 Subject: [PATCH] POI-55292 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1508691 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/xssf/usermodel/XSSFSimpleShape.java | 727 ++++++++++++++++-- .../poi/xssf/usermodel/TestXSSFDrawing.java | 426 ++++++++++ test-data/spreadsheet/WithTextBox.xlsx | Bin 12118 -> 9661 bytes test-data/spreadsheet/WithTextBox2.xlsx | Bin 0 -> 10129 bytes 4 files changed, 1110 insertions(+), 43 deletions(-) create mode 100644 test-data/spreadsheet/WithTextBox2.xlsx diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSimpleShape.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSimpleShape.java index 62ce22486..d24bd01e6 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSimpleShape.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSimpleShape.java @@ -17,6 +17,11 @@ package org.apache.poi.xssf.usermodel; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + + import org.apache.poi.hssf.util.HSSFColor; import org.openxmlformats.schemas.drawingml.x2006.main.*; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTShape; @@ -24,15 +29,19 @@ import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTShapeNonV import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRElt; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRPrElt; import org.apache.poi.util.Internal; +import org.apache.poi.util.Units; +import org.apache.poi.ss.usermodel.VerticalAlignment; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STUnderlineValues; /** * Represents a shape with a predefined geometry in a SpreadsheetML drawing. * Possible shape types are defined in {@link org.apache.poi.ss.usermodel.ShapeTypes} - * - * @author Yegor Kozlov */ -public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable superclass +public class XSSFSimpleShape extends XSSFShape implements Iterable { // TODO - instantiable superclass + /** + * List of the paragraphs that make up the text in this shape + */ + private final List _paragraphs; /** * A default instance of CTShape used for creating new shapes. */ @@ -46,6 +55,15 @@ public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable supercla protected XSSFSimpleShape(XSSFDrawing drawing, CTShape ctShape) { this.drawing = drawing; this.ctShape = ctShape; + + _paragraphs = new ArrayList(); + + // initialize any existing paragraphs - this will be the default body paragraph in a new shape, + // or existing paragraphs that have been loaded from the file + CTTextBody body = ctShape.getTxBody(); + for(int i = 0; i < body.sizeOfPArray(); i++) { + _paragraphs.add(new XSSFTextParagraph(body.getPArray(i), ctShape)); + } } /** @@ -74,34 +92,18 @@ public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable supercla geom.setPrst(STShapeType.RECT); geom.addNewAvLst(); - CTShapeStyle style = shape.addNewStyle(); - CTSchemeColor scheme = style.addNewLnRef().addNewSchemeClr(); - scheme.setVal(STSchemeColorVal.ACCENT_1); - scheme.addNewShade().setVal(50000); - style.getLnRef().setIdx(2); - - CTStyleMatrixReference fillref = style.addNewFillRef(); - fillref.setIdx(1); - fillref.addNewSchemeClr().setVal(STSchemeColorVal.ACCENT_1); - - CTStyleMatrixReference effectRef = style.addNewEffectRef(); - effectRef.setIdx(0); - effectRef.addNewSchemeClr().setVal(STSchemeColorVal.ACCENT_1); - - CTFontReference fontRef = style.addNewFontRef(); - fontRef.setIdx(STFontCollectionIndex.MINOR); - fontRef.addNewSchemeClr().setVal(STSchemeColorVal.LT_1); - CTTextBody body = shape.addNewTxBody(); CTTextBodyProperties bodypr = body.addNewBodyPr(); - bodypr.setAnchor(STTextAnchoringType.CTR); + bodypr.setAnchor(STTextAnchoringType.T); bodypr.setRtlCol(false); CTTextParagraph p = body.addNewP(); - p.addNewPPr().setAlgn(STTextAlignType.CTR); + p.addNewPPr().setAlgn(STTextAlignType.L); CTTextCharacterProperties endPr = p.addNewEndParaRPr(); endPr.setLang("en-US"); - endPr.setSz(1100); - + endPr.setSz(1100); + CTSolidColorFillProperties scfpr = endPr.addNewSolidFill(); + scfpr.addNewSrgbClr().setVal(new byte[] { 0, 0, 0 }); + body.addNewLstStyle(); prototype = shape; @@ -114,30 +116,255 @@ public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable supercla return ctShape; } - /** - * Gets the shape type, one of the constants defined in {@link org.apache.poi.ss.usermodel.ShapeTypes}. - * - * @return the shape type - * @see org.apache.poi.ss.usermodel.ShapeTypes - */ - public int getShapeType() { - return ctShape.getSpPr().getPrstGeom().getPrst().intValue(); + + public Iterator iterator(){ + return _paragraphs.iterator(); } /** - * Sets the shape types. - * - * @param type the shape type, one of the constants defined in {@link org.apache.poi.ss.usermodel.ShapeTypes}. - * @see org.apache.poi.ss.usermodel.ShapeTypes + * Returns the text from all paragraphs in the shape. Paragraphs are separated by new lines. + * + * @return text contained within this shape or empty string */ - public void setShapeType(int type) { - ctShape.getSpPr().getPrstGeom().setPrst(STShapeType.Enum.forInt(type)); + public String getText() { + final int MAX_LEVELS = 9; + StringBuilder out = new StringBuilder(); + List levelCount = new ArrayList(MAX_LEVELS); // maximum 9 levels + XSSFTextParagraph p = null; + + // initialise the levelCount array - this maintains a record of the numbering to be used at each level + for (int k = 0; k < MAX_LEVELS; k++){ + levelCount.add(0); + } + + for(int i = 0; i < _paragraphs.size(); i++) { + if (out.length() > 0) out.append('\n'); + p = _paragraphs.get(i); + + if(p.isBullet() && p.getText().length() > 0){ + + int level = Math.min(p.getLevel(), MAX_LEVELS - 1); + + if(p.isBulletAutoNumber()){ + i = processAutoNumGroup(i, level, levelCount, out); + } else { + // indent appropriately for the level + for(int j = 0; j < level; j++){ + out.append('\t'); + } + String character = p.getBulletCharacter(); + out.append(character.length() > 0 ? character + " " : "- "); + out.append(p.getText()); + } + } else { + out.append(p.getText()); + + // this paragraph is not a bullet, so reset the count array + for (int k = 0; k < MAX_LEVELS; k++){ + levelCount.set(k, 0); + } + } + } + + return out.toString(); } - protected CTShapeProperties getShapeProperties(){ - return ctShape.getSpPr(); + /** + * + */ + private int processAutoNumGroup(int index, int level, List levelCount, StringBuilder out){ + XSSFTextParagraph p = null; + XSSFTextParagraph nextp = null; + ListAutoNumber scheme, nextScheme; + int startAt, nextStartAt; + + p = _paragraphs.get(index); + + // The rules for generating the auto numbers are as follows. If the following paragraph is also + // an auto-number, has the same type/scheme (and startAt if defined on this paragraph) then they are + // considered part of the same group. An empty bullet paragraph is counted as part of the same + // group but does not increment the count for the group. A change of type, startAt or the paragraph + // not being a bullet resets the count for that level to 1. + + // first auto-number paragraph so initialise to 1 or the bullets startAt if present + startAt = p.getBulletAutoNumberStart(); + scheme = p.getBulletAutoNumberScheme(); + if(levelCount.get(level) == 0) { + levelCount.set(level, startAt == 0 ? 1 : startAt); + } + // indent appropriately for the level + for(int j = 0; j < level; j++){ + out.append('\t'); + } + if (p.getText().length() > 0){ + out.append(getBulletPrefix(scheme, levelCount.get(level))); + out.append(p.getText()); + } + while(true) { + nextp = (index + 1) == _paragraphs.size() ? null : _paragraphs.get(index + 1); + if(nextp == null) break; // out of paragraphs + if(!(nextp.isBullet() && p.isBulletAutoNumber())) break; // not an auto-number bullet + if(nextp.getLevel() > level) { + // recurse into the new level group + if (out.length() > 0) out.append('\n'); + index = processAutoNumGroup(index + 1, nextp.getLevel(), levelCount, out); + continue; // restart the loop given the new index + } else if(nextp.getLevel() < level) { + break; // changed level + } + nextScheme = nextp.getBulletAutoNumberScheme(); + nextStartAt = nextp.getBulletAutoNumberStart(); + + if(nextScheme == scheme && nextStartAt == startAt) { + // bullet is valid, so increment i + ++index; + if (out.length() > 0) out.append('\n'); + // indent for the level + for(int j = 0; j < level; j++){ + out.append('\t'); + } + // check for empty text - only output a bullet if there is text, but it is still part of the group + if(nextp.getText().length() > 0) { + // increment the count for this level + levelCount.set(level, levelCount.get(level) + 1); + out.append(getBulletPrefix(nextScheme, levelCount.get(level))); + out.append(nextp.getText()); + } + } else { + // something doesn't match so stop + break; + } + } + // end of the group so reset the count for this level + levelCount.set(level, 0); + + return index; + } + /** + * Returns a string containing an appropriate prefix for an auto-numbering bullet + * @param scheme the auto-numbering scheme used by the bullet + * @param value the value of the bullet + * @return + */ + private String getBulletPrefix(ListAutoNumber scheme, int value){ + StringBuilder out = new StringBuilder(); + + switch(scheme) { + case ALPHA_LC_PARENT_BOTH: + case ALPHA_LC_PARENT_R: + if(scheme == ListAutoNumber.ALPHA_LC_PARENT_BOTH) out.append('('); + out.append(valueToAlpha(value).toLowerCase()); + out.append(')'); + break; + case ALPHA_UC_PARENT_BOTH: + case ALPHA_UC_PARENT_R: + if(scheme == ListAutoNumber.ALPHA_UC_PARENT_BOTH) out.append('('); + out.append(valueToAlpha(value)); + out.append(')'); + break; + case ALPHA_LC_PERIOD: + out.append(valueToAlpha(value).toLowerCase()); + out.append('.'); + break; + case ALPHA_UC_PERIOD: + out.append(valueToAlpha(value)); + out.append('.'); + break; + case ARABIC_PARENT_BOTH: + case ARABIC_PARENT_R: + if(scheme == ListAutoNumber.ARABIC_PARENT_BOTH) out.append('('); + out.append(value); + out.append(')'); + break; + case ARABIC_PERIOD: + out.append(value); + out.append('.'); + break; + case ARABIC_PLAIN: + out.append(value); + break; + case ROMAN_LC_PARENT_BOTH: + case ROMAN_LC_PARENT_R: + if(scheme == ListAutoNumber.ROMAN_LC_PARENT_BOTH) out.append('('); + out.append(valueToRoman(value).toLowerCase()); + out.append(')'); + break; + case ROMAN_UC_PARENT_BOTH: + case ROMAN_UC_PARENT_R: + if(scheme == ListAutoNumber.ROMAN_UC_PARENT_BOTH) out.append('('); + out.append(valueToRoman(value)); + out.append(')'); + break; + case ROMAN_LC_PERIOD: + out.append(valueToRoman(value).toLowerCase()); + out.append('.'); + break; + case ROMAN_UC_PERIOD: + out.append(valueToRoman(value)); + out.append('.'); + break; + default: + out.append('\u2022'); // can't set the font to wingdings so use the default bullet character + break; + } + out.append(" "); + return out.toString(); + } + + /** + * Convert an integer to its alpha equivalent e.g. 1 = A, 2 = B, 27 = AA etc + */ + private String valueToAlpha(int value) { + String alpha = ""; + int modulo; + while (value > 0) { + modulo = (value - 1) % 26; + alpha = (char)(65 + modulo) + alpha; + value = (int)((value - modulo) / 26); + } + return alpha; + } + + private static String[] _romanChars = new String[] { "M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I" }; + private static int[] _romanAlphaValues = new int[] { 1000,900,500,400,100,90,50,40,10,9,5,4,1 }; + + /** + * Convert an integer to its roman equivalent e.g. 1 = I, 9 = IX etc + */ + private String valueToRoman(int value) { + StringBuilder out = new StringBuilder(); + for(int i = 0; value > 0 && i < _romanChars.length; i++) { + while(_romanAlphaValues[i] <= value) { + out.append(_romanChars[i]); + value -= _romanAlphaValues[i]; + } + } + return out.toString(); + } + + /** + * Clear all text from this shape + */ + public void clearText(){ + _paragraphs.clear(); + CTTextBody txBody = ctShape.getTxBody(); + txBody.setPArray(null); // remove any existing paragraphs + } + + /** + * Set a single paragraph of text on the shape. Note this will replace all existing paragraphs created on the shape. + * @param text string representing the paragraph text + */ + public void setText(String text){ + clearText(); + + addNewTextParagraph().addNewTextRun().setText(text); } + /** + * Set a single paragraph of text on the shape. Note this will replace all existing paragraphs created on the shape. + * @param str rich text string representing the paragraph text + */ public void setText(XSSFRichTextString str){ XSSFWorkbook wb = (XSSFWorkbook)getDrawing().getParent().getParent(); @@ -166,12 +393,426 @@ public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable supercla r.setT(lt.getT()); } } + + clearText(); ctShape.getTxBody().setPArray(new CTTextParagraph[]{p}); + _paragraphs.add(new XSSFTextParagraph(ctShape.getTxBody().getPArray(0), ctShape)); + } + + /** + * Returns a collection of the XSSFTextParagraphs that are attached to this shape + * + * @return text paragraphs in this shape + */ + public List getTextParagraphs() { + return _paragraphs; + } + /** + * Add a new paragraph run to this shape + * + * @return created paragraph run + */ + public XSSFTextParagraph addNewTextParagraph() { + CTTextBody txBody = ctShape.getTxBody(); + CTTextParagraph p = txBody.addNewP(); + XSSFTextParagraph paragraph = new XSSFTextParagraph(p, ctShape); + _paragraphs.add(paragraph); + return paragraph; + } + + /** + * Add a new paragraph run to this shape, set to the provided string + * + * @return created paragraph run + */ + public XSSFTextParagraph addNewTextParagraph(String text) { + XSSFTextParagraph paragraph = addNewTextParagraph(); + paragraph.addNewTextRun().setText(text); + return paragraph; + } + + /** + * Add a new paragraph run to this shape, set to the provided rich text string + * + * @return created paragraph run + */ + public XSSFTextParagraph addNewTextParagraph(XSSFRichTextString str) { + CTTextBody txBody = ctShape.getTxBody(); + CTTextParagraph p = txBody.addNewP(); + + if(str.numFormattingRuns() == 0){ + CTRegularTextRun r = p.addNewR(); + CTTextCharacterProperties rPr = r.addNewRPr(); + rPr.setLang("en-US"); + rPr.setSz(1100); + r.setT(str.getString()); + + } else { + for (int i = 0; i < str.getCTRst().sizeOfRArray(); i++) { + CTRElt lt = str.getCTRst().getRArray(i); + CTRPrElt ltPr = lt.getRPr(); + if(ltPr == null) ltPr = lt.addNewRPr(); + + CTRegularTextRun r = p.addNewR(); + CTTextCharacterProperties rPr = r.addNewRPr(); + rPr.setLang("en-US"); + + applyAttributes(ltPr, rPr); + + r.setT(lt.getT()); + } + } + + // Note: the XSSFTextParagraph constructor will create its required XSSFTextRuns from the provided CTTextParagraph + XSSFTextParagraph paragraph = new XSSFTextParagraph(p, ctShape); + _paragraphs.add(paragraph); + + return paragraph; + } + + /** + * Sets the type of horizontal overflow for the text. + * + * @param overflow - the type of horizontal overflow. + * A null values unsets this property. + */ + public void setTextHorizontalOverflow(TextHorizontalOverflow overflow){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(anchor == null) { + if(bodyPr.isSetHorzOverflow()) bodyPr.unsetHorzOverflow(); + } else { + bodyPr.setHorzOverflow(STTextHorzOverflowType.Enum.forInt(overflow.ordinal() + 1)); + } + } + } + + /** + * Returns the type of horizontal overflow for the text. + * + * @return the type of horizontal overflow + */ + public TextHorizontalOverflow getTextHorizontalOverflow(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if(bodyPr != null) { + if(bodyPr.isSetHorzOverflow()){ + return TextHorizontalOverflow.values()[bodyPr.getVertOverflow().intValue() - 1]; + } + } + return TextHorizontalOverflow.OVERFLOW; + } + + /** + * Sets the type of vertical overflow for the text. + * + * @param overflow - the type of vertical overflow. + * A null values unsets this property. + */ + public void setTextVerticalOverflow(TextVerticalOverflow overflow){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(anchor == null) { + if(bodyPr.isSetVertOverflow()) bodyPr.unsetVertOverflow(); + } else { + bodyPr.setVertOverflow(STTextVertOverflowType.Enum.forInt(overflow.ordinal() + 1)); + } + } + } + + /** + * Returns the type of vertical overflow for the text. + * + * @return the type of vertical overflow + */ + public TextVerticalOverflow getTextVerticalOverflow(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if(bodyPr != null) { + if(bodyPr.isSetVertOverflow()){ + return TextVerticalOverflow.values()[bodyPr.getVertOverflow().intValue() - 1]; + } + } + return TextVerticalOverflow.OVERFLOW; + } + + /** + * Sets the type of vertical alignment for the text within the shape. + * + * @param anchor - the type of alignment. + * A null values unsets this property. + */ + public void setVerticalAlignment(VerticalAlignment anchor){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(anchor == null) { + if(bodyPr.isSetAnchor()) bodyPr.unsetAnchor(); + } else { + bodyPr.setAnchor(STTextAnchoringType.Enum.forInt(anchor.ordinal() + 1)); + } + } + } + + /** + * Returns the type of vertical alignment for the text within the shape. + * + * @return the type of vertical alignment + */ + public VerticalAlignment getVerticalAlignment(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if(bodyPr != null) { + if(bodyPr.isSetAnchor()){ + return VerticalAlignment.values()[bodyPr.getAnchor().intValue() - 1]; + } + } + return VerticalAlignment.TOP; + } + + /** + * Sets the vertical orientation of the text + * + * @param orientation vertical orientation of the text + * A null values unsets this property. + */ + public void setTextDirection(TextDirection orientation){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(orientation == null) { + if(bodyPr.isSetVert()) bodyPr.unsetVert(); + } else { + bodyPr.setVert(STTextVerticalType.Enum.forInt(orientation.ordinal() + 1)); + } + } + } + + /** + * Gets the vertical orientation of the text + * + * @return vertical orientation of the text + */ + public TextDirection getTextDirection(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + STTextVerticalType.Enum val = bodyPr.getVert(); + if(val != null){ + return TextDirection.values()[val.intValue() - 1]; + } + } + return TextDirection.HORIZONTAL; + } + + + /** + * Returns the distance (in points) between the bottom of the text frame + * and the bottom of the inscribed rectangle of the shape that contains the text. + * + * @return the bottom inset in points + */ + public double getBottomInset(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetBIns()){ + return Units.toPoints(bodyPr.getBIns()); + } + } + // If this attribute is omitted, then a value of 0.05 inches is implied + return 3.6; + } + + /** + * Returns the distance (in points) between the left edge of the text frame + * and the left edge of the inscribed rectangle of the shape that contains + * the text. + * + * @return the left inset in points + */ + public double getLeftInset(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetLIns()){ + return Units.toPoints(bodyPr.getLIns()); + } + } + // If this attribute is omitted, then a value of 0.05 inches is implied + return 3.6; + } + + /** + * Returns the distance (in points) between the right edge of the + * text frame and the right edge of the inscribed rectangle of the shape + * that contains the text. + * + * @return the right inset in points + */ + public double getRightInset(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetRIns()){ + return Units.toPoints(bodyPr.getRIns()); + } + } + // If this attribute is omitted, then a value of 0.05 inches is implied + return 3.6; + } + + /** + * Returns the distance (in points) between the top of the text frame + * and the top of the inscribed rectangle of the shape that contains the text. + * + * @return the top inset in points + */ + public double getTopInset(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetTIns()){ + return Units.toPoints(bodyPr.getTIns()); + } + } + // If this attribute is omitted, then a value of 0.05 inches is implied + return 3.6; + } + + /** + * Sets the bottom inset. + * @see #getBottomInset() + * + * @param margin the bottom margin + */ + public void setBottomInset(double margin){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(margin == -1) bodyPr.unsetBIns(); + else bodyPr.setBIns(Units.toEMU(margin)); + } + } + + /** + * Sets the left inset. + * @see #getLeftInset() + * + * @param margin the left margin + */ + public void setLeftInset(double margin){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(margin == -1) bodyPr.unsetLIns(); + else bodyPr.setLIns(Units.toEMU(margin)); + } + } + + /** + * Sets the right inset. + * @see #getRightInset() + * + * @param margin the right margin + */ + public void setRightInset(double margin){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(margin == -1) bodyPr.unsetRIns(); + else bodyPr.setRIns(Units.toEMU(margin)); + } + } + + /** + * Sets the top inset. + * @see #getTopInset() + * + * @param margin the top margin + */ + public void setTopInset(double margin){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(margin == -1) bodyPr.unsetTIns(); + else bodyPr.setTIns(Units.toEMU(margin)); + } + } + + + /** + * @return whether to wrap words within the bounding rectangle + */ + public boolean getWordWrap(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetWrap()){ + return bodyPr.getWrap() == STTextWrappingType.SQUARE; + } + } + return true; } /** * + * @param wrap whether to wrap words within the bounding rectangle + */ + public void setWordWrap(boolean wrap){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + bodyPr.setWrap(wrap ? STTextWrappingType.SQUARE : STTextWrappingType.NONE); + } + } + + /** + * + * Specifies that a shape should be auto-fit to fully contain the text described within it. + * Auto-fitting is when text within a shape is scaled in order to contain all the text inside + * + * @param value type of autofit + */ + public void setTextAutofit(TextAutofit value){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetSpAutoFit()) bodyPr.unsetSpAutoFit(); + if(bodyPr.isSetNoAutofit()) bodyPr.unsetNoAutofit(); + if(bodyPr.isSetNormAutofit()) bodyPr.unsetNormAutofit(); + + switch(value){ + case NONE: bodyPr.addNewNoAutofit(); break; + case NORMAL: bodyPr.addNewNormAutofit(); break; + case SHAPE: bodyPr.addNewSpAutoFit(); break; + } + } + } + + /** + * + * @return type of autofit + */ + public TextAutofit getTextAutofit(){ + CTTextBodyProperties bodyPr = ctShape.getTxBody().getBodyPr(); + if (bodyPr != null) { + if(bodyPr.isSetNoAutofit()) return TextAutofit.NONE; + else if (bodyPr.isSetNormAutofit()) return TextAutofit.NORMAL; + else if (bodyPr.isSetSpAutoFit()) return TextAutofit.SHAPE; + } + return TextAutofit.NORMAL; + } + + /** + * Gets the shape type, one of the constants defined in {@link org.apache.poi.ss.usermodel.ShapeTypes}. + * + * @return the shape type + * @see org.apache.poi.ss.usermodel.ShapeTypes + */ + public int getShapeType() { + return ctShape.getSpPr().getPrstGeom().getPrst().intValue(); + } + + /** + * Sets the shape types. + * + * @param type the shape type, one of the constants defined in {@link org.apache.poi.ss.usermodel.ShapeTypes}. + * @see org.apache.poi.ss.usermodel.ShapeTypes + */ + public void setShapeType(int type) { + ctShape.getSpPr().getPrstGeom().setPrst(STShapeType.Enum.forInt(type)); + } + + protected CTShapeProperties getShapeProperties(){ + return ctShape.getSpPr(); + } + + /** * org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRPrElt to * org.openxmlformats.schemas.drawingml.x2006.main.CTFont adapter */ @@ -186,8 +827,8 @@ public class XSSFSimpleShape extends XSSFShape { // TODO - instantiable supercla } if(pr.sizeOfIArray() > 0) rPr.setI(pr.getIArray(0).getVal()); - if(pr.sizeOfFamilyArray() > 0) { - CTTextFont rFont = rPr.addNewLatin(); + if(pr.sizeOfRFontArray() > 0) { + CTTextFont rFont = rPr.isSetLatin() ? rPr.getLatin() : rPr.addNewLatin(); rFont.setTypeface(pr.getRFontArray(0).getVal()); } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDrawing.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDrawing.java index 79d0c9855..77f48c74a 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDrawing.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDrawing.java @@ -17,6 +17,10 @@ package org.apache.poi.xssf.usermodel; import java.awt.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.Arrays; import java.util.List; @@ -243,4 +247,426 @@ public class TestXSSFDrawing extends TestCase { } + + /** + * ensure that font and color rich text attributes defined in a XSSFRichTextString + * are passed to XSSFSimpleShape. + * + * See Bugzilla 54969. + */ + public void testRichTextFontAndColor() { + 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))); + 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()); + assertTrue(Arrays.equals( + new byte[]{0, (byte)128, (byte)128} , + rPr.getSolidFill().getSrgbClr().getVal())); + + } + + /** + * Test setText single paragraph to ensure backwards compatibility + */ + public void testSetTextSingleParagraph() { + + 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))); + font.setFontName("Arial"); + rt.applyFont(font); + + shape.setText(rt); + + List paras = shape.getTextParagraphs(); + assertEquals(1, paras.size()); + assertEquals("Test String", paras.get(0).getText()); + + List runs = paras.get(0).getTextRuns(); + assertEquals(1, runs.size()); + assertEquals("Arial", runs.get(0).getFontFamily()); + + Color clr = runs.get(0).getFontColor(); + assertTrue(Arrays.equals( + new int[] { 0, 255, 255 } , + new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() })); + } + + /** + * Test addNewTextParagraph + */ + public void testAddNewTextParagraph() { + + 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 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 runs = para.getTextRuns(); + assertEquals(1, runs.size()); + assertEquals("Line 1", runs.get(0).getText()); + } + + /** + * Test addNewTextParagraph using RichTextString + */ + public void testAddNewTextParagraphWithRTS() { + + 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 Rich Text String"); + + XSSFFont font = wb.createFont(); + font.setColor(new XSSFColor(new Color(0, 255, 255))); + font.setFontName("Arial"); + rt.applyFont(font); + + XSSFFont midfont = wb.createFont(); + midfont.setColor(new XSSFColor(new Color(0, 255, 0))); + 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 + wb = XSSFTestDataSamples.writeOutAndReadBack(wb); + sheet = wb.getSheetAt(0); + + // Check + drawing = sheet.createDrawingPatriarch(); + + List shapes = drawing.getShapes(); + assertEquals(1, shapes.size()); + assertTrue(shapes.get(0) instanceof XSSFSimpleShape); + + XSSFSimpleShape sshape = (XSSFSimpleShape) shapes.get(0); + + List 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 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(); + assertTrue(Arrays.equals( + 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(); + assertTrue(Arrays.equals( + 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(); + assertTrue(Arrays.equals( + new int[] { 0, 255, 255 } , + new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() })); + } + + /** + * Test add multiple paragraphs and retrieve text + */ + public void testAddMultipleParagraphs() { + + 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 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()); + } + + /** + * Test setting the text, then adding multiple paragraphs and retrieve text + */ + public void testSetAddMultipleParagraphs() { + + 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 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()); + } + + /** + * Test reading text from a textbox in an existing file + */ + public void testReadTextBox(){ + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithDrawing.xlsx"); + XSSFSheet sheet = wb.getSheetAt(0); + //the sheet has one relationship and it is XSSFDrawing + List rels = sheet.getRelations(); + assertEquals(1, rels.size()); + assertTrue(rels.get(0) instanceof XSSFDrawing); + + XSSFDrawing drawing = (XSSFDrawing)rels.get(0); + //sheet.createDrawingPatriarch() should return the same instance of XSSFDrawing + assertSame(drawing, sheet.createDrawingPatriarch()); + String drawingId = drawing.getPackageRelationship().getId(); + + //there should be a relation to this drawing in the worksheet + assertTrue(sheet.getCTWorksheet().isSetDrawing()); + assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId()); + + List 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()); + } + + + /** + * Test reading multiple paragraphs from a textbox in an existing file + */ + public void testReadTextBoxParagraphs(){ + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithTextBox.xlsx"); + XSSFSheet sheet = wb.getSheetAt(0); + //the sheet has one relationship and it is XSSFDrawing + List rels = sheet.getRelations(); + assertEquals(1, rels.size()); + + assertTrue(rels.get(0) instanceof XSSFDrawing); + + XSSFDrawing drawing = (XSSFDrawing)rels.get(0); + + //sheet.createDrawingPatriarch() should return the same instance of XSSFDrawing + assertSame(drawing, sheet.createDrawingPatriarch()); + String drawingId = drawing.getPackageRelationship().getId(); + + //there should be a relation to this drawing in the worksheet + assertTrue(sheet.getCTWorksheet().isSetDrawing()); + assertEquals(drawingId, sheet.getCTWorksheet().getDrawing().getId()); + + List shapes = drawing.getShapes(); + assertEquals(1, shapes.size()); + + assertTrue(shapes.get(0) instanceof XSSFSimpleShape); + + XSSFSimpleShape textbox = (XSSFSimpleShape) shapes.get(0); + + List 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(); + assertTrue(Arrays.equals( + new int[] { 255, 0, 0 } , + new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() })); + + clr = paras.get(1).getTextRuns().get(0).getFontColor(); + assertTrue(Arrays.equals( + new int[] { 0, 255, 0 } , + new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() })); + + clr = paras.get(2).getTextRuns().get(0).getFontColor(); + assertTrue(Arrays.equals( + new int[] { 0, 0, 255 } , + new int[] { clr.getRed(), clr.getGreen(), clr.getBlue() })); + } + /** + * Test adding and reading back paragraphs as bullet points + */ + public void testAddBulletParagraphs() { + + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.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); + 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); + 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 + wb = XSSFTestDataSamples.writeOutAndReadBack(wb); + sheet = wb.getSheetAt(0); + + // Check + drawing = sheet.createDrawingPatriarch(); + + List shapes = drawing.getShapes(); + assertEquals(1, shapes.size()); + assertTrue(shapes.get(0) instanceof XSSFSimpleShape); + + XSSFSimpleShape sshape = (XSSFSimpleShape) shapes.get(0); + + List 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 + + StringBuilder builder = new StringBuilder(); + + builder.append(paraString1); + builder.append("\n"); + builder.append("\u2022 "); + builder.append(paraString2); + builder.append("\n"); + builder.append("\t\u2022 "); + builder.append(paraString3); + builder.append("\n"); + builder.append("\u2022 "); + builder.append(paraString4); + builder.append("\n"); + builder.append(paraString5); + builder.append("\n"); + builder.append("1. "); + builder.append(paraString6); + builder.append("\n"); + builder.append("\t3. "); + builder.append(paraString7); + builder.append("\n"); + builder.append("\t4. "); + builder.append(paraString8); + builder.append("\n"); + builder.append("\t"); // should be empty + builder.append("\n"); + builder.append("\t5. "); + builder.append(paraString9); + builder.append("\n"); + builder.append("2. "); + builder.append(paraString10); + + assertEquals(builder.toString(), sshape.getText()); + } + + /** + * Test reading bullet numbering from a textbox in an existing file + */ + public void testReadTextBox2(){ + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("WithTextBox2.xlsx"); + XSSFSheet sheet = wb.getSheetAt(0); + XSSFDrawing drawing = sheet.createDrawingPatriarch(); + List shapes = drawing.getShapes(); + XSSFSimpleShape textbox = (XSSFSimpleShape) shapes.get(0); + String extracted = textbox.getText(); + StringBuilder sb = new StringBuilder(); + sb.append("1. content1A\n"); + sb.append("\t1. content1B\n"); + sb.append("\t2. content2B\n"); + sb.append("\t3. content3B\n"); + sb.append("2. content2A\n"); + sb.append("\t3. content2BStartAt3\n"); + sb.append("\t\n\t\n\t"); + sb.append("4. content2BStartAt3Incremented\n"); + sb.append("\t\n\t\n\t\n\t"); + + assertEquals(sb.toString(), extracted); + } } diff --git a/test-data/spreadsheet/WithTextBox.xlsx b/test-data/spreadsheet/WithTextBox.xlsx index b4adac4af7f3e6d4415d9d8a86575c48233b2359..375e9f11da9606affc5d879f8d3b9f553fa81e39 100644 GIT binary patch delta 2820 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 literal 0 HcmV?d00001