From: Yegor Kozlov Date: Thu, 17 Nov 2011 10:33:59 +0000 (+0000) Subject: misc improvements in text rendering in xlsf X-Git-Tag: REL_3_8_BETA5~28 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=67c8cdac99bc119c074bbacfa1d60108ec7a076d;p=poi.git misc improvements in text rendering in xlsf git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1203143 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/RenderableShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/RenderableShape.java index 27ba5d701b..c59ea6d1dd 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/RenderableShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/RenderableShape.java @@ -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; diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/TextFragment.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/TextFragment.java new file mode 100644 index 0000000000..6bcb716ee5 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/TextFragment.java @@ -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(); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontManager.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontManager.java new file mode 100644 index 0000000000..e61933fb37 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontManager.java @@ -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); +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java index 1c68193476..85b426ef92 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java @@ -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); } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java index e8d70437ce..1b467ce82a 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java @@ -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); -} \ No newline at end of file + 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); + +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java index 4608736398..a0a5c73fda 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java @@ -719,7 +719,7 @@ public class XSLFTextParagraph implements Iterable{ 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{ 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{ 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{ } /** - * 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{ } } - 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 breakText(Graphics2D graphics){ _lines = new ArrayList(); // does this paragraph contain text? @@ -834,15 +819,16 @@ public class XSLFTextParagraph implements Iterable{ 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{ 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{ _bullet = new TextFragment(layout, str); } } - + return _lines; } CTTextParagraphProperties getDefaultStyle(){ diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java index 4f05caec95..f67fde8ae1 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java @@ -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 visitor = new CharacterPropertyFetcher(_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. * diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java index cce5a61e84..2aabbd2b18 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java @@ -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 lines = p.getTextLines(); + List lines = p.getTextLines(); if(i > 0 && lines.size() > 0) { // the amount of vertical white space before the paragraph diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextParagraph.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextParagraph.java index 6b9cf596e7..b87363b1d6 100755 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextParagraph.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextParagraph.java @@ -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 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); + + } }