/* ==================================================================== | |
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.sl.draw; | |
import java.awt.Color; | |
import java.awt.Graphics2D; | |
import java.awt.font.*; | |
import java.awt.geom.Rectangle2D; | |
import java.text.*; | |
import java.text.AttributedCharacterIterator.Attribute; | |
import java.util.*; | |
import org.apache.poi.sl.usermodel.*; | |
import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle; | |
import org.apache.poi.sl.usermodel.TextParagraph.TextAlign; | |
import org.apache.poi.sl.usermodel.TextRun.TextCap; | |
import org.apache.poi.util.Units; | |
public class DrawTextParagraph<T extends TextRun> implements Drawable { | |
protected TextParagraph<T> paragraph; | |
double x, y; | |
protected List<DrawTextFragment> lines = new ArrayList<DrawTextFragment>(); | |
protected String rawText; | |
protected DrawTextFragment bullet; | |
protected int autoNbrIdx = 0; | |
/** | |
* the highest line in this paragraph. Used for line spacing. | |
*/ | |
protected double maxLineHeight; | |
public DrawTextParagraph(TextParagraph<T> paragraph) { | |
this.paragraph = paragraph; | |
} | |
public void setPosition(double x, double y) { | |
// TODO: replace it, by applyTransform???? | |
this.x = x; | |
this.y = y; | |
} | |
public double getY() { | |
return y; | |
} | |
/** | |
* Sets the auto numbering index of the handled paragraph | |
* @param index the auto numbering index | |
*/ | |
public void setAutoNumberingIdx(int index) { | |
autoNbrIdx = index; | |
} | |
public void draw(Graphics2D graphics){ | |
if (lines.isEmpty()) return; | |
Insets2D insets = paragraph.getParentShape().getInsets(); | |
double leftInset = insets.left; | |
double rightInset = insets.right; | |
double penY = y; | |
boolean firstLine = true; | |
int indentLevel = paragraph.getIndentLevel(); | |
Double leftMargin = paragraph.getLeftMargin(); | |
if (leftMargin == null) { | |
// if the marL attribute is omitted, then a value of 347663 is implied | |
leftMargin = Units.toPoints(347663*(indentLevel+1)); | |
} | |
Double indent = paragraph.getIndent(); | |
if (indent == null) { | |
indent = Units.toPoints(347663*indentLevel); | |
} | |
Double rightMargin = paragraph.getRightMargin(); | |
if (rightMargin == null) { | |
rightMargin = 0d; | |
} | |
//The vertical line spacing | |
Double spacing = paragraph.getLineSpacing(); | |
if (spacing == null) spacing = 100d; | |
for(DrawTextFragment line : lines){ | |
double penX; | |
if(firstLine) { | |
if (!isEmptyParagraph()) { | |
// TODO: find out character style for empty, but bulleted/numbered lines | |
bullet = getBullet(graphics, line.getAttributedString().getIterator()); | |
} | |
if (bullet != null){ | |
bullet.setPosition(x + indent, penY); | |
bullet.draw(graphics); | |
// don't let text overlay the bullet and advance by the bullet width | |
double bulletWidth = bullet.getLayout().getAdvance() + 1; | |
penX = x + Math.max(leftMargin, indent+bulletWidth); | |
} else { | |
penX = x + indent; | |
} | |
} else { | |
penX = x + leftMargin; | |
} | |
Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape()); | |
TextAlign ta = paragraph.getTextAlign(); | |
if (ta == null) ta = TextAlign.LEFT; | |
switch (ta) { | |
case CENTER: | |
penX += (anchor.getWidth() - leftMargin - line.getWidth() - leftInset - rightInset) / 2; | |
break; | |
case RIGHT: | |
penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset); | |
break; | |
default: | |
break; | |
} | |
line.setPosition(penX, penY); | |
line.draw(graphics); | |
if(spacing > 0) { | |
// If linespacing >= 0, then linespacing is a percentage of normal line height. | |
penY += spacing*0.01* line.getHeight(); | |
} else { | |
// negative value means absolute spacing in points | |
penY += -spacing; | |
} | |
firstLine = false; | |
} | |
y = penY - y; | |
} | |
public float getFirstLineHeight() { | |
return (lines.isEmpty()) ? 0 : lines.get(0).getHeight(); | |
} | |
public float getLastLineHeight() { | |
return (lines.isEmpty()) ? 0 : lines.get(lines.size()-1).getHeight(); | |
} | |
public boolean isEmptyParagraph() { | |
return (lines.isEmpty() || rawText.trim().isEmpty()); | |
} | |
public void applyTransform(Graphics2D graphics) { | |
} | |
public void drawContent(Graphics2D graphics) { | |
} | |
/** | |
* break text into lines, each representing a line of text that fits in the wrapping width | |
* | |
* @param graphics | |
*/ | |
protected void breakText(Graphics2D graphics){ | |
lines.clear(); | |
DrawFactory fact = DrawFactory.getInstance(graphics); | |
StringBuilder text = new StringBuilder(); | |
AttributedString at = getAttributedString(graphics, text); | |
boolean emptyParagraph = ("".equals(text.toString().trim())); | |
AttributedCharacterIterator it = at.getIterator(); | |
LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext()); | |
for (;;) { | |
int startIndex = measurer.getPosition(); | |
double wrappingWidth = getWrappingWidth(lines.size() == 0, graphics) + 1; // add a pixel to compensate rounding errors | |
// shape width can be smaller that the sum of insets (this was proved by a test file) | |
if(wrappingWidth < 0) wrappingWidth = 1; | |
int nextBreak = text.indexOf("\n", startIndex + 1); | |
if(nextBreak == -1) nextBreak = it.getEndIndex(); | |
TextLayout layout = measurer.nextLayout((float)wrappingWidth, nextBreak, true); | |
if (layout == null) { | |
// layout can be null if the entire word at the current position | |
// does not fit within the wrapping width. Try with requireNextWord=false. | |
layout = measurer.nextLayout((float)wrappingWidth, nextBreak, false); | |
} | |
if(layout == null) { | |
// exit if can't break any more | |
break; | |
} | |
int endIndex = measurer.getPosition(); | |
// skip over new line breaks (we paint 'clear' text runs not starting or ending with \n) | |
if(endIndex < it.getEndIndex() && text.charAt(endIndex) == '\n'){ | |
measurer.setPosition(endIndex + 1); | |
} | |
TextAlign hAlign = paragraph.getTextAlign(); | |
if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) { | |
layout = layout.getJustifiedLayout((float)wrappingWidth); | |
} | |
AttributedString str = (emptyParagraph) | |
? null // we will not paint empty paragraphs | |
: new AttributedString(it, startIndex, endIndex); | |
DrawTextFragment line = fact.getTextFragment(layout, str); | |
lines.add(line); | |
maxLineHeight = Math.max(maxLineHeight, line.getHeight()); | |
if(endIndex == it.getEndIndex()) break; | |
} | |
rawText = text.toString(); | |
} | |
protected DrawTextFragment getBullet(Graphics2D graphics, AttributedCharacterIterator firstLineAttr) { | |
BulletStyle bulletStyle = paragraph.getBulletStyle(); | |
if (bulletStyle == null) return null; | |
String buCharacter; | |
AutoNumberingScheme ans = bulletStyle.getAutoNumberingScheme(); | |
if (ans != null) { | |
buCharacter = ans.format(autoNbrIdx); | |
} else { | |
buCharacter = bulletStyle.getBulletCharacter(); | |
} | |
if (buCharacter == null) return null; | |
String buFont = bulletStyle.getBulletFont(); | |
if (buFont == null) buFont = paragraph.getDefaultFontFamily(); | |
assert(buFont != null); | |
Color buColor = bulletStyle.getBulletFontColor(); | |
if (buColor == null) buColor = (Color)firstLineAttr.getAttribute(TextAttribute.FOREGROUND); | |
float fontSize = (Float)firstLineAttr.getAttribute(TextAttribute.SIZE); | |
Double buSz = bulletStyle.getBulletFontSize(); | |
if (buSz == null) buSz = 100d; | |
if (buSz > 0) fontSize *= buSz* 0.01; | |
else fontSize = (float)-buSz; | |
AttributedString str = new AttributedString(buCharacter); | |
str.addAttribute(TextAttribute.FOREGROUND, buColor); | |
str.addAttribute(TextAttribute.FAMILY, buFont); | |
str.addAttribute(TextAttribute.SIZE, fontSize); | |
TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext()); | |
DrawFactory fact = DrawFactory.getInstance(graphics); | |
return fact.getTextFragment(layout, str); | |
} | |
protected String getRenderableText(TextRun tr) { | |
StringBuilder buf = new StringBuilder(); | |
TextCap cap = tr.getTextCap(); | |
String tabs = null; | |
for (char c : tr.getRawText().toCharArray()) { | |
if(c == '\t') { | |
if (tabs == null) { | |
tabs = tab2space(tr); | |
} | |
buf.append(tabs); | |
continue; | |
} | |
switch (cap) { | |
case ALL: c = Character.toUpperCase(c); break; | |
case SMALL: c = Character.toLowerCase(c); break; | |
case NONE: break; | |
} | |
buf.append(c); | |
} | |
return buf.toString(); | |
} | |
/** | |
* Replace a tab with the effective number of white spaces. | |
*/ | |
private String tab2space(TextRun tr) { | |
AttributedString string = new AttributedString(" "); | |
String typeFace = tr.getFontFamily(); | |
if (typeFace == null) typeFace = "Lucida Sans"; | |
string.addAttribute(TextAttribute.FAMILY, typeFace); | |
Double fs = tr.getFontSize(); | |
if (fs == null) fs = 12d; | |
string.addAttribute(TextAttribute.SIZE, fs.floatValue()); | |
TextLayout l = new TextLayout(string.getIterator(), new FontRenderContext(null, true, true)); | |
double wspace = l.getAdvance(); | |
Double tabSz = paragraph.getDefaultTabSize(); | |
if (tabSz == null) tabSz = wspace*4; | |
int numSpaces = (int)Math.ceil(tabSz / wspace); | |
StringBuilder buf = new StringBuilder(); | |
for(int i = 0; i < numSpaces; i++) { | |
buf.append(' '); | |
} | |
return buf.toString(); | |
} | |
/** | |
* Returns wrapping width to break lines in this paragraph | |
* | |
* @param firstLine whether the first line is breaking | |
* | |
* @return wrapping width in points | |
*/ | |
protected double getWrappingWidth(boolean firstLine, Graphics2D graphics){ | |
// internal margins for the text box | |
Insets2D insets = paragraph.getParentShape().getInsets(); | |
double leftInset = insets.left; | |
double rightInset = insets.right; | |
Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape()); | |
int indentLevel = paragraph.getIndentLevel(); | |
Double leftMargin = paragraph.getLeftMargin(); | |
if (leftMargin == null) { | |
// if the marL attribute is omitted, then a value of 347663 is implied | |
leftMargin = Units.toPoints(347663*(indentLevel+1)); | |
} | |
Double indent = paragraph.getIndent(); | |
if (indent == null) { | |
indent = Units.toPoints(347663*indentLevel); | |
} | |
Double rightMargin = paragraph.getRightMargin(); | |
if (rightMargin == null) { | |
rightMargin = 0d; | |
} | |
double width; | |
TextShape<? extends TextParagraph<T>> ts = paragraph.getParentShape(); | |
if (!ts.getWordWrap()) { | |
// if wordWrap == false then we return the advance to the right border of the sheet | |
width = ts.getSheet().getSlideShow().getPageSize().getWidth() - anchor.getX(); | |
} else { | |
width = anchor.getWidth() - leftInset - rightInset - leftMargin - rightMargin; | |
if (firstLine) { | |
if (bullet != null){ | |
if (indent > 0) width -= indent; | |
} else { | |
if (indent > 0) width -= indent; // first line indentation | |
else if (indent < 0) { // hanging indentation: the first line start at the left margin | |
width += leftMargin; | |
} | |
} | |
} | |
} | |
return width; | |
} | |
private static class AttributedStringData { | |
Attribute attribute; | |
Object value; | |
int beginIndex, endIndex; | |
AttributedStringData(Attribute attribute, Object value, int beginIndex, int endIndex) { | |
this.attribute = attribute; | |
this.value = value; | |
this.beginIndex = beginIndex; | |
this.endIndex = endIndex; | |
} | |
} | |
protected AttributedString getAttributedString(Graphics2D graphics, StringBuilder text){ | |
List<AttributedStringData> attList = new ArrayList<AttributedStringData>(); | |
if (text == null) text = new StringBuilder(); | |
DrawFontManager fontHandler = (DrawFontManager)graphics.getRenderingHint(Drawable.FONT_HANDLER); | |
for (TextRun run : paragraph){ | |
String runText = getRenderableText(run); | |
// skip empty runs | |
if (runText.isEmpty()) continue; | |
int beginIndex = text.length(); | |
text.append(runText); | |
int endIndex = text.length(); | |
Color fgColor = run.getFontColor(); | |
if (fgColor == null) fgColor = Color.BLACK; | |
attList.add(new AttributedStringData(TextAttribute.FOREGROUND, fgColor, beginIndex, endIndex)); | |
// user can pass an custom object to convert fonts | |
String fontFamily = run.getFontFamily(); | |
@SuppressWarnings("unchecked") | |
Map<String,String> fontMap = (Map<String,String>)graphics.getRenderingHint(Drawable.FONT_MAP); | |
if (fontMap != null && fontMap.containsKey(fontFamily)) { | |
fontFamily = fontMap.get(fontFamily); | |
} | |
if(fontHandler != null) { | |
fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily()); | |
} | |
if (fontFamily == null) { | |
fontFamily = paragraph.getDefaultFontFamily(); | |
} | |
attList.add(new AttributedStringData(TextAttribute.FAMILY, fontFamily, beginIndex, endIndex)); | |
Double fontSz = run.getFontSize(); | |
if (fontSz == null) fontSz = paragraph.getDefaultFontSize(); | |
attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), beginIndex, endIndex)); | |
if(run.isBold()) { | |
attList.add(new AttributedStringData(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, beginIndex, endIndex)); | |
} | |
if(run.isItalic()) { | |
attList.add(new AttributedStringData(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, beginIndex, endIndex)); | |
} | |
if(run.isUnderlined()) { | |
attList.add(new AttributedStringData(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, beginIndex, endIndex)); | |
attList.add(new AttributedStringData(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL, beginIndex, endIndex)); | |
} | |
if(run.isStrikethrough()) { | |
attList.add(new AttributedStringData(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, beginIndex, endIndex)); | |
} | |
if(run.isSubscript()) { | |
attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB, beginIndex, endIndex)); | |
} | |
if(run.isSuperscript()) { | |
attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, beginIndex, endIndex)); | |
} | |
} | |
// ensure that the paragraph contains at least one character | |
// We need this trick to correctly measure text | |
if (text.length() == 0) { | |
Double fontSz = paragraph.getDefaultFontSize(); | |
text.append(" "); | |
attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), 0, 1)); | |
} | |
AttributedString string = new AttributedString(text.toString()); | |
for (AttributedStringData asd : attList) { | |
string.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex); | |
} | |
return string; | |
} | |
} |