org.apache.poi.hslf.model.TextPainter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of poi-scratchpad Show documentation
Show all versions of poi-scratchpad Show documentation
Apache POI - Java API To Access Microsoft Format Files (Scratchpad)
/* ====================================================================
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.model;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.poi.hslf.record.TextRulerAtom;
import org.apache.poi.hslf.usermodel.RichTextRun;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
/**
* Paint text into java.awt.Graphics2D
*
* @author Yegor Kozlov
*/
public final class TextPainter {
public static final Key KEY_FONTFALLBACK = new Key(50, "Font fallback map");
public static final Key KEY_FONTMAP = new Key(51, "Font map");
protected POILogger logger = POILogFactory.getLogger(this.getClass());
/**
* Display unicode square if a bullet char can't be displayed,
* for example, if Wingdings font is used.
* TODO: map Wingdngs and Symbol to unicode Arial
*/
protected static final char DEFAULT_BULLET_CHAR = '\u25a0';
protected TextShape _shape;
public TextPainter(TextShape shape){
_shape = shape;
}
public AttributedString getAttributedString(TextRun txrun) {
return getAttributedString(txrun, null);
}
/**
* Convert the underlying set of rich text runs into java.text.AttributedString
*/
public AttributedString getAttributedString(TextRun txrun, Graphics2D graphics){
String text = txrun.getText();
//TODO: properly process tabs
text = text.replace('\t', ' ');
text = text.replace((char)160, ' ');
AttributedString at = new AttributedString(text);
RichTextRun[] rt = txrun.getRichTextRuns();
for (int i = 0; i < rt.length; i++) {
int start = rt[i].getStartIndex();
int end = rt[i].getEndIndex();
if(start == end) {
logger.log(POILogger.INFO, "Skipping RichTextRun with zero length");
continue;
}
String mappedFont = rt[i].getFontName();
String fallbackFont = Font.SANS_SERIF;
if (graphics != null) {
@SuppressWarnings("unchecked")
Map fontMap = (Map)graphics.getRenderingHint(KEY_FONTMAP);
if (fontMap != null && fontMap.containsKey(mappedFont)) {
mappedFont = fontMap.get(mappedFont);
}
@SuppressWarnings("unchecked")
Map fallbackMap = (Map)graphics.getRenderingHint(KEY_FONTFALLBACK);
if (fallbackMap != null && fallbackMap.containsKey(mappedFont)) {
fallbackFont = fallbackMap.get(mappedFont);
}
}
at.addAttribute(TextAttribute.FAMILY, mappedFont, start, end);
at.addAttribute(TextAttribute.SIZE, new Float(rt[i].getFontSize()), start, end);
at.addAttribute(TextAttribute.FOREGROUND, rt[i].getFontColor(), start, end);
if(rt[i].isBold()) at.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, start, end);
if(rt[i].isItalic()) at.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, start, end);
if(rt[i].isUnderlined()) {
at.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, start, end);
at.addAttribute(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL, start, end);
}
if(rt[i].isStrikethrough()) at.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, start, end);
int superScript = rt[i].getSuperscript();
if(superScript != 0) at.addAttribute(TextAttribute.SUPERSCRIPT, superScript > 0 ? TextAttribute.SUPERSCRIPT_SUPER : TextAttribute.SUPERSCRIPT_SUB, start, end);
int style = (rt[i].isBold() ? Font.BOLD : 0) | (rt[i].isItalic() ? Font.ITALIC : 0);
Font f = new Font(mappedFont, style, rt[i].getFontSize());
// check for unsupported characters and add a fallback font for these
char textChr[] = text.toCharArray();
int nextEnd = f.canDisplayUpTo(textChr, start, end);
boolean isNextValid = nextEnd == start;
for (int last = start; nextEnd != -1 && nextEnd <= end; ) {
if (isNextValid) {
nextEnd = f.canDisplayUpTo(textChr, nextEnd, end);
isNextValid = false;
} else {
if (nextEnd >= end || f.canDisplay(Character.codePointAt(textChr, nextEnd, end)) ) {
at.addAttribute(TextAttribute.FAMILY, fallbackFont, last, Math.min(nextEnd,end));
if (nextEnd >= end) break;
last = nextEnd;
isNextValid = true;
} else {
boolean isHS = Character.isHighSurrogate(textChr[nextEnd]);
nextEnd+=(isHS?2:1);
}
}
}
}
return at;
}
public void paint(Graphics2D graphics){
AffineTransform tx = graphics.getTransform();
Rectangle2D anchor = _shape.getLogicalAnchor2D();
TextElement[] elem = getTextElements((float)anchor.getWidth(), graphics.getFontRenderContext(), graphics);
if(elem == null) return;
float textHeight = 0;
for (int i = 0; i < elem.length; i++) {
textHeight += elem[i].ascent + elem[i].descent;
}
int valign = _shape.getVerticalAlignment();
double y0 = anchor.getY();
switch (valign){
case TextShape.AnchorTopBaseline:
case TextShape.AnchorTop:
y0 += _shape.getMarginTop();
break;
case TextShape.AnchorBottom:
y0 += anchor.getHeight() - textHeight - _shape.getMarginBottom();
break;
default:
case TextShape.AnchorMiddle:
float delta = (float)anchor.getHeight() - textHeight - _shape.getMarginTop() - _shape.getMarginBottom();
y0 += _shape.getMarginTop() + delta/2;
break;
}
// Transform of text in flipped shapes is special.
// At this point the flip and rotation transform is already applied
// (see XSLFShape#applyTransform ), but we need to restore it to avoid painting "upside down".
// See Bugzilla 54210.
if(_shape.getFlipVertical()){
graphics.translate(anchor.getX(), anchor.getY() + anchor.getHeight());
graphics.scale(1, -1);
graphics.translate(-anchor.getX(), -anchor.getY());
// text in vertically flipped shapes is rotated by 180 degrees
double centerX = anchor.getX() + anchor.getWidth()/2;
double centerY = anchor.getY() + anchor.getHeight()/2;
graphics.translate(centerX, centerY);
graphics.rotate(Math.toRadians(180));
graphics.translate(-centerX, -centerY);
}
// Horizontal flipping applies only to shape outline and not to the text in the shape.
// Applying flip second time restores the original not-flipped transform
if(_shape.getFlipHorizontal()){
graphics.translate(anchor.getX() + anchor.getWidth(), anchor.getY());
graphics.scale(-1, 1);
graphics.translate(-anchor.getX() , -anchor.getY());
}
//finally draw the text fragments
for (int i = 0; i < elem.length; i++) {
y0 += elem[i].ascent;
Point2D.Double pen = new Point2D.Double();
pen.y = y0;
switch (elem[i]._align) {
default:
case TextShape.AlignLeft:
pen.x = anchor.getX() + _shape.getMarginLeft();
break;
case TextShape.AlignCenter:
pen.x = anchor.getX() + _shape.getMarginLeft() +
(anchor.getWidth() - elem[i].advance - _shape.getMarginLeft() - _shape.getMarginRight()) / 2;
break;
case TextShape.AlignRight:
pen.x = anchor.getX() + _shape.getMarginLeft() +
(anchor.getWidth() - elem[i].advance - _shape.getMarginLeft() - _shape.getMarginRight());
break;
}
if(elem[i]._bullet != null){
graphics.drawString(elem[i]._bullet.getIterator(), (float)(pen.x + elem[i]._bulletOffset), (float)pen.y);
}
AttributedCharacterIterator chIt = elem[i]._text.getIterator();
if(chIt.getEndIndex() > chIt.getBeginIndex()) {
graphics.drawString(chIt, (float)(pen.x + elem[i]._textOffset), (float)pen.y);
}
y0 += elem[i].descent;
}
graphics.setTransform(tx);
}
public TextElement[] getTextElements(float textWidth, FontRenderContext frc){
return getTextElements(textWidth, frc, null);
}
public TextElement[] getTextElements(float textWidth, FontRenderContext frc, Graphics2D graphics){
TextRun run = _shape.getTextRun();
if (run == null) return null;
String text = run.getText();
if (text == null || text.equals("")) return null;
AttributedString at = getAttributedString(run, graphics);
AttributedCharacterIterator it = at.getIterator();
int paragraphStart = it.getBeginIndex();
int paragraphEnd = it.getEndIndex();
List lines = new ArrayList();
LineBreakMeasurer measurer = new LineBreakMeasurer(it, frc);
measurer.setPosition(paragraphStart);
while (measurer.getPosition() < paragraphEnd) {
int startIndex = measurer.getPosition();
int nextBreak = text.indexOf('\n', measurer.getPosition() + 1);
boolean prStart = text.charAt(startIndex) == '\n';
if(prStart) measurer.setPosition(startIndex++);
RichTextRun rt = run.getRichTextRunAt(startIndex == text.length() ? (startIndex-1) : startIndex);
if(rt == null) {
logger.log(POILogger.WARN, "RichTextRun not found at pos" + startIndex + "; text.length: " + text.length());
break;
}
float wrappingWidth = textWidth - _shape.getMarginLeft() - _shape.getMarginRight();
int bulletOffset = rt.getBulletOffset();
int textOffset = rt.getTextOffset();
int indent = rt.getIndentLevel();
TextRulerAtom ruler = run.getTextRuler();
if(ruler != null) {
int bullet_val = ruler.getBulletOffsets()[indent]*Shape.POINT_DPI/Shape.MASTER_DPI;
int text_val = ruler.getTextOffsets()[indent]*Shape.POINT_DPI/Shape.MASTER_DPI;
if(bullet_val > text_val){
int a = bullet_val;
bullet_val = text_val;
text_val = a;
}
if(bullet_val != 0 ) bulletOffset = bullet_val;
if(text_val != 0) textOffset = text_val;
}
if(bulletOffset > 0 || prStart || startIndex == 0) wrappingWidth -= textOffset;
if (_shape.getWordWrap() == TextShape.WrapNone) {
wrappingWidth = _shape.getSheet().getSlideShow().getPageSize().width;
}
TextLayout textLayout = measurer.nextLayout(wrappingWidth + 1,
nextBreak == -1 ? paragraphEnd : nextBreak, true);
if (textLayout == null) {
textLayout = measurer.nextLayout(textWidth,
nextBreak == -1 ? paragraphEnd : nextBreak, false);
}
if(textLayout == null){
logger.log(POILogger.WARN, "Failed to break text into lines: wrappingWidth: "+wrappingWidth+
"; text: " + rt.getText());
measurer.setPosition(rt.getEndIndex());
continue;
}
int endIndex = measurer.getPosition();
float lineHeight = (float)textLayout.getBounds().getHeight();
int linespacing = rt.getLineSpacing();
if(linespacing == 0) linespacing = 100;
TextElement el = new TextElement();
if(linespacing >= 0){
el.ascent = textLayout.getAscent()*linespacing/100;
} else {
el.ascent = -linespacing*Shape.POINT_DPI/Shape.MASTER_DPI;
}
el._align = rt.getAlignment();
el.advance = textLayout.getAdvance();
el._textOffset = textOffset;
el._text = new AttributedString(it, startIndex, endIndex);
el.textStartIndex = startIndex;
el.textEndIndex = endIndex;
if (prStart){
int sp = rt.getSpaceBefore();
float spaceBefore;
if(sp >= 0){
spaceBefore = lineHeight * sp/100;
} else {
spaceBefore = -sp*Shape.POINT_DPI/Shape.MASTER_DPI;
}
el.ascent += spaceBefore;
}
float descent;
if(linespacing >= 0){
descent = (textLayout.getDescent() + textLayout.getLeading())*linespacing/100;
} else {
descent = -linespacing*Shape.POINT_DPI/Shape.MASTER_DPI;
}
if (prStart){
int sp = rt.getSpaceAfter();
float spaceAfter;
if(sp >= 0){
spaceAfter = lineHeight * sp/100;
} else {
spaceAfter = -sp*Shape.POINT_DPI/Shape.MASTER_DPI;
}
el.ascent += spaceAfter;
}
el.descent = descent;
if(rt.isBullet() && (prStart || startIndex == 0)){
it.setIndex(startIndex);
AttributedString bat = new AttributedString(Character.toString(rt.getBulletChar()));
Color clr = rt.getBulletColor();
if (clr != null) bat.addAttribute(TextAttribute.FOREGROUND, clr);
else bat.addAttribute(TextAttribute.FOREGROUND, it.getAttribute(TextAttribute.FOREGROUND));
int fontIdx = rt.getBulletFont();
if(fontIdx == -1) fontIdx = rt.getFontIndex();
PPFont bulletFont = _shape.getSheet().getSlideShow().getFont(fontIdx);
bat.addAttribute(TextAttribute.FAMILY, bulletFont.getFontName());
int bulletSize = rt.getBulletSize();
int fontSize = rt.getFontSize();
if(bulletSize != -1) fontSize = Math.round(fontSize*bulletSize*0.01f);
bat.addAttribute(TextAttribute.SIZE, new Float(fontSize));
if(!new Font(bulletFont.getFontName(), Font.PLAIN, 1).canDisplay(rt.getBulletChar())){
bat.addAttribute(TextAttribute.FAMILY, "Arial");
bat = new AttributedString("" + DEFAULT_BULLET_CHAR, bat.getIterator().getAttributes());
}
if(text.substring(startIndex, endIndex).length() > 1){
el._bullet = bat;
el._bulletOffset = bulletOffset;
}
}
lines.add(el);
}
//finally draw the text fragments
TextElement[] elems = new TextElement[lines.size()];
return lines.toArray(elems);
}
public static class TextElement {
public AttributedString _text;
public int _textOffset;
public AttributedString _bullet;
public int _bulletOffset;
public int _align;
public float ascent, descent;
public float advance;
public int textStartIndex, textEndIndex;
}
public static class Key extends RenderingHints.Key {
String description;
public Key(int paramInt, String paramString) {
super(paramInt);
this.description = paramString;
}
public final int getIndex() {
return intKey();
}
public final String toString() {
return this.description;
}
public boolean isCompatibleValue(Object paramObject) {
return true;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy