]> source.dussan.org Git - poi.git/commitdiff
misc improvements in text rendering in xlsf
authorYegor Kozlov <yegor@apache.org>
Thu, 17 Nov 2011 10:33:59 +0000 (10:33 +0000)
committerYegor Kozlov <yegor@apache.org>
Thu, 17 Nov 2011 10:33:59 +0000 (10:33 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1203143 13f79535-47bb-0310-9956-ffa450edef68

src/ooxml/java/org/apache/poi/xslf/usermodel/RenderableShape.java
src/ooxml/java/org/apache/poi/xslf/usermodel/TextFragment.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontManager.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java
src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextParagraph.java

index 27ba5d701bfdbd78c1e9bf57450a42ab1b376704..c59ea6d1dddfc8a4f4f586b2d282809d4cce1647 100644 (file)
@@ -431,6 +431,8 @@ class RenderableShape {
 \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
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 (file)
index 0000000..6bcb716
--- /dev/null
@@ -0,0 +1,88 @@
+/*\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
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 (file)
index 0000000..e61933f
--- /dev/null
@@ -0,0 +1,39 @@
+/*\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
index 1c681934766cc893c8d73279c79eecb747a64417..85b426ef9245d54a47eb46d07f61f2e785718ff6 100644 (file)
@@ -283,8 +283,8 @@ public class XSLFGroupShape extends XSLFShape {
         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
@@ -302,7 +302,7 @@ public class XSLFGroupShape extends XSLFShape {
             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
index e8d70437cea205210647b252f67c42e16e4b72a8..1b467ce82a02d053bdb4740e02990e66d3ed083a 100644 (file)
@@ -51,12 +51,12 @@ public class XSLFRenderingHint extends RenderingHints.Key {
     /**\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
@@ -67,13 +67,19 @@ public class XSLFRenderingHint extends RenderingHints.Key {
     /**\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
index 46087363989febf7cc050a6b837ef3acfa7cdfa7..a0a5c73fdaa41ace3ac540ba31524ffcd5976cac 100644 (file)
@@ -719,7 +719,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
 \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
@@ -731,41 +731,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
         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
@@ -777,11 +750,15 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
 \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
@@ -813,7 +790,8 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
     }\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
@@ -824,7 +802,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
         }\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
@@ -834,15 +819,16 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
         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
@@ -861,14 +847,22 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
             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
@@ -897,7 +891,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
                 _bullet = new TextFragment(layout, str);\r
             }\r
         }\r
-\r
+        return _lines;\r
     }\r
 \r
     CTTextParagraphProperties getDefaultStyle(){\r
index 4f05caec95a0b4d8839633ce1968e05b85111022..f67fde8ae1004753b1e19a80bffb0065a0b52b10 100644 (file)
@@ -63,15 +63,15 @@ public class XSLFTextRun {
 \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
@@ -268,6 +268,24 @@ public class XSLFTextRun {
         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
index cce5a61e84fe49b3c750deca3d4ff61a884748a3..2aabbd2b18fccc34e80fc400e67552ddc8a30730 100644 (file)
@@ -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
index 6b9cf596e7a3e2347a2f7a9edf50f52dee99fc8a..b87363b1d696a8749ca7be86d56eeb0c5cf9ee24 100755 (executable)
@@ -2,9 +2,10 @@ package org.apache.poi.xslf.usermodel;
 \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
@@ -98,4 +99,74 @@ public class TestXSLFTextParagraph extends TestCase {
         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