| /* |
| * ==================================================================== |
| * 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); |
| } |
| } |