git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1203143 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_8_BETA5
@@ -431,6 +431,8 @@ class RenderableShape { | |||
float lineWidth = (float) _shape.getLineWidth(); | |||
if(lineWidth == 0.0f) lineWidth = 0.25f; // Both PowerPoint and OOo draw zero-length lines as 0.25pt | |||
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE); | |||
if(fontScale != null) lineWidth *= fontScale.floatValue(); | |||
LineDash lineDash = _shape.getLineDash(); | |||
float[] dash = null; |
@@ -0,0 +1,88 @@ | |||
/* | |||
* ==================================================================== | |||
* 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.*; | |||
import java.awt.font.TextLayout; | |||
import java.text.AttributedCharacterIterator; | |||
import java.text.AttributedString; | |||
/** | |||
* a renderable text fragment | |||
*/ | |||
class TextFragment { | |||
final TextLayout _layout; | |||
final AttributedString _str; | |||
TextFragment(TextLayout layout, AttributedString str){ | |||
_layout = layout; | |||
_str = str; | |||
} | |||
void draw(Graphics2D graphics, double x, double y){ | |||
if(_str == null) { | |||
return; | |||
} | |||
double yBaseline = y + _layout.getAscent(); | |||
Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE); | |||
if(textMode != null && textMode == XSLFRenderingHint.TEXT_AS_SHAPES){ | |||
_layout.draw(graphics, (float)x, (float)yBaseline); | |||
} else { | |||
graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline ); | |||
} | |||
} | |||
/** | |||
* @return full height of this text run which is sum of ascent, descent and leading | |||
*/ | |||
public float getHeight(){ | |||
return _layout.getAscent() + _layout.getDescent() + _layout.getLeading(); | |||
} | |||
/** | |||
* | |||
* @return width if this text run | |||
*/ | |||
public float getWidth(){ | |||
return _layout.getAdvance(); | |||
} | |||
/** | |||
* | |||
* @return the string to be painted | |||
*/ | |||
public String getString(){ | |||
if(_str == null) return ""; | |||
AttributedCharacterIterator it = _str.getIterator(); | |||
StringBuffer buf = new StringBuffer(); | |||
for (char c = it.first(); c != it.DONE; c = it.next()) { | |||
buf.append(c); | |||
} | |||
return buf.toString(); | |||
} | |||
@Override | |||
public String toString(){ | |||
return "[" + getClass().getSimpleName() + "] " + getString(); | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* ==================================================================== | |||
* 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; | |||
/** | |||
* Manages fonts when rendering slides. | |||
* | |||
* Use this class to handle unknown / missing fonts or to substitute fonts | |||
*/ | |||
public interface XSLFFontManager { | |||
/** | |||
* select a font to be used to paint text | |||
* | |||
* @param family the font family as defined in the .pptx file. | |||
* This can be unknown or missing in the graphic environment. | |||
* | |||
* @return the font to be used to paint text | |||
*/ | |||
String getRendererableFont(String typeface, int pitchFamily); | |||
} |
@@ -283,8 +283,8 @@ public class XSLFGroupShape extends XSLFShape { | |||
double scaleY = exterior.getHeight() / interior.getHeight(); | |||
// group transform scales shapes but not fonts | |||
Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE); | |||
graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, Math.abs(1/scaleY)); | |||
Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE); | |||
graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, Math.abs(1/scaleY)); | |||
graphics.scale(scaleX, scaleY); | |||
graphics.translate(-interior.getX(), -interior.getY()); | |||
@@ -302,7 +302,7 @@ public class XSLFGroupShape extends XSLFShape { | |||
graphics.setRenderingHint(XSLFRenderingHint.GRESTORE, true); | |||
} | |||
graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, prevFontScale); | |||
graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, prevFontScale); | |||
} | |||
@@ -51,12 +51,12 @@ public class XSLFRenderingHint extends RenderingHints.Key { | |||
/** | |||
* how to render text: | |||
* | |||
* {@link #TEXT_MODE_CHARACTERS} (default) means to draw via | |||
* {@link #TEXT_AS_CHARACTERS} (default) means to draw via | |||
* {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)}. | |||
* This mode draws text as characters. Use it if the target graphics writes the actual | |||
* character codes instead of glyph outlines (PDFGraphics2D, SVGGraphics2D, etc.) | |||
* | |||
* {@link #TEXT_MODE_GLYPHS} means to render via | |||
* {@link #TEXT_AS_SHAPES} means to render via | |||
* {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)}. | |||
* This mode draws glyphs as shapes and provides some advanced capabilities such as | |||
* justification and font substitution. Use it if the target graphics is an image. | |||
@@ -67,13 +67,19 @@ public class XSLFRenderingHint extends RenderingHints.Key { | |||
/** | |||
* draw text via {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)} | |||
*/ | |||
public static final int TEXT_MODE_CHARACTERS = 1; | |||
public static final int TEXT_AS_CHARACTERS = 1; | |||
/** | |||
* draw text via {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)} | |||
*/ | |||
public static final int TEXT_MODE_GLYPHS = 2; | |||
public static final int TEXT_AS_SHAPES = 2; | |||
@Internal | |||
public static final XSLFRenderingHint FONT_SCALE = new XSLFRenderingHint(5); | |||
} | |||
static final XSLFRenderingHint GROUP_SCALE = new XSLFRenderingHint(5); | |||
/** | |||
* Use this object to resolve unknown / missing fonts when rendering slides | |||
*/ | |||
public static final XSLFRenderingHint FONT_HANDLER = new XSLFRenderingHint(6); | |||
} |
@@ -719,7 +719,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
if(spacing > 0) { | |||
// If linespacing >= 0, then linespacing is a percentage of normal line height. | |||
penY += spacing*0.01* _maxLineHeight; | |||
penY += spacing*0.01* line.getHeight(); | |||
} else { | |||
// positive value means absolute spacing in points | |||
penY += -spacing; | |||
@@ -731,41 +731,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
return penY - y; | |||
} | |||
static class TextFragment { | |||
private TextLayout _layout; | |||
private AttributedString _str; | |||
TextFragment(TextLayout layout, AttributedString str){ | |||
_layout = layout; | |||
_str = str; | |||
} | |||
void draw(Graphics2D graphics, double x, double y){ | |||
double yBaseline = y + _layout.getAscent(); | |||
Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE); | |||
if(textMode != null && textMode == XSLFRenderingHint.TEXT_MODE_GLYPHS){ | |||
_layout.draw(graphics, (float)x, (float)yBaseline); | |||
} else { | |||
graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline ); | |||
} | |||
} | |||
public float getHeight(){ | |||
return _layout.getAscent() + _layout.getDescent() + _layout.getLeading(); | |||
} | |||
public float getWidth(){ | |||
return _layout.getAdvance(); | |||
} | |||
} | |||
AttributedString getAttributedString(Graphics2D graphics){ | |||
String text = getRenderableText(); | |||
AttributedString string = new AttributedString(text); | |||
XSLFFontManager fontHandler = (XSLFFontManager)graphics.getRenderingHint(XSLFRenderingHint.FONT_HANDLER); | |||
int startIndex = 0; | |||
for (XSLFTextRun run : _runs){ | |||
int length = run.getRenderableText().length(); | |||
@@ -777,11 +750,15 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
string.addAttribute(TextAttribute.FOREGROUND, run.getFontColor(), startIndex, endIndex); | |||
// user can pass an object to convert fonts via a rendering hint | |||
string.addAttribute(TextAttribute.FAMILY, run.getFontFamily(), startIndex, endIndex); | |||
// user can pass an custom object to convert fonts | |||
String fontFamily = run.getFontFamily(); | |||
if(fontHandler != null) { | |||
fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily()); | |||
} | |||
string.addAttribute(TextAttribute.FAMILY, fontFamily, startIndex, endIndex); | |||
float fontSz = (float)run.getFontSize(); | |||
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE); | |||
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE); | |||
if(fontScale != null) fontSz *= fontScale.floatValue(); | |||
string.addAttribute(TextAttribute.SIZE, fontSz , startIndex, endIndex); | |||
@@ -813,7 +790,8 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
} | |||
/** | |||
* ensure that the paragraph contains at least one character | |||
* ensure that the paragraph contains at least one character. | |||
* We need this trick to correctly measure text | |||
*/ | |||
private void ensureNotEmpty(){ | |||
XSLFTextRun r = addNewTextRun(); | |||
@@ -824,7 +802,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
} | |||
} | |||
void breakText(Graphics2D graphics){ | |||
/** | |||
* break text into lines | |||
* | |||
* @param graphics | |||
* @return array of text fragments, | |||
* each representing a line of text that fits in the wrapping width | |||
*/ | |||
List<TextFragment> breakText(Graphics2D graphics){ | |||
_lines = new ArrayList<TextFragment>(); | |||
// does this paragraph contain text? | |||
@@ -834,15 +819,16 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
if(_runs.size() == 0) ensureNotEmpty(); | |||
String text = getRenderableText(); | |||
if(text.length() == 0) return; | |||
if(text.length() == 0) return _lines; | |||
AttributedString at = getAttributedString(graphics); | |||
AttributedCharacterIterator it = at.getIterator(); | |||
LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext()); | |||
for (;;) { | |||
int startIndex = measurer.getPosition(); | |||
double wrappingWidth = getWrappingWidth(_lines.size() == 0) + 1; // add a pixel to compensate rounding errors | |||
// shape width can be smaller that the sum of insets (proved by a test file) | |||
// 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); | |||
@@ -861,14 +847,22 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) { | |||
layout = layout.getJustifiedLayout((float)wrappingWidth); | |||
} | |||
// 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); | |||
} | |||
AttributedString str = new AttributedString(it, startIndex, endIndex); | |||
TextFragment line = new TextFragment(layout, str); | |||
TextFragment line = new TextFragment( | |||
layout, // we will not paint empty paragraphs | |||
emptyParagraph ? null : str); | |||
_lines.add(line); | |||
_maxLineHeight = Math.max(_maxLineHeight, line.getHeight()); | |||
if(endIndex == it.getEndIndex()) break; | |||
} | |||
if(isBullet() && !emptyParagraph) { | |||
@@ -897,7 +891,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{ | |||
_bullet = new TextFragment(layout, str); | |||
} | |||
} | |||
return _lines; | |||
} | |||
CTTextParagraphProperties getDefaultStyle(){ |
@@ -63,15 +63,15 @@ public class XSLFTextRun { | |||
String getRenderableText(){ | |||
String txt = _r.getT(); | |||
TextCap cap = getTextCap(); | |||
StringBuffer buf = new StringBuffer(); | |||
for(int i = 0; i < txt.length(); i++) { | |||
char c = txt.charAt(i); | |||
if(c == '\t') { | |||
// replace tab with the effective number of white spaces | |||
// TODO: finish support for tabs | |||
buf.append(" "); | |||
} else { | |||
switch (getTextCap()){ | |||
switch (cap){ | |||
case ALL: | |||
buf.append(Character.toUpperCase(c)); | |||
break; | |||
@@ -268,6 +268,24 @@ public class XSLFTextRun { | |||
return visitor.getValue(); | |||
} | |||
public byte getPitchAndFamily(){ | |||
final XSLFTheme theme = _p.getParentShape().getSheet().getTheme(); | |||
CharacterPropertyFetcher<Byte> visitor = new CharacterPropertyFetcher<Byte>(_p.getLevel()){ | |||
public boolean fetch(CTTextCharacterProperties props){ | |||
CTTextFont font = props.getLatin(); | |||
if(font != null){ | |||
setValue(font.getPitchFamily()); | |||
return true; | |||
} | |||
return false; | |||
} | |||
}; | |||
fetchCharacterProperty(visitor); | |||
return visitor.getValue() == null ? 0 : visitor.getValue(); | |||
} | |||
/** | |||
* Specifies whether a run of text will be formatted as strikethrough text. | |||
* |
@@ -493,7 +493,7 @@ public abstract class XSLFTextShape extends XSLFSimpleShape implements Iterable< | |||
double y0 = y; | |||
for(int i = 0; i < _paragraphs.size(); i++){ | |||
XSLFTextParagraph p = _paragraphs.get(i); | |||
List<XSLFTextParagraph.TextFragment> lines = p.getTextLines(); | |||
List<TextFragment> lines = p.getTextLines(); | |||
if(i > 0 && lines.size() > 0) { | |||
// the amount of vertical white space before the paragraph |
@@ -2,9 +2,10 @@ package org.apache.poi.xslf.usermodel; | |||
import junit.framework.TestCase; | |||
import java.awt.Color; | |||
import java.awt.Rectangle; | |||
import java.awt.*; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
import java.util.List; | |||
/** | |||
* Created by IntelliJ IDEA. | |||
@@ -98,4 +99,74 @@ public class TestXSLFTextParagraph extends TestCase { | |||
assertEquals(244.0, expectedWidth); // 300 - 10 - 10 - 36 | |||
assertEquals(expectedWidth, p.getWrappingWidth(false)); | |||
} | |||
public void testBreakLines(){ | |||
XMLSlideShow ppt = new XMLSlideShow(); | |||
XSLFSlide slide = ppt.createSlide(); | |||
XSLFTextShape sh = slide.createAutoShape(); | |||
XSLFTextParagraph p = sh.addNewTextParagraph(); | |||
XSLFTextRun r = p.addNewTextRun(); | |||
r.setFontFamily("serif"); // this should always be available | |||
r.setFontSize(12); | |||
r.setText( | |||
"Paragraph formatting allows for more granular control " + | |||
"of text within a shape. Properties here apply to all text " + | |||
"residing within the corresponding paragraph."); | |||
sh.setAnchor(new Rectangle(50, 50, 300, 200)); | |||
BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); | |||
Graphics2D graphics = img.createGraphics(); | |||
List<TextFragment> lines; | |||
lines = p.breakText(graphics); | |||
assertEquals(3, lines.size()); | |||
// descrease the shape width from 300 pt to 100 pt | |||
sh.setAnchor(new Rectangle(50, 50, 100, 200)); | |||
lines = p.breakText(graphics); | |||
assertEquals(10, lines.size()); | |||
// descrease the shape width from 300 pt to 100 pt | |||
sh.setAnchor(new Rectangle(50, 50, 600, 200)); | |||
lines = p.breakText(graphics); | |||
assertEquals(2, lines.size()); | |||
// set left and right margins to 200pt. This leaves 200pt for wrapping text | |||
sh.setLeftInset(200); | |||
sh.setRightInset(200); | |||
lines = p.breakText(graphics); | |||
assertEquals(4, lines.size()); | |||
r.setText("Apache POI"); | |||
lines = p.breakText(graphics); | |||
assertEquals(1, lines.size()); | |||
assertEquals("Apache POI", lines.get(0).getString()); | |||
r.setText("Apache\nPOI"); | |||
lines = p.breakText(graphics); | |||
assertEquals(2, lines.size()); | |||
assertEquals("Apache", lines.get(0).getString()); | |||
assertEquals("POI", lines.get(1).getString()); | |||
XSLFAutoShape sh2 = slide.createAutoShape(); | |||
sh2.setAnchor(new Rectangle(50, 50, 300, 200)); | |||
XSLFTextParagraph p2 = sh2.addNewTextParagraph(); | |||
XSLFTextRun r2 = p2.addNewTextRun(); | |||
r2.setFontFamily("serif"); // this should always be available | |||
r2.setFontSize(30); | |||
r2.setText("Apache\n"); | |||
XSLFTextRun r3 = p2.addNewTextRun(); | |||
r3.setFontFamily("serif"); // this should always be available | |||
r3.setFontSize(10); | |||
r3.setText("POI"); | |||
lines = p2.breakText(graphics); | |||
assertEquals(2, lines.size()); | |||
assertEquals("Apache", lines.get(0).getString()); | |||
assertEquals("POI", lines.get(1).getString()); | |||
// the first line is at least two times higher than the second | |||
assertTrue(lines.get(0).getHeight() > lines.get(1).getHeight()*2); | |||
} | |||
} |