| /* ==================================================================== |
| 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.hslf.usermodel; |
| |
| import static org.apache.logging.log4j.util.Unbox.box; |
| import static org.apache.poi.hslf.record.RecordTypes.OutlineTextRefAtom; |
| |
| import java.awt.Color; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.poi.common.usermodel.fonts.FontGroup; |
| import org.apache.poi.common.usermodel.fonts.FontInfo; |
| import org.apache.poi.hslf.exceptions.HSLFException; |
| import org.apache.poi.hslf.model.textproperties.BitMaskTextProp; |
| import org.apache.poi.hslf.model.textproperties.FontAlignmentProp; |
| import org.apache.poi.hslf.model.textproperties.HSLFTabStop; |
| import org.apache.poi.hslf.model.textproperties.HSLFTabStopPropCollection; |
| import org.apache.poi.hslf.model.textproperties.IndentProp; |
| import org.apache.poi.hslf.model.textproperties.ParagraphFlagsTextProp; |
| import org.apache.poi.hslf.model.textproperties.TextAlignmentProp; |
| import org.apache.poi.hslf.model.textproperties.TextPFException9; |
| import org.apache.poi.hslf.model.textproperties.TextProp; |
| import org.apache.poi.hslf.model.textproperties.TextPropCollection; |
| import org.apache.poi.hslf.model.textproperties.TextPropCollection.TextPropType; |
| import org.apache.poi.hslf.record.ColorSchemeAtom; |
| import org.apache.poi.hslf.record.EscherTextboxWrapper; |
| import org.apache.poi.hslf.record.InteractiveInfo; |
| import org.apache.poi.hslf.record.MasterTextPropAtom; |
| import org.apache.poi.hslf.record.OutlineTextRefAtom; |
| import org.apache.poi.hslf.record.PPDrawing; |
| import org.apache.poi.hslf.record.Record; |
| import org.apache.poi.hslf.record.RecordContainer; |
| import org.apache.poi.hslf.record.RecordTypes; |
| import org.apache.poi.hslf.record.RoundTripHFPlaceholder12; |
| import org.apache.poi.hslf.record.SlideListWithText; |
| import org.apache.poi.hslf.record.SlidePersistAtom; |
| import org.apache.poi.hslf.record.StyleTextProp9Atom; |
| import org.apache.poi.hslf.record.StyleTextPropAtom; |
| import org.apache.poi.hslf.record.TextBytesAtom; |
| import org.apache.poi.hslf.record.TextCharsAtom; |
| import org.apache.poi.hslf.record.TextHeaderAtom; |
| import org.apache.poi.hslf.record.TextRulerAtom; |
| import org.apache.poi.hslf.record.TextSpecInfoAtom; |
| import org.apache.poi.hslf.record.TxInteractiveInfoAtom; |
| import org.apache.poi.sl.draw.DrawPaint; |
| import org.apache.poi.sl.usermodel.AutoNumberingScheme; |
| import org.apache.poi.sl.usermodel.PaintStyle; |
| import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint; |
| import org.apache.poi.sl.usermodel.Placeholder; |
| import org.apache.poi.sl.usermodel.TabStop; |
| import org.apache.poi.sl.usermodel.TabStop.TabStopType; |
| import org.apache.poi.sl.usermodel.TextParagraph; |
| import org.apache.poi.sl.usermodel.TextShape.TextPlaceholder; |
| import org.apache.poi.util.StringUtil; |
| import org.apache.poi.util.Units; |
| |
| /** |
| * This class represents a run of text in a powerpoint document. That |
| * run could be text on a sheet, or text in a note. |
| * It is only a very basic class for now |
| */ |
| |
| public final class HSLFTextParagraph implements TextParagraph<HSLFShape,HSLFTextParagraph,HSLFTextRun> { |
| private static final Logger LOG = LogManager.getLogger(HSLFTextParagraph.class); |
| |
| /** |
| * How to align the text |
| */ |
| /* package */static final int AlignLeft = 0; |
| /* package */static final int AlignCenter = 1; |
| /* package */static final int AlignRight = 2; |
| /* package */static final int AlignJustify = 3; |
| |
| // Note: These fields are protected to help with unit testing |
| // Other classes shouldn't really go playing with them! |
| private final TextHeaderAtom _headerAtom; |
| private TextBytesAtom _byteAtom; |
| private TextCharsAtom _charAtom; |
| private TextPropCollection _paragraphStyle; |
| |
| protected TextRulerAtom _ruler; |
| protected final List<HSLFTextRun> _runs = new ArrayList<>(); |
| protected HSLFTextShape _parentShape; |
| private HSLFSheet _sheet; |
| private int shapeId; |
| |
| private StyleTextProp9Atom styleTextProp9Atom; |
| |
| private boolean _dirty; |
| |
| private final List<HSLFTextParagraph> parentList; |
| |
| private class HSLFTabStopDecorator implements TabStop { |
| final HSLFTabStop tabStop; |
| |
| HSLFTabStopDecorator(final HSLFTabStop tabStop) { |
| this.tabStop = tabStop; |
| } |
| |
| public double getPositionInPoints() { |
| return tabStop.getPositionInPoints(); |
| } |
| |
| public void setPositionInPoints(double position) { |
| tabStop.setPositionInPoints(position); |
| setDirty(); |
| } |
| |
| public TabStopType getType() { |
| return tabStop.getType(); |
| } |
| |
| public void setType(TabStopType type) { |
| tabStop.setType(type); |
| setDirty(); |
| } |
| } |
| |
| |
| /** |
| * Constructs a Text Run from a Unicode text block. |
| * Either a {@link TextCharsAtom} or a {@link TextBytesAtom} needs to be provided. |
| * |
| * @param tha the TextHeaderAtom that defines what's what |
| * @param tba the TextBytesAtom containing the text or null if {@link TextCharsAtom} is provided |
| * @param tca the TextCharsAtom containing the text or null if {@link TextBytesAtom} is provided |
| * @param parentList the list which contains this paragraph |
| */ |
| /* package */ HSLFTextParagraph( |
| TextHeaderAtom tha, |
| TextBytesAtom tba, |
| TextCharsAtom tca, |
| List<HSLFTextParagraph> parentList |
| ) { |
| if (tha == null) { |
| throw new IllegalArgumentException("TextHeaderAtom must be set."); |
| } |
| _headerAtom = tha; |
| _byteAtom = tba; |
| _charAtom = tca; |
| this.parentList = parentList; |
| setParagraphStyle(new TextPropCollection(1, TextPropType.paragraph)); |
| } |
| |
| /* package */HSLFTextParagraph(HSLFTextParagraph other) { |
| _headerAtom = other._headerAtom; |
| _byteAtom = other._byteAtom; |
| _charAtom = other._charAtom; |
| _parentShape = other._parentShape; |
| _sheet = other._sheet; |
| _ruler = other._ruler; |
| shapeId = other.shapeId; |
| parentList = other.parentList; |
| setParagraphStyle(other._paragraphStyle); |
| } |
| |
| public void addTextRun(HSLFTextRun run) { |
| _runs.add(run); |
| } |
| |
| @Override |
| public List<HSLFTextRun> getTextRuns() { |
| return _runs; |
| } |
| |
| public TextPropCollection getParagraphStyle() { |
| return _paragraphStyle; |
| } |
| |
| public void setParagraphStyle(TextPropCollection paragraphStyle) { |
| _paragraphStyle = paragraphStyle; |
| } |
| |
| /** |
| * Supply the Sheet we belong to, which might have an assigned SlideShow |
| * Also passes it on to our child RichTextRuns |
| */ |
| public static void supplySheet(List<HSLFTextParagraph> paragraphs, HSLFSheet sheet) { |
| if (paragraphs == null) { |
| return; |
| } |
| for (HSLFTextParagraph p : paragraphs) { |
| p.supplySheet(sheet); |
| } |
| |
| assert(sheet.getSlideShow() != null); |
| } |
| |
| /** |
| * Supply the Sheet we belong to, which might have an assigned SlideShow |
| * Also passes it on to our child RichTextRuns |
| */ |
| private void supplySheet(HSLFSheet sheet) { |
| this._sheet = sheet; |
| |
| for (HSLFTextRun rt : _runs) { |
| rt.updateSheet(); |
| } |
| } |
| |
| public HSLFSheet getSheet() { |
| return this._sheet; |
| } |
| |
| /** |
| * @return Shape ID |
| */ |
| protected int getShapeId() { |
| return shapeId; |
| } |
| |
| /** |
| * @param id Shape ID |
| */ |
| protected void setShapeId(int id) { |
| shapeId = id; |
| } |
| |
| /** |
| * @return 0-based index of the text run in the SLWT container |
| */ |
| protected int getIndex() { |
| return (_headerAtom != null) ? _headerAtom.getIndex() : -1; |
| } |
| |
| /** |
| * Sets the index of the paragraph in the SLWT container |
| * |
| * @param index |
| */ |
| protected void setIndex(int index) { |
| if (_headerAtom != null) { |
| _headerAtom.setIndex(index); |
| } |
| } |
| |
| /** |
| * Returns the type of the text, from the TextHeaderAtom. |
| * Possible values can be seen from TextHeaderAtom |
| * @see TextHeaderAtom |
| */ |
| public int getRunType() { |
| return (_headerAtom != null) ? _headerAtom.getTextType() : -1; |
| } |
| |
| public void setRunType(int runType) { |
| if (_headerAtom != null) { |
| _headerAtom.setTextType(runType); |
| } |
| } |
| |
| /** |
| * Is this Text Run one from a {@link PPDrawing}, or is it |
| * one from the {@link SlideListWithText}? |
| */ |
| public boolean isDrawingBased() { |
| return (getIndex() == -1); |
| } |
| |
| public TextRulerAtom getTextRuler() { |
| return _ruler; |
| } |
| |
| public TextRulerAtom createTextRuler() { |
| _ruler = getTextRuler(); |
| if (_ruler == null) { |
| _ruler = TextRulerAtom.getParagraphInstance(); |
| Record childAfter = _byteAtom; |
| if (childAfter == null) { |
| childAfter = _charAtom; |
| } |
| if (childAfter == null) { |
| childAfter = _headerAtom; |
| } |
| _headerAtom.getParentRecord().addChildAfter(_ruler, childAfter); |
| } |
| return _ruler; |
| } |
| |
| /** |
| * Returns records that make up the list of text paragraphs |
| * (there can be misc InteractiveInfo, TxInteractiveInfo and other records) |
| * |
| * @return text run records |
| */ |
| public Record[] getRecords() { |
| Record[] r = _headerAtom.getParentRecord().getChildRecords(); |
| return getRecords(r, new int[] { 0 }, _headerAtom); |
| } |
| |
| private static Record[] getRecords(Record[] records, int[] startIdx, TextHeaderAtom headerAtom) { |
| if (records == null) { |
| throw new NullPointerException("records need to be set."); |
| } |
| |
| for (; startIdx[0] < records.length; startIdx[0]++) { |
| Record r = records[startIdx[0]]; |
| if (r instanceof TextHeaderAtom && (headerAtom == null || r == headerAtom)) { |
| break; |
| } |
| } |
| |
| if (startIdx[0] >= records.length) { |
| LOG.atInfo().log("header atom wasn't found - container might contain only an OutlineTextRefAtom"); |
| return new Record[0]; |
| } |
| |
| int length; |
| for (length = 1; startIdx[0] + length < records.length; length++) { |
| Record r = records[startIdx[0]+length]; |
| if (r instanceof TextHeaderAtom || r instanceof SlidePersistAtom) { |
| break; |
| } |
| } |
| |
| Record[] result = Arrays.copyOfRange(records, startIdx[0], startIdx[0]+length, Record[].class); |
| startIdx[0] += length; |
| |
| return result; |
| } |
| |
| /** Numbered List info */ |
| public void setStyleTextProp9Atom(final StyleTextProp9Atom styleTextProp9Atom) { |
| this.styleTextProp9Atom = styleTextProp9Atom; |
| } |
| |
| /** Numbered List info */ |
| public StyleTextProp9Atom getStyleTextProp9Atom() { |
| return this.styleTextProp9Atom; |
| } |
| |
| @Override |
| public Iterator<HSLFTextRun> iterator() { |
| return _runs.iterator(); |
| } |
| |
| @Override |
| public Double getLeftMargin() { |
| Integer val = null; |
| if (_ruler != null) { |
| Integer[] toList = _ruler.getTextOffsets(); |
| val = (toList.length > getIndentLevel()) ? toList[getIndentLevel()] : null; |
| } |
| |
| if (val == null) { |
| TextProp tp = getPropVal(_paragraphStyle, "text.offset"); |
| val = (tp == null) ? null : tp.getValue(); |
| } |
| |
| return (val == null) ? null : Units.masterToPoints(val); |
| } |
| |
| @Override |
| public void setLeftMargin(Double leftMargin) { |
| Integer val = (leftMargin == null) ? null : Units.pointsToMaster(leftMargin); |
| setParagraphTextPropVal("text.offset", val); |
| } |
| |
| @Override |
| public Double getRightMargin() { |
| // TODO: find out, how to determine this value |
| return null; |
| } |
| |
| @Override |
| public void setRightMargin(Double rightMargin) { |
| // TODO: find out, how to set this value |
| } |
| |
| @Override |
| public Double getIndent() { |
| Integer val = null; |
| if (_ruler != null) { |
| Integer[] toList = _ruler.getBulletOffsets(); |
| val = (toList.length > getIndentLevel()) ? toList[getIndentLevel()] : null; |
| } |
| |
| if (val == null) { |
| TextProp tp = getPropVal(_paragraphStyle, "bullet.offset"); |
| val = (tp == null) ? null : tp.getValue(); |
| } |
| |
| return (val == null) ? null : Units.masterToPoints(val); |
| } |
| |
| @Override |
| public void setIndent(Double indent) { |
| Integer val = (indent == null) ? null : Units.pointsToMaster(indent); |
| setParagraphTextPropVal("bullet.offset", val); |
| } |
| |
| @Override |
| public String getDefaultFontFamily() { |
| FontInfo fontInfo = null; |
| if (!_runs.isEmpty()) { |
| HSLFTextRun tr = _runs.get(0); |
| fontInfo = tr.getFontInfo(null); |
| // fallback to LATIN if the font for the font group wasn't defined |
| if (fontInfo == null) { |
| fontInfo = tr.getFontInfo(FontGroup.LATIN); |
| } |
| } |
| if (fontInfo == null) { |
| fontInfo = HSLFFontInfoPredefined.ARIAL; |
| } |
| |
| return fontInfo.getTypeface(); |
| } |
| |
| @Override |
| public Double getDefaultFontSize() { |
| Double d = null; |
| if (!_runs.isEmpty()) { |
| d = _runs.get(0).getFontSize(); |
| } |
| |
| return (d != null) ? d : 12d; |
| } |
| |
| @Override |
| public void setTextAlign(TextAlign align) { |
| Integer alignInt = null; |
| if (align != null) { |
| switch (align) { |
| default: |
| case LEFT: alignInt = TextAlignmentProp.LEFT;break; |
| case CENTER: alignInt = TextAlignmentProp.CENTER; break; |
| case RIGHT: alignInt = TextAlignmentProp.RIGHT; break; |
| case DIST: alignInt = TextAlignmentProp.DISTRIBUTED; break; |
| case JUSTIFY: alignInt = TextAlignmentProp.JUSTIFY; break; |
| case JUSTIFY_LOW: alignInt = TextAlignmentProp.JUSTIFYLOW; break; |
| case THAI_DIST: alignInt = TextAlignmentProp.THAIDISTRIBUTED; break; |
| } |
| } |
| setParagraphTextPropVal("alignment", alignInt); |
| } |
| |
| @Override |
| public TextAlign getTextAlign() { |
| TextProp tp = getPropVal(_paragraphStyle, "alignment"); |
| if (tp == null) { |
| return null; |
| } |
| switch (tp.getValue()) { |
| default: |
| case TextAlignmentProp.LEFT: return TextAlign.LEFT; |
| case TextAlignmentProp.CENTER: return TextAlign.CENTER; |
| case TextAlignmentProp.RIGHT: return TextAlign.RIGHT; |
| case TextAlignmentProp.JUSTIFY: return TextAlign.JUSTIFY; |
| case TextAlignmentProp.JUSTIFYLOW: return TextAlign.JUSTIFY_LOW; |
| case TextAlignmentProp.DISTRIBUTED: return TextAlign.DIST; |
| case TextAlignmentProp.THAIDISTRIBUTED: return TextAlign.THAI_DIST; |
| } |
| } |
| |
| @Override |
| public FontAlign getFontAlign() { |
| TextProp tp = getPropVal(_paragraphStyle, FontAlignmentProp.NAME); |
| if (tp == null) { |
| return null; |
| } |
| |
| switch (tp.getValue()) { |
| case FontAlignmentProp.BASELINE: return FontAlign.BASELINE; |
| case FontAlignmentProp.TOP: return FontAlign.TOP; |
| case FontAlignmentProp.CENTER: return FontAlign.CENTER; |
| case FontAlignmentProp.BOTTOM: return FontAlign.BOTTOM; |
| default: return FontAlign.AUTO; |
| } |
| } |
| |
| public AutoNumberingScheme getAutoNumberingScheme() { |
| if (styleTextProp9Atom == null) { |
| return null; |
| } |
| TextPFException9[] ant = styleTextProp9Atom.getAutoNumberTypes(); |
| int level = getIndentLevel(); |
| if (ant == null || level == -1 || level >= ant.length) { |
| return null; |
| } |
| return ant[level].getAutoNumberScheme(); |
| } |
| |
| public Integer getAutoNumberingStartAt() { |
| if (styleTextProp9Atom == null) { |
| return null; |
| } |
| TextPFException9[] ant = styleTextProp9Atom.getAutoNumberTypes(); |
| int level = getIndentLevel(); |
| if (ant == null || level >= ant.length) { |
| return null; |
| } |
| Short startAt = ant[level].getAutoNumberStartNumber(); |
| assert(startAt != null); |
| return startAt.intValue(); |
| } |
| |
| |
| @Override |
| public BulletStyle getBulletStyle() { |
| if (!isBullet() && getAutoNumberingScheme() == null) { |
| return null; |
| } |
| |
| return new BulletStyle() { |
| @Override |
| public String getBulletCharacter() { |
| Character chr = HSLFTextParagraph.this.getBulletChar(); |
| return (chr == null || chr == 0) ? "" : "" + chr; |
| } |
| |
| @Override |
| public String getBulletFont() { |
| return HSLFTextParagraph.this.getBulletFont(); |
| } |
| |
| @Override |
| public Double getBulletFontSize() { |
| return HSLFTextParagraph.this.getBulletSize(); |
| } |
| |
| @Override |
| public void setBulletFontColor(Color color) { |
| setBulletFontColor(DrawPaint.createSolidPaint(color)); |
| } |
| |
| @Override |
| public void setBulletFontColor(PaintStyle color) { |
| if (!(color instanceof SolidPaint)) { |
| throw new IllegalArgumentException("HSLF only supports SolidPaint"); |
| } |
| SolidPaint sp = (SolidPaint)color; |
| Color col = DrawPaint.applyColorTransform(sp.getSolidColor()); |
| HSLFTextParagraph.this.setBulletColor(col); |
| } |
| |
| @Override |
| public PaintStyle getBulletFontColor() { |
| Color col = HSLFTextParagraph.this.getBulletColor(); |
| return DrawPaint.createSolidPaint(col); |
| } |
| |
| @Override |
| public AutoNumberingScheme getAutoNumberingScheme() { |
| return HSLFTextParagraph.this.getAutoNumberingScheme(); |
| } |
| |
| @Override |
| public Integer getAutoNumberingStartAt() { |
| return HSLFTextParagraph.this.getAutoNumberingStartAt(); |
| } |
| }; |
| } |
| |
| @Override |
| public void setBulletStyle(Object... styles) { |
| if (styles.length == 0) { |
| setBullet(false); |
| } else { |
| setBullet(true); |
| for (Object ostyle : styles) { |
| if (ostyle instanceof Number) { |
| setBulletSize(((Number)ostyle).doubleValue()); |
| } else if (ostyle instanceof Color) { |
| setBulletColor((Color)ostyle); |
| } else if (ostyle instanceof Character) { |
| setBulletChar((Character)ostyle); |
| } else if (ostyle instanceof String) { |
| setBulletFont((String)ostyle); |
| } else if (ostyle instanceof AutoNumberingScheme) { |
| throw new HSLFException("setting bullet auto-numberin scheme for HSLF not supported ... yet"); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public HSLFTextShape getParentShape() { |
| return _parentShape; |
| } |
| |
| public void setParentShape(HSLFTextShape parentShape) { |
| _parentShape = parentShape; |
| } |
| |
| @Override |
| public int getIndentLevel() { |
| return _paragraphStyle == null ? 0 : _paragraphStyle.getIndentLevel(); |
| } |
| |
| @Override |
| public void setIndentLevel(int level) { |
| if( _paragraphStyle != null ) { |
| _paragraphStyle.setIndentLevel((short)level); |
| } |
| } |
| |
| /** |
| * Sets whether this rich text run has bullets |
| */ |
| public void setBullet(boolean flag) { |
| setFlag(ParagraphFlagsTextProp.BULLET_IDX, flag); |
| } |
| |
| /** |
| * Returns whether this rich text run has bullets |
| */ |
| public boolean isBullet() { |
| return getFlag(ParagraphFlagsTextProp.BULLET_IDX); |
| } |
| |
| /** |
| * Sets the bullet character |
| */ |
| public void setBulletChar(Character c) { |
| Integer val = (c == null) ? null : (int)c.charValue(); |
| setParagraphTextPropVal("bullet.char", val); |
| } |
| |
| /** |
| * Returns the bullet character |
| */ |
| public Character getBulletChar() { |
| TextProp tp = getPropVal(_paragraphStyle, "bullet.char"); |
| return (tp == null) ? null : (char)tp.getValue(); |
| } |
| |
| /** |
| * Sets the bullet size |
| */ |
| public void setBulletSize(Double size) { |
| setPctOrPoints("bullet.size", size); |
| } |
| |
| /** |
| * Returns the bullet size, null if unset |
| */ |
| public Double getBulletSize() { |
| return getPctOrPoints("bullet.size"); |
| } |
| |
| /** |
| * Sets the bullet color |
| */ |
| public void setBulletColor(Color color) { |
| Integer val = (color == null) ? null : new Color(color.getBlue(), color.getGreen(), color.getRed(), 254).getRGB(); |
| setParagraphTextPropVal("bullet.color", val); |
| setFlag(ParagraphFlagsTextProp.BULLET_HARDCOLOR_IDX, (color != null)); |
| } |
| |
| /** |
| * Returns the bullet color |
| */ |
| public Color getBulletColor() { |
| TextProp tp = getPropVal(_paragraphStyle, "bullet.color"); |
| boolean hasColor = getFlag(ParagraphFlagsTextProp.BULLET_HARDCOLOR_IDX); |
| if (tp == null || !hasColor) { |
| // if bullet color is undefined, return color of first run |
| if (_runs.isEmpty()) { |
| return null; |
| } |
| |
| SolidPaint sp = _runs.get(0).getFontColor(); |
| if(sp == null) { |
| return null; |
| } |
| |
| return DrawPaint.applyColorTransform(sp.getSolidColor()); |
| } |
| |
| return getColorFromColorIndexStruct(tp.getValue(), _sheet); |
| } |
| |
| /** |
| * Sets the bullet font |
| */ |
| public void setBulletFont(String typeface) { |
| if (typeface == null) { |
| setPropVal(_paragraphStyle, "bullet.font", null); |
| setFlag(ParagraphFlagsTextProp.BULLET_HARDFONT_IDX, false); |
| return; |
| } |
| |
| HSLFFontInfo fi = new HSLFFontInfo(typeface); |
| fi = getSheet().getSlideShow().addFont(fi); |
| |
| setParagraphTextPropVal("bullet.font", fi.getIndex()); |
| setFlag(ParagraphFlagsTextProp.BULLET_HARDFONT_IDX, true); |
| } |
| |
| /** |
| * Returns the bullet font |
| */ |
| public String getBulletFont() { |
| TextProp tp = getPropVal(_paragraphStyle, "bullet.font"); |
| boolean hasFont = getFlag(ParagraphFlagsTextProp.BULLET_HARDFONT_IDX); |
| if (tp == null || !hasFont) { |
| return getDefaultFontFamily(); |
| } |
| HSLFFontInfo ppFont = getSheet().getSlideShow().getFont(tp.getValue()); |
| assert(ppFont != null); |
| return ppFont.getTypeface(); |
| } |
| |
| @Override |
| public void setLineSpacing(Double lineSpacing) { |
| setPctOrPoints("linespacing", lineSpacing); |
| } |
| |
| @Override |
| public Double getLineSpacing() { |
| return getPctOrPoints("linespacing"); |
| } |
| |
| @Override |
| public void setSpaceBefore(Double spaceBefore) { |
| setPctOrPoints("spacebefore", spaceBefore); |
| } |
| |
| @Override |
| public Double getSpaceBefore() { |
| return getPctOrPoints("spacebefore"); |
| } |
| |
| @Override |
| public void setSpaceAfter(Double spaceAfter) { |
| setPctOrPoints("spaceafter", spaceAfter); |
| } |
| |
| @Override |
| public Double getSpaceAfter() { |
| return getPctOrPoints("spaceafter"); |
| } |
| |
| @Override |
| public Double getDefaultTabSize() { |
| // TODO: implement |
| return null; |
| } |
| |
| |
| @Override |
| public List<? extends TabStop> getTabStops() { |
| final List<HSLFTabStop> tabStops; |
| final TextRulerAtom textRuler; |
| if (getSheet() instanceof HSLFSlideMaster) { |
| final HSLFTabStopPropCollection tpc = getMasterPropVal(_paragraphStyle, HSLFTabStopPropCollection.NAME); |
| if (tpc == null) { |
| return null; |
| } |
| tabStops = tpc.getTabStops(); |
| textRuler = null; |
| } else { |
| textRuler = (TextRulerAtom)_headerAtom.getParentRecord().findFirstOfType(RecordTypes.TextRulerAtom.typeID); |
| if (textRuler == null) { |
| return null; |
| } |
| tabStops = textRuler.getTabStops(); |
| } |
| |
| return tabStops.stream().map((tabStop) -> new HSLFTabStopDecorator(tabStop)).collect(Collectors.toList()); |
| } |
| |
| @Override |
| public void addTabStops(final double positionInPoints, final TabStopType tabStopType) { |
| final HSLFTabStop ts = new HSLFTabStop(0, tabStopType); |
| ts.setPositionInPoints(positionInPoints); |
| |
| if (getSheet() instanceof HSLFSlideMaster) { |
| final Consumer<HSLFTabStopPropCollection> con = (tp) -> tp.addTabStop(ts); |
| setPropValInner(_paragraphStyle, HSLFTabStopPropCollection.NAME, con); |
| } else { |
| final RecordContainer cont = _headerAtom.getParentRecord(); |
| TextRulerAtom textRuler = (TextRulerAtom)cont.findFirstOfType(RecordTypes.TextRulerAtom.typeID); |
| if (textRuler == null) { |
| textRuler = TextRulerAtom.getParagraphInstance(); |
| cont.appendChildRecord(textRuler); |
| } |
| textRuler.getTabStops().add(ts); |
| } |
| } |
| |
| @Override |
| public void clearTabStops() { |
| if (getSheet() instanceof HSLFSlideMaster) { |
| setPropValInner(_paragraphStyle, HSLFTabStopPropCollection.NAME, null); |
| } else { |
| final RecordContainer cont = _headerAtom.getParentRecord(); |
| final TextRulerAtom textRuler = (TextRulerAtom)cont.findFirstOfType(RecordTypes.TextRulerAtom.typeID); |
| if (textRuler == null) { |
| return; |
| } |
| textRuler.getTabStops().clear(); |
| } |
| } |
| |
| private Double getPctOrPoints(String propName) { |
| TextProp tp = getPropVal(_paragraphStyle, propName); |
| if (tp == null) { |
| return null; |
| } |
| int val = tp.getValue(); |
| return (val < 0) ? Units.masterToPoints(val) : val; |
| } |
| |
| private void setPctOrPoints(String propName, Double dval) { |
| Integer ival = null; |
| if (dval != null) { |
| ival = (dval < 0) ? Units.pointsToMaster(dval) : dval.intValue(); |
| } |
| setParagraphTextPropVal(propName, ival); |
| } |
| |
| private boolean getFlag(int index) { |
| BitMaskTextProp tp = getPropVal(_paragraphStyle, ParagraphFlagsTextProp.NAME); |
| return (tp == null) ? false : tp.getSubValue(index); |
| } |
| |
| private void setFlag(int index, boolean value) { |
| BitMaskTextProp tp = (BitMaskTextProp)_paragraphStyle.addWithName(ParagraphFlagsTextProp.NAME); |
| tp.setSubValue(value, index); |
| setDirty(); |
| } |
| |
| /** |
| * Fetch the value of the given Paragraph related TextProp. Returns null if |
| * that TextProp isn't present. If the TextProp isn't present, the value |
| * from the appropriate Master Sheet will apply. |
| * |
| * The propName can be a comma-separated list, in case multiple equivalent values |
| * are queried |
| */ |
| protected <T extends TextProp> T getPropVal(TextPropCollection props, String propName) { |
| String[] propNames = propName.split(","); |
| for (String pn : propNames) { |
| T prop = props.findByName(pn); |
| if (isValidProp(prop)) { |
| return prop; |
| } |
| } |
| |
| return getMasterPropVal(props, propName); |
| } |
| |
| private <T extends TextProp> T getMasterPropVal(final TextPropCollection props, final String propName) { |
| boolean isChar = props.getTextPropType() == TextPropType.character; |
| |
| // check if we can delegate to master for the property |
| if (!isChar) { |
| BitMaskTextProp maskProp = props.findByName(ParagraphFlagsTextProp.NAME); |
| boolean hardAttribute = (maskProp != null && maskProp.getValue() == 0); |
| if (hardAttribute) { |
| return null; |
| } |
| } |
| |
| final String[] propNames = propName.split(","); |
| final HSLFSheet sheet = getSheet(); |
| final int txtype = getRunType(); |
| final HSLFMasterSheet master; |
| if (sheet instanceof HSLFMasterSheet) { |
| master = (HSLFMasterSheet)sheet; |
| } else { |
| master = sheet.getMasterSheet(); |
| if (master == null) { |
| LOG.atWarn().log("MasterSheet is not available"); |
| return null; |
| } |
| } |
| |
| for (String pn : propNames) { |
| TextPropCollection masterProps = master.getPropCollection(txtype, getIndentLevel(), pn, isChar); |
| if (masterProps != null) { |
| T prop = masterProps.findByName(pn); |
| if (isValidProp(prop)) { |
| return prop; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| private static boolean isValidProp(TextProp prop) { |
| // Font properties (maybe other too???) can have an index of -1 |
| // so we check the master for this font index then |
| return prop != null && (!prop.getName().contains("font") || prop.getValue() != -1); |
| } |
| |
| /** |
| * Returns the named TextProp, either by fetching it (if it exists) or |
| * adding it (if it didn't) |
| * |
| * @param props the TextPropCollection to fetch from / add into |
| * @param name the name of the TextProp to fetch/add |
| * @param val the value, null if unset |
| */ |
| protected void setPropVal(final TextPropCollection props, final String name, final Integer val) { |
| setPropValInner(props, name, val == null ? null : tp -> tp.setValue(val)); |
| } |
| |
| private void setPropValInner(final TextPropCollection props, final String name, Consumer<? extends TextProp> handler) { |
| final boolean isChar = props.getTextPropType() == TextPropType.character; |
| |
| final TextPropCollection pc; |
| if (_sheet instanceof HSLFMasterSheet) { |
| pc = ((HSLFMasterSheet)_sheet).getPropCollection(getRunType(), getIndentLevel(), "*", isChar); |
| if (pc == null) { |
| throw new HSLFException("Master text property collection can't be determined."); |
| } |
| } else { |
| pc = props; |
| } |
| |
| if (handler == null) { |
| pc.removeByName(name); |
| } else { |
| // Fetch / Add the TextProp |
| handler.accept(pc.addWithName(name)); |
| } |
| setDirty(); |
| } |
| |
| |
| /** |
| * Check and add linebreaks to text runs leading other paragraphs |
| */ |
| protected static void fixLineEndings(List<HSLFTextParagraph> paragraphs) { |
| HSLFTextRun lastRun = null; |
| for (HSLFTextParagraph p : paragraphs) { |
| if (lastRun != null && !lastRun.getRawText().endsWith("\r")) { |
| lastRun.setText(lastRun.getRawText() + "\r"); |
| } |
| List<HSLFTextRun> ltr = p.getTextRuns(); |
| if (ltr.isEmpty()) { |
| throw new HSLFException("paragraph without textruns found"); |
| } |
| lastRun = ltr.get(ltr.size() - 1); |
| assert (lastRun.getRawText() != null); |
| } |
| } |
| |
| /** |
| * Search for a StyleTextPropAtom is for this text header (list of paragraphs) |
| * |
| * @param header the header |
| * @param textLen the length of the rawtext, or -1 if the length is not known |
| */ |
| private static StyleTextPropAtom findStyleAtomPresent(TextHeaderAtom header, int textLen) { |
| boolean afterHeader = false; |
| StyleTextPropAtom style = null; |
| for (Record record : header.getParentRecord().getChildRecords()) { |
| long rt = record.getRecordType(); |
| if (afterHeader && rt == RecordTypes.TextHeaderAtom.typeID) { |
| // already on the next header, quit searching |
| break; |
| } |
| afterHeader |= (header == record); |
| if (afterHeader && rt == RecordTypes.StyleTextPropAtom.typeID) { |
| // found it |
| style = (StyleTextPropAtom) record; |
| } |
| } |
| |
| if (style == null) { |
| LOG.atInfo().log("styles atom doesn't exist. Creating dummy record for later saving."); |
| style = new StyleTextPropAtom((textLen < 0) ? 1 : textLen); |
| } else { |
| if (textLen >= 0) { |
| style.setParentTextSize(textLen); |
| } |
| } |
| |
| return style; |
| } |
| |
| /** |
| * Saves the modified paragraphs/textrun to the records. |
| * Also updates the styles to the correct text length. |
| */ |
| protected static void storeText(List<HSLFTextParagraph> paragraphs) { |
| fixLineEndings(paragraphs); |
| updateTextAtom(paragraphs); |
| updateStyles(paragraphs); |
| updateHyperlinks(paragraphs); |
| refreshRecords(paragraphs); |
| |
| for (HSLFTextParagraph p : paragraphs) { |
| p._dirty = false; |
| } |
| } |
| |
| /** |
| * Set the correct text atom depending on the multibyte usage |
| */ |
| private static void updateTextAtom(List<HSLFTextParagraph> paragraphs) { |
| final String rawText = toInternalString(getRawText(paragraphs)); |
| |
| // Will it fit in a 8 bit atom? |
| boolean isUnicode = StringUtil.hasMultibyte(rawText); |
| // isUnicode = true; |
| |
| TextHeaderAtom headerAtom = paragraphs.get(0)._headerAtom; |
| TextBytesAtom byteAtom = paragraphs.get(0)._byteAtom; |
| TextCharsAtom charAtom = paragraphs.get(0)._charAtom; |
| StyleTextPropAtom styleAtom = findStyleAtomPresent(headerAtom, rawText.length()); |
| |
| // Store in the appropriate record |
| Record oldRecord = null, newRecord; |
| if (isUnicode) { |
| if (byteAtom != null || charAtom == null) { |
| oldRecord = byteAtom; |
| charAtom = new TextCharsAtom(); |
| } |
| newRecord = charAtom; |
| charAtom.setText(rawText); |
| } else { |
| if (charAtom != null || byteAtom == null) { |
| oldRecord = charAtom; |
| byteAtom = new TextBytesAtom(); |
| } |
| newRecord = byteAtom; |
| byte[] byteText = new byte[rawText.length()]; |
| StringUtil.putCompressedUnicode(rawText, byteText, 0); |
| byteAtom.setText(byteText); |
| } |
| assert (newRecord != null); |
| |
| RecordContainer _txtbox = headerAtom.getParentRecord(); |
| Record[] cr = _txtbox.getChildRecords(); |
| int /* headerIdx = -1, */ textIdx = -1, styleIdx = -1; |
| for (int i = 0; i < cr.length; i++) { |
| Record r = cr[i]; |
| if (r == headerAtom) { |
| // headerIdx = i; |
| } else if (r == oldRecord || r == newRecord) { |
| textIdx = i; |
| } else if (r == styleAtom) { |
| styleIdx = i; |
| } |
| } |
| |
| if (textIdx == -1) { |
| // the old record was never registered, ignore it |
| _txtbox.addChildAfter(newRecord, headerAtom); |
| // textIdx = headerIdx + 1; |
| } else { |
| // swap not appropriated records - noop if unchanged |
| cr[textIdx] = newRecord; |
| } |
| |
| if (styleIdx == -1) { |
| // Add the new StyleTextPropAtom after the TextCharsAtom / TextBytesAtom |
| _txtbox.addChildAfter(styleAtom, newRecord); |
| } |
| |
| for (HSLFTextParagraph p : paragraphs) { |
| if (newRecord == byteAtom) { |
| p._byteAtom = byteAtom; |
| p._charAtom = null; |
| } else { |
| p._byteAtom = null; |
| p._charAtom = charAtom; |
| } |
| } |
| |
| } |
| |
| /** |
| * Update paragraph and character styles - merges them when subsequential styles match |
| */ |
| private static void updateStyles(List<HSLFTextParagraph> paragraphs) { |
| final String rawText = toInternalString(getRawText(paragraphs)); |
| TextHeaderAtom headerAtom = paragraphs.get(0)._headerAtom; |
| StyleTextPropAtom styleAtom = findStyleAtomPresent(headerAtom, rawText.length()); |
| |
| // Update the text length for its Paragraph and Character stylings |
| // * reset the length, to the new string's length |
| // * add on +1 if the last block |
| |
| styleAtom.clearStyles(); |
| |
| TextPropCollection lastPTPC = null, lastRTPC = null, ptpc = null, rtpc = null; |
| for (HSLFTextParagraph para : paragraphs) { |
| ptpc = para.getParagraphStyle(); |
| ptpc.updateTextSize(0); |
| if (!ptpc.equals(lastPTPC)) { |
| lastPTPC = ptpc.copy(); |
| lastPTPC.updateTextSize(0); |
| styleAtom.addParagraphTextPropCollection(lastPTPC); |
| } |
| for (HSLFTextRun tr : para.getTextRuns()) { |
| rtpc = tr.getCharacterStyle(); |
| rtpc.updateTextSize(0); |
| if (!rtpc.equals(lastRTPC)) { |
| lastRTPC = rtpc.copy(); |
| lastRTPC.updateTextSize(0); |
| styleAtom.addCharacterTextPropCollection(lastRTPC); |
| } |
| int len = tr.getLength(); |
| ptpc.updateTextSize(ptpc.getCharactersCovered() + len); |
| rtpc.updateTextSize(len); |
| lastPTPC.updateTextSize(lastPTPC.getCharactersCovered() + len); |
| lastRTPC.updateTextSize(lastRTPC.getCharactersCovered() + len); |
| } |
| } |
| |
| if (lastPTPC == null || lastRTPC == null || ptpc == null || rtpc == null) { // NOSONAR |
| throw new HSLFException("Not all TextPropCollection could be determined."); |
| } |
| |
| ptpc.updateTextSize(ptpc.getCharactersCovered() + 1); |
| rtpc.updateTextSize(rtpc.getCharactersCovered() + 1); |
| lastPTPC.updateTextSize(lastPTPC.getCharactersCovered() + 1); |
| lastRTPC.updateTextSize(lastRTPC.getCharactersCovered() + 1); |
| |
| // If TextSpecInfoAtom is present, we must update the text size in it, |
| // otherwise the ppt will be corrupted |
| for (Record r : paragraphs.get(0).getRecords()) { |
| if (r instanceof TextSpecInfoAtom) { |
| ((TextSpecInfoAtom) r).setParentSize(rawText.length() + 1); |
| break; |
| } |
| } |
| } |
| |
| private static void updateHyperlinks(List<HSLFTextParagraph> paragraphs) { |
| TextHeaderAtom headerAtom = paragraphs.get(0)._headerAtom; |
| RecordContainer _txtbox = headerAtom.getParentRecord(); |
| // remove existing hyperlink records |
| for (Record r : _txtbox.getChildRecords()) { |
| if (r instanceof InteractiveInfo || r instanceof TxInteractiveInfoAtom) { |
| _txtbox.removeChild(r); |
| } |
| } |
| // now go through all the textruns and check for hyperlinks |
| HSLFHyperlink lastLink = null; |
| for (HSLFTextParagraph para : paragraphs) { |
| for (HSLFTextRun run : para) { |
| HSLFHyperlink thisLink = run.getHyperlink(); |
| if (thisLink != null && thisLink == lastLink) { |
| // the hyperlink extends over this text run, increase its length |
| // TODO: the text run might be longer than the hyperlink |
| thisLink.setEndIndex(thisLink.getEndIndex()+run.getLength()); |
| } else { |
| if (lastLink != null) { |
| InteractiveInfo info = lastLink.getInfo(); |
| TxInteractiveInfoAtom txinfo = lastLink.getTextRunInfo(); |
| assert(info != null && txinfo != null); |
| _txtbox.appendChildRecord(info); |
| _txtbox.appendChildRecord(txinfo); |
| } |
| } |
| lastLink = thisLink; |
| } |
| } |
| |
| if (lastLink != null) { |
| InteractiveInfo info = lastLink.getInfo(); |
| TxInteractiveInfoAtom txinfo = lastLink.getTextRunInfo(); |
| assert(info != null && txinfo != null); |
| _txtbox.appendChildRecord(info); |
| _txtbox.appendChildRecord(txinfo); |
| } |
| } |
| |
| /** |
| * Writes the textbox records back to the document record |
| */ |
| private static void refreshRecords(List<HSLFTextParagraph> paragraphs) { |
| TextHeaderAtom headerAtom = paragraphs.get(0)._headerAtom; |
| RecordContainer _txtbox = headerAtom.getParentRecord(); |
| if (_txtbox instanceof EscherTextboxWrapper) { |
| try { |
| _txtbox.writeOut(null); |
| } catch (IOException e) { |
| throw new HSLFException("failed dummy write", e); |
| } |
| } |
| } |
| |
| /** |
| * Adds the supplied text onto the end of the TextParagraphs, |
| * creating a new RichTextRun for it to sit in. |
| * |
| * @param text the text string used by this object. |
| */ |
| protected static HSLFTextRun appendText(List<HSLFTextParagraph> paragraphs, String text, boolean newParagraph) { |
| text = toInternalString(text); |
| |
| // check paragraphs |
| assert(!paragraphs.isEmpty() && !paragraphs.get(0).getTextRuns().isEmpty()); |
| |
| HSLFTextParagraph htp = paragraphs.get(paragraphs.size() - 1); |
| HSLFTextRun htr = htp.getTextRuns().get(htp.getTextRuns().size() - 1); |
| |
| boolean addParagraph = newParagraph; |
| for (String rawText : text.split("(?<=\r)")) { |
| // special case, if last text paragraph or run is empty, we will reuse it |
| boolean lastRunEmpty = (htr.getLength() == 0); |
| boolean lastParaEmpty = lastRunEmpty && (htp.getTextRuns().size() == 1); |
| |
| if (addParagraph && !lastParaEmpty) { |
| TextPropCollection tpc = htp.getParagraphStyle(); |
| HSLFTextParagraph prevHtp = htp; |
| htp = new HSLFTextParagraph(htp._headerAtom, htp._byteAtom, htp._charAtom, paragraphs); |
| htp.setParagraphStyle(tpc.copy()); |
| htp.setParentShape(prevHtp.getParentShape()); |
| htp.setShapeId(prevHtp.getShapeId()); |
| htp.supplySheet(prevHtp.getSheet()); |
| paragraphs.add(htp); |
| } |
| addParagraph = true; |
| |
| if (!lastRunEmpty) { |
| TextPropCollection tpc = htr.getCharacterStyle(); |
| htr = new HSLFTextRun(htp); |
| htr.setCharacterStyle(tpc.copy()); |
| htp.addTextRun(htr); |
| } |
| htr.setText(rawText); |
| } |
| |
| storeText(paragraphs); |
| |
| return htr; |
| } |
| |
| /** |
| * Sets (overwrites) the current text. |
| * Uses the properties of the first paragraph / textrun |
| * |
| * @param text the text string used by this object. |
| */ |
| public static HSLFTextRun setText(List<HSLFTextParagraph> paragraphs, String text) { |
| // check paragraphs |
| assert(!paragraphs.isEmpty() && !paragraphs.get(0).getTextRuns().isEmpty()); |
| |
| Iterator<HSLFTextParagraph> paraIter = paragraphs.iterator(); |
| HSLFTextParagraph htp = paraIter.hasNext() ? paraIter.next() : null; // keep first |
| assert (htp != null); |
| while (paraIter.hasNext()) { |
| paraIter.next(); |
| paraIter.remove(); |
| } |
| |
| Iterator<HSLFTextRun> runIter = htp.getTextRuns().iterator(); |
| if (runIter.hasNext()) { |
| HSLFTextRun htr = runIter.next(); |
| htr.setText(""); |
| while (runIter.hasNext()) { |
| runIter.next(); |
| runIter.remove(); |
| } |
| } else { |
| HSLFTextRun trun = new HSLFTextRun(htp); |
| htp.addTextRun(trun); |
| } |
| |
| return appendText(paragraphs, text, false); |
| } |
| |
| public static String getText(List<HSLFTextParagraph> paragraphs) { |
| assert (!paragraphs.isEmpty()); |
| String rawText = getRawText(paragraphs); |
| return toExternalString(rawText, paragraphs.get(0).getRunType()); |
| } |
| |
| public static String getRawText(List<HSLFTextParagraph> paragraphs) { |
| StringBuilder sb = new StringBuilder(); |
| for (HSLFTextParagraph p : paragraphs) { |
| for (HSLFTextRun r : p.getTextRuns()) { |
| sb.append(r.getRawText()); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| for (HSLFTextRun r : getTextRuns()) { |
| sb.append(r.getRawText()); |
| } |
| return toExternalString(sb.toString(), getRunType()); |
| } |
| |
| /** |
| * Returns a new string with line breaks converted into internal ppt |
| * representation |
| */ |
| protected static String toInternalString(String s) { |
| return s.replaceAll("\\r?\\n", "\r"); |
| } |
| |
| /** |
| * Converts raw text from the text paragraphs to a formatted string, |
| * i.e. it converts certain control characters used in the raw txt |
| * |
| * @param rawText the raw text |
| * @param runType the run type of the shape, paragraph or headerAtom. |
| * use -1 if unknown |
| * @return the formatted string |
| */ |
| public static String toExternalString(String rawText, int runType) { |
| // PowerPoint seems to store files with \r as the line break |
| // The messes things up on everything but a Mac, so translate |
| // them to \n |
| String text = rawText.replace('\r', '\n'); |
| |
| // 0xB acts like carriage return in page titles and like blank in the others |
| final char repl = (runType == -1 || |
| runType == TextPlaceholder.TITLE.nativeId || |
| runType == TextPlaceholder.CENTER_TITLE.nativeId) ? '\n' : ' '; |
| |
| return text.replace('\u000b', repl); |
| } |
| |
| /** |
| * For a given PPDrawing, grab all the TextRuns |
| */ |
| public static List<List<HSLFTextParagraph>> findTextParagraphs(PPDrawing ppdrawing, HSLFSheet sheet) { |
| List<List<HSLFTextParagraph>> runsV = new ArrayList<>(); |
| for (EscherTextboxWrapper wrapper : ppdrawing.getTextboxWrappers()) { |
| List<HSLFTextParagraph> p = findTextParagraphs(wrapper, sheet); |
| if (p != null) { |
| runsV.add(p); |
| } |
| } |
| return runsV; |
| } |
| |
| /** |
| * Scans through the supplied record array, looking for |
| * a TextHeaderAtom followed by one of a TextBytesAtom or |
| * a TextCharsAtom. Builds up TextRuns from these |
| * |
| * @param wrapper an EscherTextboxWrapper |
| */ |
| protected static List<HSLFTextParagraph> findTextParagraphs(EscherTextboxWrapper wrapper, HSLFSheet sheet) { |
| // propagate parents to parent-aware records |
| RecordContainer.handleParentAwareRecords(wrapper); |
| int shapeId = wrapper.getShapeId(); |
| List<HSLFTextParagraph> rv = null; |
| |
| OutlineTextRefAtom ota = (OutlineTextRefAtom)wrapper.findFirstOfType(OutlineTextRefAtom.typeID); |
| if (ota != null) { |
| // if we are based on an outline, there are no further records to be parsed from the wrapper |
| if (sheet == null) { |
| throw new HSLFException("Outline atom reference can't be solved without a sheet record"); |
| } |
| |
| List<List<HSLFTextParagraph>> sheetRuns = sheet.getTextParagraphs(); |
| assert (sheetRuns != null); |
| |
| int idx = ota.getTextIndex(); |
| for (List<HSLFTextParagraph> r : sheetRuns) { |
| if (r.isEmpty()) { |
| continue; |
| } |
| int ridx = r.get(0).getIndex(); |
| if (ridx > idx) { |
| break; |
| } |
| if (ridx == idx) { |
| if (rv == null) { |
| rv = r; |
| } else { |
| // create a new container |
| // TODO: ... is this case really happening? |
| rv = new ArrayList<>(rv); |
| rv.addAll(r); |
| } |
| } |
| } |
| if (rv == null || rv.isEmpty()) { |
| LOG.atWarn().log("text run not found for OutlineTextRefAtom.TextIndex={}", box(idx)); |
| } |
| } else { |
| if (sheet != null) { |
| // check sheet runs first, so we get exactly the same paragraph list |
| List<List<HSLFTextParagraph>> sheetRuns = sheet.getTextParagraphs(); |
| assert (sheetRuns != null); |
| |
| for (List<HSLFTextParagraph> paras : sheetRuns) { |
| if (!paras.isEmpty() && paras.get(0)._headerAtom.getParentRecord() == wrapper) { |
| rv = paras; |
| break; |
| } |
| } |
| } |
| |
| if (rv == null) { |
| // if we haven't found the wrapper in the sheet runs, create a new paragraph list from its record |
| List<List<HSLFTextParagraph>> rvl = findTextParagraphs(wrapper.getChildRecords()); |
| switch (rvl.size()) { |
| case 0: break; // nothing found |
| case 1: rv = rvl.get(0); break; // normal case |
| default: |
| throw new HSLFException("TextBox contains more than one list of paragraphs."); |
| } |
| } |
| } |
| |
| if (rv != null) { |
| StyleTextProp9Atom styleTextProp9Atom = wrapper.getStyleTextProp9Atom(); |
| |
| for (HSLFTextParagraph htp : rv) { |
| htp.setShapeId(shapeId); |
| htp.setStyleTextProp9Atom(styleTextProp9Atom); |
| } |
| } |
| return rv; |
| } |
| |
| /** |
| * Scans through the supplied record array, looking for |
| * a TextHeaderAtom followed by one of a TextBytesAtom or |
| * a TextCharsAtom. Builds up TextRuns from these |
| * |
| * @param records the records to build from |
| */ |
| protected static List<List<HSLFTextParagraph>> findTextParagraphs(Record[] records) { |
| List<List<HSLFTextParagraph>> paragraphCollection = new ArrayList<>(); |
| |
| int[] recordIdx = { 0 }; |
| |
| for (int slwtIndex = 0; recordIdx[0] < records.length; slwtIndex++) { |
| TextHeaderAtom header = null; |
| TextBytesAtom tbytes = null; |
| TextCharsAtom tchars = null; |
| TextRulerAtom ruler = null; |
| MasterTextPropAtom indents = null; |
| |
| for (Record r : getRecords(records, recordIdx, null)) { |
| long rt = r.getRecordType(); |
| if (RecordTypes.TextHeaderAtom.typeID == rt) { |
| header = (TextHeaderAtom) r; |
| } else if (RecordTypes.TextBytesAtom.typeID == rt) { |
| tbytes = (TextBytesAtom) r; |
| } else if (RecordTypes.TextCharsAtom.typeID == rt) { |
| tchars = (TextCharsAtom) r; |
| } else if (RecordTypes.TextRulerAtom.typeID == rt) { |
| ruler = (TextRulerAtom) r; |
| } else if (RecordTypes.MasterTextPropAtom.typeID == rt) { |
| indents = (MasterTextPropAtom) r; |
| } |
| // don't search for RecordTypes.StyleTextPropAtom.typeID here ... see findStyleAtomPresent below |
| } |
| |
| if (header == null) { |
| break; |
| } |
| |
| if (header.getParentRecord() instanceof SlideListWithText) { |
| // runs found in PPDrawing are not linked with SlideListWithTexts |
| header.setIndex(slwtIndex); |
| } |
| |
| if (tbytes == null && tchars == null) { |
| tbytes = new TextBytesAtom(); |
| // don't add record yet - set it in storeText |
| LOG.atInfo().log("bytes nor chars atom doesn't exist. Creating dummy record for later saving."); |
| } |
| |
| String rawText = (tchars != null) ? tchars.getText() : tbytes.getText(); |
| StyleTextPropAtom styles = findStyleAtomPresent(header, rawText.length()); |
| |
| List<HSLFTextParagraph> paragraphs = new ArrayList<>(); |
| paragraphCollection.add(paragraphs); |
| |
| // split, but keep delimiter |
| for (String para : rawText.split("(?<=\r)")) { |
| HSLFTextParagraph tpara = new HSLFTextParagraph(header, tbytes, tchars, paragraphs); |
| paragraphs.add(tpara); |
| tpara._ruler = ruler; |
| tpara.getParagraphStyle().updateTextSize(para.length()); |
| |
| HSLFTextRun trun = new HSLFTextRun(tpara); |
| tpara.addTextRun(trun); |
| trun.setText(para); |
| } |
| |
| applyCharacterStyles(paragraphs, styles.getCharacterStyles()); |
| applyParagraphStyles(paragraphs, styles.getParagraphStyles()); |
| if (indents != null) { |
| applyParagraphIndents(paragraphs, indents.getIndents()); |
| } |
| } |
| |
| if (paragraphCollection.isEmpty()) { |
| LOG.atDebug().log("No text records found."); |
| } |
| |
| return paragraphCollection; |
| } |
| |
| protected static void applyHyperlinks(List<HSLFTextParagraph> paragraphs) { |
| List<HSLFHyperlink> links = HSLFHyperlink.find(paragraphs); |
| |
| for (HSLFHyperlink h : links) { |
| int csIdx = 0; |
| for (HSLFTextParagraph p : paragraphs) { |
| if (csIdx > h.getEndIndex()) { |
| break; |
| } |
| List<HSLFTextRun> runs = p.getTextRuns(); |
| for (int rlen=0,rIdx=0; rIdx < runs.size(); csIdx+=rlen, rIdx++) { |
| HSLFTextRun run = runs.get(rIdx); |
| rlen = run.getLength(); |
| if (csIdx < h.getEndIndex() && h.getStartIndex() < csIdx+rlen) { |
| String rawText = run.getRawText(); |
| int startIdx = h.getStartIndex()-csIdx; |
| if (startIdx > 0) { |
| // hyperlink starts within current textrun |
| HSLFTextRun newRun = new HSLFTextRun(p); |
| newRun.setCharacterStyle(run.getCharacterStyle()); |
| newRun.setText(rawText.substring(startIdx)); |
| run.setText(rawText.substring(0, startIdx)); |
| runs.add(rIdx+1, newRun); |
| rlen = startIdx; |
| continue; |
| } |
| int endIdx = Math.min(rlen, h.getEndIndex()-h.getStartIndex()); |
| if (endIdx < rlen) { |
| // hyperlink ends before end of current textrun |
| HSLFTextRun newRun = new HSLFTextRun(p); |
| newRun.setCharacterStyle(run.getCharacterStyle()); |
| newRun.setText(rawText.substring(0, endIdx)); |
| run.setText(rawText.substring(endIdx)); |
| runs.add(rIdx, newRun); |
| rlen = endIdx; |
| run = newRun; |
| } |
| run.setHyperlink(h); |
| } |
| } |
| } |
| } |
| } |
| |
| protected static void applyCharacterStyles(List<HSLFTextParagraph> paragraphs, List<TextPropCollection> charStyles) { |
| int paraIdx = 0, runIdx = 0; |
| HSLFTextRun trun; |
| |
| for (int csIdx = 0; csIdx < charStyles.size(); csIdx++) { |
| TextPropCollection p = charStyles.get(csIdx); |
| for (int ccRun = 0, ccStyle = p.getCharactersCovered(); ccRun < ccStyle;) { |
| HSLFTextParagraph para = paragraphs.get(paraIdx); |
| List<HSLFTextRun> runs = para.getTextRuns(); |
| trun = runs.get(runIdx); |
| final int len = trun.getLength(); |
| |
| if (ccRun + len <= ccStyle) { |
| ccRun += len; |
| } else { |
| String text = trun.getRawText(); |
| trun.setText(text.substring(0, ccStyle - ccRun)); |
| |
| HSLFTextRun nextRun = new HSLFTextRun(para); |
| nextRun.setText(text.substring(ccStyle - ccRun)); |
| runs.add(runIdx + 1, nextRun); |
| |
| ccRun += ccStyle - ccRun; |
| } |
| |
| trun.setCharacterStyle(p); |
| |
| if (paraIdx == paragraphs.size()-1 && runIdx == runs.size()-1) { |
| if (csIdx < charStyles.size() - 1) { |
| // special case, empty trailing text run |
| HSLFTextRun nextRun = new HSLFTextRun(para); |
| nextRun.setText(""); |
| runs.add(nextRun); |
| ccRun++; |
| } else { |
| // need to add +1 to the last run of the last paragraph |
| trun.getCharacterStyle().updateTextSize(trun.getLength()+1); |
| ccRun++; |
| } |
| } |
| |
| // need to compare it again, in case a run has been added after |
| if (++runIdx == runs.size()) { |
| paraIdx++; |
| runIdx = 0; |
| } |
| } |
| } |
| } |
| |
| protected static void applyParagraphStyles(List<HSLFTextParagraph> paragraphs, List<TextPropCollection> paraStyles) { |
| int paraIdx = 0; |
| for (TextPropCollection p : paraStyles) { |
| for (int ccPara = 0, ccStyle = p.getCharactersCovered(); ccPara < ccStyle; paraIdx++) { |
| if (paraIdx >= paragraphs.size()) { |
| return; |
| } |
| HSLFTextParagraph htp = paragraphs.get(paraIdx); |
| TextPropCollection pCopy = p.copy(); |
| htp.setParagraphStyle(pCopy); |
| int len = 0; |
| for (HSLFTextRun trun : htp.getTextRuns()) { |
| len += trun.getLength(); |
| } |
| if (paraIdx == paragraphs.size()-1) { |
| len++; |
| } |
| pCopy.updateTextSize(len); |
| ccPara += len; |
| } |
| } |
| } |
| |
| protected static void applyParagraphIndents(List<HSLFTextParagraph> paragraphs, List<IndentProp> paraStyles) { |
| int paraIdx = 0; |
| for (IndentProp p : paraStyles) { |
| for (int ccPara = 0, ccStyle = p.getCharactersCovered(); ccPara < ccStyle; paraIdx++) { |
| if (paraIdx >= paragraphs.size() || ccPara >= ccStyle-1) { |
| return; |
| } |
| HSLFTextParagraph para = paragraphs.get(paraIdx); |
| int len = 0; |
| for (HSLFTextRun trun : para.getTextRuns()) { |
| len += trun.getLength(); |
| } |
| para.setIndentLevel(p.getIndentLevel()); |
| ccPara += len + 1; |
| } |
| } |
| } |
| |
| public EscherTextboxWrapper getTextboxWrapper() { |
| return (EscherTextboxWrapper) _headerAtom.getParentRecord(); |
| } |
| |
| protected static Color getColorFromColorIndexStruct(int rgb, HSLFSheet sheet) { |
| int cidx = rgb >>> 24; |
| Color tmp; |
| switch (cidx) { |
| // Background ... Accent 3 color |
| case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: |
| if (sheet == null) { |
| return null; |
| } |
| ColorSchemeAtom ca = sheet.getColorScheme(); |
| tmp = new Color(ca.getColor(cidx), true); |
| break; |
| // Color is an sRGB value specified by red, green, and blue fields. |
| case 0xFE: |
| tmp = new Color(rgb, true); |
| break; |
| // Color is undefined. |
| default: |
| case 0xFF: |
| return null; |
| } |
| return new Color(tmp.getBlue(), tmp.getGreen(), tmp.getRed()); |
| } |
| |
| /** |
| * Sets the value of the given Paragraph TextProp, add if required |
| * @param propName The name of the Paragraph TextProp |
| * @param val The value to set for the TextProp |
| */ |
| public void setParagraphTextPropVal(String propName, Integer val) { |
| setPropVal(_paragraphStyle, propName, val); |
| setDirty(); |
| } |
| |
| /** |
| * marks this paragraph dirty, so its records will be renewed on save |
| */ |
| public void setDirty() { |
| _dirty = true; |
| } |
| |
| public boolean isDirty() { |
| return _dirty; |
| } |
| |
| /** |
| * Calculates the start index of the given text run |
| * |
| * @param textrun the text run to search for |
| * @return the start index with the paragraph collection or -1 if not found |
| */ |
| /* package */ int getStartIdxOfTextRun(HSLFTextRun textrun) { |
| int idx = 0; |
| for (HSLFTextParagraph p : parentList) { |
| for (HSLFTextRun r : p) { |
| if (r == textrun) { |
| return idx; |
| } |
| idx += r.getLength(); |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see RoundTripHFPlaceholder12 |
| */ |
| @Override |
| public boolean isHeaderOrFooter() { |
| HSLFTextShape s = getParentShape(); |
| if (s == null) { |
| return false; |
| } |
| Placeholder ph = s.getPlaceholder(); |
| if (ph == null) { |
| return false; |
| } |
| switch (ph) { |
| case DATETIME: |
| case SLIDE_NUMBER: |
| case FOOTER: |
| case HEADER: |
| return true; |
| default: |
| return false; |
| } |
| } |
| } |