/* * ==================================================================== * 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 java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.poi.POIXMLException; import org.apache.poi.sl.draw.DrawFactory; import org.apache.poi.sl.draw.DrawTextShape; import org.apache.poi.sl.usermodel.Insets2D; import org.apache.poi.sl.usermodel.Placeholder; import org.apache.poi.sl.usermodel.TextShape; import org.apache.poi.sl.usermodel.VerticalAlignment; import org.apache.poi.util.Beta; import org.apache.poi.util.Units; import org.apache.poi.xslf.model.PropertyFetcher; import org.apache.poi.xslf.model.TextBodyPropertyFetcher; import org.apache.xmlbeans.XmlObject; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBody; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBodyProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; import org.openxmlformats.schemas.drawingml.x2006.main.STTextAnchoringType; import org.openxmlformats.schemas.drawingml.x2006.main.STTextVerticalType; import org.openxmlformats.schemas.drawingml.x2006.main.STTextWrappingType; import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; /** * Represents a shape that can hold text. */ @Beta public abstract class XSLFTextShape extends XSLFSimpleShape implements TextShape<XSLFShape,XSLFTextParagraph> { private final List<XSLFTextParagraph> _paragraphs; /*package*/ XSLFTextShape(XmlObject shape, XSLFSheet sheet) { super(shape, sheet); _paragraphs = new ArrayList<XSLFTextParagraph>(); CTTextBody txBody = getTextBody(false); if (txBody != null) { for (CTTextParagraph p : txBody.getPArray()) { _paragraphs.add(newTextParagraph(p)); } } } public Iterator<XSLFTextParagraph> iterator(){ return getTextParagraphs().iterator(); } @Override public String getText() { StringBuilder out = new StringBuilder(); for (XSLFTextParagraph p : _paragraphs) { if (out.length() > 0) out.append('\n'); out.append(p.getText()); } return out.toString(); } /** * unset text from this shape */ public void clearText(){ _paragraphs.clear(); CTTextBody txBody = getTextBody(true); txBody.setPArray(null); // remove any existing paragraphs } @Override public XSLFTextRun setText(String text) { // calling clearText or setting to a new Array leads to a XmlValueDisconnectedException if (!_paragraphs.isEmpty()) { CTTextBody txBody = getTextBody(false); int cntPs = txBody.sizeOfPArray(); for (int i = cntPs; i > 1; i--) { txBody.removeP(i-1); _paragraphs.remove(i-1); } _paragraphs.get(0).clearButKeepProperties(); } return appendText(text, false); } @Override public XSLFTextRun appendText(String text, boolean newParagraph) { if (text == null) return null; // copy properties from last paragraph / textrun or paragraph end marker CTTextParagraphProperties otherPPr = null; CTTextCharacterProperties otherRPr = null; boolean firstPara; XSLFTextParagraph para; if (_paragraphs.isEmpty()) { firstPara = false; para = null; } else { firstPara = !newParagraph; para = _paragraphs.get(_paragraphs.size()-1); CTTextParagraph ctp = para.getXmlObject(); otherPPr = ctp.getPPr(); List<XSLFTextRun> runs = para.getTextRuns(); if (!runs.isEmpty()) { XSLFTextRun r0 = runs.get(runs.size()-1); otherRPr = r0.getRPr(false); if (otherRPr == null) { otherRPr = ctp.getEndParaRPr(); } } // don't copy endParaRPr to the run in case there aren't any other runs // this is the case when setText() was called initially // otherwise the master style will be overridden/ignored } XSLFTextRun run = null; for (String lineTxt : text.split("\\r\\n?|\\n")) { if (!firstPara) { if (para != null) { CTTextParagraph ctp = para.getXmlObject(); CTTextCharacterProperties unexpectedRPr = ctp.getEndParaRPr(); if (unexpectedRPr != null && unexpectedRPr != otherRPr) { ctp.unsetEndParaRPr(); } } para = addNewTextParagraph(); if (otherPPr != null) { para.getXmlObject().setPPr(otherPPr); } } boolean firstRun = true; for (String runText : lineTxt.split("[\u000b]")) { if (!firstRun) { para.addLineBreak(); } run = para.addNewTextRun(); run.setText(runText); if (otherRPr != null) { run.getRPr(true).set(otherRPr); } firstRun = false; } firstPara = false; } assert(run != null); return run; } @Override public List<XSLFTextParagraph> getTextParagraphs() { return _paragraphs; } /** * add a new paragraph run to this shape * * @return created paragraph run */ public XSLFTextParagraph addNewTextParagraph() { CTTextBody txBody = getTextBody(false); CTTextParagraph p; if (txBody == null) { txBody = getTextBody(true); p = txBody.getPArray(0); p.removeR(0); } else { p = txBody.addNewP(); } XSLFTextParagraph paragraph = newTextParagraph(p); _paragraphs.add(paragraph); return paragraph; } @Override public void setVerticalAlignment(VerticalAlignment anchor){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(anchor == null) { if(bodyPr.isSetAnchor()) bodyPr.unsetAnchor(); } else { bodyPr.setAnchor(STTextAnchoringType.Enum.forInt(anchor.ordinal() + 1)); } } } @Override public VerticalAlignment getVerticalAlignment(){ PropertyFetcher<VerticalAlignment> fetcher = new TextBodyPropertyFetcher<VerticalAlignment>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetAnchor()){ int val = props.getAnchor().intValue(); setValue(VerticalAlignment.values()[val - 1]); return true; } return false; } }; fetchShapeProperty(fetcher); return fetcher.getValue() == null ? VerticalAlignment.TOP : fetcher.getValue(); } @Override public void setHorizontalCentered(Boolean isCentered){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if (isCentered == null) { if (bodyPr.isSetAnchorCtr()) bodyPr.unsetAnchorCtr(); } else { bodyPr.setAnchorCtr(isCentered); } } } @Override public boolean isHorizontalCentered(){ PropertyFetcher<Boolean> fetcher = new TextBodyPropertyFetcher<Boolean>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetAnchorCtr()){ setValue(props.getAnchorCtr()); return true; } return false; } }; fetchShapeProperty(fetcher); return fetcher.getValue() == null ? false : fetcher.getValue(); } @Override public void setTextDirection(TextDirection orientation){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(orientation == null) { if(bodyPr.isSetVert()) bodyPr.unsetVert(); } else { bodyPr.setVert(STTextVerticalType.Enum.forInt(orientation.ordinal() + 1)); } } } @Override public TextDirection getTextDirection(){ CTTextBodyProperties bodyPr = getTextBodyPr(); if (bodyPr != null) { STTextVerticalType.Enum val = bodyPr.getVert(); if(val != null) { switch (val.intValue()) { default: case STTextVerticalType.INT_HORZ: return TextDirection.HORIZONTAL; case STTextVerticalType.INT_EA_VERT: case STTextVerticalType.INT_MONGOLIAN_VERT: case STTextVerticalType.INT_VERT: return TextDirection.VERTICAL; case STTextVerticalType.INT_VERT_270: return TextDirection.VERTICAL_270; case STTextVerticalType.INT_WORD_ART_VERT_RTL: case STTextVerticalType.INT_WORD_ART_VERT: return TextDirection.STACKED; } } } return TextDirection.HORIZONTAL; } @Override public Double getTextRotation() { CTTextBodyProperties bodyPr = getTextBodyPr(); if (bodyPr != null && bodyPr.isSetRot()) { return bodyPr.getRot() / 60000.; } return null; } @Override public void setTextRotation(Double rotation) { CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { bodyPr.setRot((int)(rotation * 60000.)); } } /** * 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(){ PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetBIns()){ double val = Units.toPoints(props.getBIns()); setValue(val); return true; } return false; } }; fetchShapeProperty(fetcher); // If this attribute is omitted, then a value of 0.05 inches is implied return fetcher.getValue() == null ? 3.6 : fetcher.getValue(); } /** * 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(){ PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetLIns()){ double val = Units.toPoints(props.getLIns()); setValue(val); return true; } return false; } }; fetchShapeProperty(fetcher); // If this attribute is omitted, then a value of 0.1 inches is implied return fetcher.getValue() == null ? 7.2 : fetcher.getValue(); } /** * 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(){ PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetRIns()){ double val = Units.toPoints(props.getRIns()); setValue(val); return true; } return false; } }; fetchShapeProperty(fetcher); // If this attribute is omitted, then a value of 0.1 inches is implied return fetcher.getValue() == null ? 7.2 : fetcher.getValue(); } /** * 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(){ PropertyFetcher<Double> fetcher = new TextBodyPropertyFetcher<Double>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetTIns()){ double val = Units.toPoints(props.getTIns()); setValue(val); return true; } return false; } }; fetchShapeProperty(fetcher); // If this attribute is omitted, then a value of 0.05 inches is implied return fetcher.getValue() == null ? 3.6 : fetcher.getValue(); } /** * Sets the bottom margin. * @see #getBottomInset() * * @param margin the bottom margin */ public void setBottomInset(double margin){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(margin == -1) bodyPr.unsetBIns(); else bodyPr.setBIns(Units.toEMU(margin)); } } /** * Sets the left margin. * @see #getLeftInset() * * @param margin the left margin */ public void setLeftInset(double margin){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(margin == -1) bodyPr.unsetLIns(); else bodyPr.setLIns(Units.toEMU(margin)); } } /** * Sets the right margin. * @see #getRightInset() * * @param margin the right margin */ public void setRightInset(double margin){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(margin == -1) bodyPr.unsetRIns(); else bodyPr.setRIns(Units.toEMU(margin)); } } /** * Sets the top margin. * @see #getTopInset() * * @param margin the top margin */ public void setTopInset(double margin){ CTTextBodyProperties bodyPr = getTextBodyPr(true); if (bodyPr != null) { if(margin == -1) bodyPr.unsetTIns(); else bodyPr.setTIns(Units.toEMU(margin)); } } @Override public Insets2D getInsets() { Insets2D insets = new Insets2D(getTopInset(), getLeftInset(), getBottomInset(), getRightInset()); return insets; } @Override public void setInsets(Insets2D insets) { setTopInset(insets.top); setLeftInset(insets.left); setBottomInset(insets.bottom); setRightInset(insets.right); } @Override public boolean getWordWrap(){ PropertyFetcher<Boolean> fetcher = new TextBodyPropertyFetcher<Boolean>(){ public boolean fetch(CTTextBodyProperties props){ if(props.isSetWrap()){ setValue(props.getWrap() == STTextWrappingType.SQUARE); return true; } return false; } }; fetchShapeProperty(fetcher); return fetcher.getValue() == null ? true : fetcher.getValue(); } @Override public void setWordWrap(boolean wrap){ CTTextBodyProperties bodyPr = getTextBodyPr(true); 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 = getTextBodyPr(true); 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 = getTextBodyPr(); 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; } protected CTTextBodyProperties getTextBodyPr(){ return getTextBodyPr(false); } protected CTTextBodyProperties getTextBodyPr(boolean create) { CTTextBody textBody = getTextBody(create); if (textBody == null) { return null; } CTTextBodyProperties textBodyPr = textBody.getBodyPr(); if (textBodyPr == null && create) { textBodyPr = textBody.addNewBodyPr(); } return textBodyPr; } protected abstract CTTextBody getTextBody(boolean create); @Override public void setPlaceholder(Placeholder placeholder) { super.setPlaceholder(placeholder); } public Placeholder getTextType(){ CTPlaceholder ph = getCTPlaceholder(); if (ph == null) return null; int val = ph.getType().intValue(); return Placeholder.lookupOoxml(val); } @Override public double getTextHeight(){ DrawFactory drawFact = DrawFactory.getInstance(null); DrawTextShape dts = drawFact.getDrawable(this); return dts.getTextHeight(); } /** * Adjust the size of the shape so it encompasses the text inside it. * * @return a <code>Rectangle2D</code> that is the bounds of this shape. */ public Rectangle2D resizeToFitText(){ Rectangle2D anchor = getAnchor(); if(anchor.getWidth() == 0.) throw new POIXMLException( "Anchor of the shape was not set."); double height = getTextHeight(); height += 1; // add a pixel to compensate rounding errors anchor.setRect(anchor.getX(), anchor.getY(), anchor.getWidth(), height); setAnchor(anchor); return anchor; } @Override void copy(XSLFShape other){ super.copy(other); XSLFTextShape otherTS = (XSLFTextShape)other; CTTextBody otherTB = otherTS.getTextBody(false); CTTextBody thisTB = getTextBody(true); if (otherTB == null) { return; } thisTB.setBodyPr((CTTextBodyProperties)otherTB.getBodyPr().copy()); if (thisTB.isSetLstStyle()) thisTB.unsetLstStyle(); if (otherTB.isSetLstStyle()) { thisTB.setLstStyle((CTTextListStyle)otherTB.getLstStyle().copy()); } boolean srcWordWrap = otherTS.getWordWrap(); if(srcWordWrap != getWordWrap()){ setWordWrap(srcWordWrap); } double leftInset = otherTS.getLeftInset(); if(leftInset != getLeftInset()) { setLeftInset(leftInset); } double rightInset = otherTS.getRightInset(); if(rightInset != getRightInset()) { setRightInset(rightInset); } double topInset = otherTS.getTopInset(); if(topInset != getTopInset()) { setTopInset(topInset); } double bottomInset = otherTS.getBottomInset(); if(bottomInset != getBottomInset()) { setBottomInset(bottomInset); } VerticalAlignment vAlign = otherTS.getVerticalAlignment(); if(vAlign != getVerticalAlignment()) { setVerticalAlignment(vAlign); } clearText(); for (XSLFTextParagraph srcP : otherTS.getTextParagraphs()) { XSLFTextParagraph tgtP = addNewTextParagraph(); tgtP.copy(srcP); } } @Override public void setTextPlaceholder(TextPlaceholder placeholder) { switch (placeholder) { default: case NOTES: case HALF_BODY: case QUARTER_BODY: case BODY: setPlaceholder(Placeholder.BODY); break; case TITLE: setPlaceholder(Placeholder.TITLE); break; case CENTER_BODY: setPlaceholder(Placeholder.BODY); setHorizontalCentered(true); break; case CENTER_TITLE: setPlaceholder(Placeholder.CENTERED_TITLE); break; case OTHER: setPlaceholder(Placeholder.CONTENT); break; } } @Override public TextPlaceholder getTextPlaceholder() { Placeholder ph = getTextType(); if (ph == null) return TextPlaceholder.BODY; switch (ph) { case BODY: return TextPlaceholder.BODY; case TITLE: return TextPlaceholder.TITLE; case CENTERED_TITLE: return TextPlaceholder.CENTER_TITLE; default: case CONTENT: return TextPlaceholder.OTHER; } } /** * Helper method to allow subclasses to provide their own text paragraph * * @param p the xml reference * * @return a new text paragraph * * @since POI 3.15-beta2 */ protected XSLFTextParagraph newTextParagraph(CTTextParagraph p) { return new XSLFTextParagraph(p, this); } }