\r
float lineWidth = (float) _shape.getLineWidth();\r
if(lineWidth == 0.0f) lineWidth = 0.25f; // Both PowerPoint and OOo draw zero-length lines as 0.25pt\r
+ Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);\r
+ if(fontScale != null) lineWidth *= fontScale.floatValue();\r
\r
LineDash lineDash = _shape.getLineDash();\r
float[] dash = null;\r
--- /dev/null
+/*\r
+ * ====================================================================\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements. See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License. You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ * ====================================================================\r
+ */\r
+\r
+package org.apache.poi.xslf.usermodel;\r
+\r
+import java.awt.*;\r
+import java.awt.font.TextLayout;\r
+import java.text.AttributedCharacterIterator;\r
+import java.text.AttributedString;\r
+\r
+/**\r
+ * a renderable text fragment\r
+*/\r
+class TextFragment {\r
+ final TextLayout _layout;\r
+ final AttributedString _str;\r
+\r
+ TextFragment(TextLayout layout, AttributedString str){\r
+ _layout = layout;\r
+ _str = str;\r
+ }\r
+\r
+ void draw(Graphics2D graphics, double x, double y){\r
+ if(_str == null) {\r
+ return;\r
+ }\r
+\r
+ double yBaseline = y + _layout.getAscent();\r
+\r
+ Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE);\r
+ if(textMode != null && textMode == XSLFRenderingHint.TEXT_AS_SHAPES){\r
+ _layout.draw(graphics, (float)x, (float)yBaseline);\r
+ } else {\r
+ graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline );\r
+ }\r
+ }\r
+\r
+ /**\r
+ * @return full height of this text run which is sum of ascent, descent and leading\r
+ */\r
+ public float getHeight(){\r
+ return _layout.getAscent() + _layout.getDescent() + _layout.getLeading();\r
+ }\r
+\r
+ /**\r
+ *\r
+ * @return width if this text run\r
+ */\r
+ public float getWidth(){\r
+ return _layout.getAdvance();\r
+ }\r
+\r
+ /**\r
+ *\r
+ * @return the string to be painted\r
+ */\r
+ public String getString(){\r
+ if(_str == null) return "";\r
+\r
+ AttributedCharacterIterator it = _str.getIterator();\r
+ StringBuffer buf = new StringBuffer();\r
+ for (char c = it.first(); c != it.DONE; c = it.next()) {\r
+ buf.append(c);\r
+ }\r
+ return buf.toString();\r
+ }\r
+\r
+ @Override\r
+ public String toString(){\r
+ return "[" + getClass().getSimpleName() + "] " + getString();\r
+ }\r
+}\r
--- /dev/null
+/*\r
+ * ====================================================================\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements. See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License. You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ * ====================================================================\r
+ */\r
+\r
+package org.apache.poi.xslf.usermodel;\r
+\r
+/**\r
+ * Manages fonts when rendering slides.\r
+ *\r
+ * Use this class to handle unknown / missing fonts or to substitute fonts\r
+ */\r
+public interface XSLFFontManager {\r
+\r
+ /**\r
+ * select a font to be used to paint text\r
+ *\r
+ * @param family the font family as defined in the .pptx file.\r
+ * This can be unknown or missing in the graphic environment.\r
+ *\r
+ * @return the font to be used to paint text\r
+ */\r
+\r
+ String getRendererableFont(String typeface, int pitchFamily);\r
+}\r
double scaleY = exterior.getHeight() / interior.getHeight();\r
\r
// group transform scales shapes but not fonts\r
- Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE);\r
- graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, Math.abs(1/scaleY));\r
+ Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);\r
+ graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, Math.abs(1/scaleY));\r
\r
graphics.scale(scaleX, scaleY);\r
graphics.translate(-interior.getX(), -interior.getY());\r
graphics.setRenderingHint(XSLFRenderingHint.GRESTORE, true);\r
}\r
\r
- graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, prevFontScale);\r
+ graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, prevFontScale);\r
\r
}\r
\r
/**\r
* how to render text:\r
*\r
- * {@link #TEXT_MODE_CHARACTERS} (default) means to draw via\r
+ * {@link #TEXT_AS_CHARACTERS} (default) means to draw via\r
* {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)}.\r
* This mode draws text as characters. Use it if the target graphics writes the actual\r
* character codes instead of glyph outlines (PDFGraphics2D, SVGGraphics2D, etc.)\r
*\r
- * {@link #TEXT_MODE_GLYPHS} means to render via\r
+ * {@link #TEXT_AS_SHAPES} means to render via\r
* {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)}.\r
* This mode draws glyphs as shapes and provides some advanced capabilities such as\r
* justification and font substitution. Use it if the target graphics is an image.\r
/**\r
* draw text via {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)}\r
*/\r
- public static final int TEXT_MODE_CHARACTERS = 1;\r
+ public static final int TEXT_AS_CHARACTERS = 1;\r
\r
/**\r
* draw text via {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)}\r
*/\r
- public static final int TEXT_MODE_GLYPHS = 2;\r
+ public static final int TEXT_AS_SHAPES = 2;\r
\r
@Internal\r
- public static final XSLFRenderingHint FONT_SCALE = new XSLFRenderingHint(5);\r
-}
\ No newline at end of file
+ static final XSLFRenderingHint GROUP_SCALE = new XSLFRenderingHint(5);\r
+\r
+ /**\r
+ * Use this object to resolve unknown / missing fonts when rendering slides\r
+ */\r
+ public static final XSLFRenderingHint FONT_HANDLER = new XSLFRenderingHint(6);\r
+\r
+}\r
\r
if(spacing > 0) {\r
// If linespacing >= 0, then linespacing is a percentage of normal line height.\r
- penY += spacing*0.01* _maxLineHeight;\r
+ penY += spacing*0.01* line.getHeight();\r
} else {\r
// positive value means absolute spacing in points\r
penY += -spacing;\r
return penY - y;\r
}\r
\r
- static class TextFragment {\r
- private TextLayout _layout;\r
- private AttributedString _str;\r
-\r
- TextFragment(TextLayout layout, AttributedString str){\r
- _layout = layout;\r
- _str = str;\r
- }\r
-\r
- void draw(Graphics2D graphics, double x, double y){\r
- double yBaseline = y + _layout.getAscent();\r
-\r
- Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE);\r
- if(textMode != null && textMode == XSLFRenderingHint.TEXT_MODE_GLYPHS){\r
- _layout.draw(graphics, (float)x, (float)yBaseline);\r
- } else {\r
- graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline );\r
- }\r
- }\r
- \r
- public float getHeight(){\r
- return _layout.getAscent() + _layout.getDescent() + _layout.getLeading();\r
- }\r
- public float getWidth(){\r
- return _layout.getAdvance();\r
- }\r
-\r
- }\r
-\r
AttributedString getAttributedString(Graphics2D graphics){\r
\r
String text = getRenderableText();\r
\r
AttributedString string = new AttributedString(text);\r
\r
+ XSLFFontManager fontHandler = (XSLFFontManager)graphics.getRenderingHint(XSLFRenderingHint.FONT_HANDLER);\r
+\r
int startIndex = 0;\r
for (XSLFTextRun run : _runs){\r
int length = run.getRenderableText().length();\r
\r
string.addAttribute(TextAttribute.FOREGROUND, run.getFontColor(), startIndex, endIndex);\r
\r
- // user can pass an object to convert fonts via a rendering hint\r
- string.addAttribute(TextAttribute.FAMILY, run.getFontFamily(), startIndex, endIndex);\r
+ // user can pass an custom object to convert fonts\r
+ String fontFamily = run.getFontFamily();\r
+ if(fontHandler != null) {\r
+ fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily());\r
+ }\r
+ string.addAttribute(TextAttribute.FAMILY, fontFamily, startIndex, endIndex);\r
\r
float fontSz = (float)run.getFontSize();\r
- Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE);\r
+ Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);\r
if(fontScale != null) fontSz *= fontScale.floatValue();\r
\r
string.addAttribute(TextAttribute.SIZE, fontSz , startIndex, endIndex);\r
}\r
\r
/**\r
- * ensure that the paragraph contains at least one character\r
+ * ensure that the paragraph contains at least one character.\r
+ * We need this trick to correctly measure text\r
*/\r
private void ensureNotEmpty(){\r
XSLFTextRun r = addNewTextRun();\r
}\r
}\r
\r
- void breakText(Graphics2D graphics){\r
+ /**\r
+ * break text into lines\r
+ *\r
+ * @param graphics\r
+ * @return array of text fragments,\r
+ * each representing a line of text that fits in the wrapping width\r
+ */\r
+ List<TextFragment> breakText(Graphics2D graphics){\r
_lines = new ArrayList<TextFragment>();\r
\r
// does this paragraph contain text?\r
if(_runs.size() == 0) ensureNotEmpty();\r
\r
String text = getRenderableText();\r
- if(text.length() == 0) return;\r
+ if(text.length() == 0) return _lines;\r
\r
AttributedString at = getAttributedString(graphics);\r
AttributedCharacterIterator it = at.getIterator();\r
LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());\r
for (;;) {\r
int startIndex = measurer.getPosition();\r
+\r
double wrappingWidth = getWrappingWidth(_lines.size() == 0) + 1; // add a pixel to compensate rounding errors\r
- // shape width can be smaller that the sum of insets (proved by a test file)\r
+ // shape width can be smaller that the sum of insets (this was proved by a test file)\r
if(wrappingWidth < 0) wrappingWidth = 1;\r
\r
int nextBreak = text.indexOf('\n', startIndex + 1);\r
if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) {\r
layout = layout.getJustifiedLayout((float)wrappingWidth);\r
}\r
- \r
+\r
+ // skip over new line breaks (we paint 'clear' text runs not starting or ending with \n)\r
+ if(endIndex < it.getEndIndex() && text.charAt(endIndex) == '\n'){\r
+ measurer.setPosition(endIndex + 1);\r
+ }\r
+\r
AttributedString str = new AttributedString(it, startIndex, endIndex);\r
- TextFragment line = new TextFragment(layout, str);\r
+ TextFragment line = new TextFragment(\r
+ layout, // we will not paint empty paragraphs\r
+ emptyParagraph ? null : str);\r
_lines.add(line);\r
\r
_maxLineHeight = Math.max(_maxLineHeight, line.getHeight());\r
\r
if(endIndex == it.getEndIndex()) break;\r
+\r
}\r
\r
if(isBullet() && !emptyParagraph) {\r
_bullet = new TextFragment(layout, str);\r
}\r
}\r
-\r
+ return _lines;\r
}\r
\r
CTTextParagraphProperties getDefaultStyle(){\r
\r
String getRenderableText(){\r
String txt = _r.getT();\r
-\r
+ TextCap cap = getTextCap();\r
StringBuffer buf = new StringBuffer();\r
for(int i = 0; i < txt.length(); i++) {\r
char c = txt.charAt(i);\r
if(c == '\t') {\r
- // replace tab with the effective number of white spaces\r
+ // TODO: finish support for tabs\r
buf.append(" ");\r
} else {\r
- switch (getTextCap()){\r
+ switch (cap){\r
case ALL:\r
buf.append(Character.toUpperCase(c));\r
break;\r
return visitor.getValue();\r
}\r
\r
+ public byte getPitchAndFamily(){\r
+ final XSLFTheme theme = _p.getParentShape().getSheet().getTheme();\r
+\r
+ CharacterPropertyFetcher<Byte> visitor = new CharacterPropertyFetcher<Byte>(_p.getLevel()){\r
+ public boolean fetch(CTTextCharacterProperties props){\r
+ CTTextFont font = props.getLatin();\r
+ if(font != null){\r
+ setValue(font.getPitchFamily());\r
+ return true;\r
+ }\r
+ return false;\r
+ }\r
+ };\r
+ fetchCharacterProperty(visitor);\r
+\r
+ return visitor.getValue() == null ? 0 : visitor.getValue();\r
+ }\r
+\r
/**\r
* Specifies whether a run of text will be formatted as strikethrough text.\r
*\r
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
\r
import junit.framework.TestCase;\r
\r
-import java.awt.Color;\r
-import java.awt.Rectangle;\r
+import java.awt.*;\r
import java.awt.geom.Rectangle2D;\r
+import java.awt.image.BufferedImage;\r
+import java.util.List;\r
\r
/**\r
* Created by IntelliJ IDEA.\r
assertEquals(244.0, expectedWidth); // 300 - 10 - 10 - 36 \r
assertEquals(expectedWidth, p.getWrappingWidth(false));\r
}\r
+\r
+ public void testBreakLines(){\r
+ XMLSlideShow ppt = new XMLSlideShow();\r
+ XSLFSlide slide = ppt.createSlide();\r
+ XSLFTextShape sh = slide.createAutoShape();\r
+\r
+ XSLFTextParagraph p = sh.addNewTextParagraph();\r
+ XSLFTextRun r = p.addNewTextRun();\r
+ r.setFontFamily("serif"); // this should always be available\r
+ r.setFontSize(12);\r
+ r.setText(\r
+ "Paragraph formatting allows for more granular control " +\r
+ "of text within a shape. Properties here apply to all text " +\r
+ "residing within the corresponding paragraph.");\r
+\r
+ sh.setAnchor(new Rectangle(50, 50, 300, 200));\r
+\r
+ BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);\r
+ Graphics2D graphics = img.createGraphics();\r
+\r
+ List<TextFragment> lines;\r
+ lines = p.breakText(graphics);\r
+ assertEquals(3, lines.size());\r
+\r
+ // descrease the shape width from 300 pt to 100 pt\r
+ sh.setAnchor(new Rectangle(50, 50, 100, 200));\r
+ lines = p.breakText(graphics);\r
+ assertEquals(10, lines.size());\r
+\r
+ // descrease the shape width from 300 pt to 100 pt\r
+ sh.setAnchor(new Rectangle(50, 50, 600, 200));\r
+ lines = p.breakText(graphics);\r
+ assertEquals(2, lines.size());\r
+\r
+ // set left and right margins to 200pt. This leaves 200pt for wrapping text\r
+ sh.setLeftInset(200);\r
+ sh.setRightInset(200);\r
+ lines = p.breakText(graphics);\r
+ assertEquals(4, lines.size());\r
+\r
+ r.setText("Apache POI");\r
+ lines = p.breakText(graphics);\r
+ assertEquals(1, lines.size());\r
+ assertEquals("Apache POI", lines.get(0).getString());\r
+\r
+ r.setText("Apache\nPOI");\r
+ lines = p.breakText(graphics);\r
+ assertEquals(2, lines.size());\r
+ assertEquals("Apache", lines.get(0).getString());\r
+ assertEquals("POI", lines.get(1).getString());\r
+\r
+ XSLFAutoShape sh2 = slide.createAutoShape();\r
+ sh2.setAnchor(new Rectangle(50, 50, 300, 200));\r
+ XSLFTextParagraph p2 = sh2.addNewTextParagraph();\r
+ XSLFTextRun r2 = p2.addNewTextRun();\r
+ r2.setFontFamily("serif"); // this should always be available\r
+ r2.setFontSize(30);\r
+ r2.setText("Apache\n");\r
+ XSLFTextRun r3 = p2.addNewTextRun();\r
+ r3.setFontFamily("serif"); // this should always be available\r
+ r3.setFontSize(10);\r
+ r3.setText("POI");\r
+ lines = p2.breakText(graphics);\r
+ assertEquals(2, lines.size());\r
+ assertEquals("Apache", lines.get(0).getString());\r
+ assertEquals("POI", lines.get(1).getString());\r
+ // the first line is at least two times higher than the second\r
+ assertTrue(lines.get(0).getHeight() > lines.get(1).getHeight()*2);\r
+\r
+ }\r
}\r