]> source.dussan.org Git - poi.git/commitdiff
Bug 55902 - Mixed fonts issue with Chinese characters (unable to form images from...
authorAndreas Beeker <kiwiwings@apache.org>
Tue, 11 Feb 2014 23:16:54 +0000 (23:16 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Tue, 11 Feb 2014 23:16:54 +0000 (23:16 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1567455 13f79535-47bb-0310-9956-ffa450edef68

build.xml
src/scratchpad/src/org/apache/poi/hslf/model/TextPainter.java
src/scratchpad/testcases/org/apache/poi/hslf/AllHSLFTests.java
src/scratchpad/testcases/org/apache/poi/hslf/usermodel/AllHSLFUserModelTests.java
src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java [new file with mode: 0644]
test-data/slideshow/bug55902-mixedChars.png [new file with mode: 0644]
test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt [new file with mode: 0644]

index 51bdf764fa34711741d85a2ca54fc3858a118a8b..9deaa09cf5d56e90faab79585980a2c1a4a5f8ba 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -834,7 +834,7 @@ under the License.
         </uptodate>
     </target>
 
-    <target name="test-scratchpad" depends="compile-main,compile-scratchpad,-test-scratchpad-check,jacocotask"
+    <target name="test-scratchpad" depends="compile-main,compile-scratchpad,-test-scratchpad-check,jacocotask,test-scratchpad-download-resources"
             unless="scratchpad.test.notRequired" xmlns:jacoco="antlib:org.jacoco.ant">
         <jacoco:coverage enabled="${coverage.enabled}" excludes="${coverage.excludes}" destfile="build/jacoco-scratchpad.exec">
             <junit printsummary="yes" fork="yes" forkmode="once" haltonfailure="${halt.on.test.failure}"
@@ -1383,7 +1383,6 @@ under the License.
     </target>
        
        <target name="findbugs"><!-- depends="assemble" -->
-               
         <antcall target="downloadfile">
             <param name="sourcefile" value="http://prdownloads.sourceforge.net/findbugs/findbugs-noUpdateChecks-2.0.3.zip?download"/>
             <param name="destfile" value="${main.lib}/findbugs-noUpdateChecks-2.0.3.zip"/>
@@ -1425,4 +1424,26 @@ under the License.
                        <sourcePath path="src/scratchpad/src" />
                </findbugs>             
        </target>
+
+       <target name="test-scratchpad-download-resources">
+               <mkdir dir="build/scratchpad-test-resources"/>
+               
+        <antcall target="downloadfile">
+            <param name="sourcefile" value="http://sourceforge.net/projects/monafont/files/monafont/monafont-2.90/monafont-ttf-2.90.zip/download"/>
+            <param name="destfile" value="build/scratchpad-test-resources/monafont-ttf-2.90.zip"/>
+        </antcall>
+
+               <unzip src="build/scratchpad-test-resources/monafont-ttf-2.90.zip"
+                      dest="build/scratchpad-test-resources">
+                   <patternset>
+                       <include name="mona.ttf"/>
+                   </patternset>
+               </unzip>
+               
+        <antcall target="downloadfile">
+            <param name="sourcefile" value="https://googlefontdirectory.googlecode.com/hg-history/c5955de4df3e40f6ab705bbccbd1f5ad93998287/cabin/Cabin-Regular.ttf"/>
+            <param name="destfile" value="build/scratchpad-test-resources/Cabin-Regular.ttf"/>
+        </antcall>
+       </target>
+       
 </project>
index ca243ff290c9b2bb51ba590ff36a4d7390dba406..6d0dbc82d0d8aefb45389d0c468eb5afcd47b943 100644 (file)
@@ -20,6 +20,7 @@ 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;
@@ -31,6 +32,7 @@ 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;
@@ -43,6 +45,9 @@ import org.apache.poi.util.POILogger;
  * @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());
 
     /**
@@ -58,10 +63,14 @@ public final class TextPainter {
         _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){
+    public AttributedString getAttributedString(TextRun txrun, Graphics2D graphics){
         String text = txrun.getText();
         //TODO: properly process tabs
         text = text.replace('\t', ' ');
@@ -77,7 +86,22 @@ public final class TextPainter {
                 continue;
             }
 
-            at.addAttribute(TextAttribute.FAMILY, rt[i].getFontName(), start, end);
+            String mappedFont = rt[i].getFontName();
+            String fallbackFont = Font.SANS_SERIF;
+            if (graphics != null) {
+                @SuppressWarnings("unchecked")
+                Map<String,String> fontMap = (Map<String,String>)graphics.getRenderingHint(KEY_FONTMAP);
+                if (fontMap != null && fontMap.containsKey(mappedFont)) {
+                    mappedFont = fontMap.get(mappedFont);
+                }
+                @SuppressWarnings("unchecked")
+                Map<String,String> fallbackMap = (Map<String,String>)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);
@@ -89,7 +113,31 @@ public final class TextPainter {
             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;
     }
@@ -98,7 +146,7 @@ public final class TextPainter {
         AffineTransform tx = graphics.getTransform();
 
         Rectangle2D anchor = _shape.getLogicalAnchor2D();
-        TextElement[] elem = getTextElements((float)anchor.getWidth(), graphics.getFontRenderContext());
+        TextElement[] elem = getTextElements((float)anchor.getWidth(), graphics.getFontRenderContext(), graphics);
         if(elem == null) return;
 
         float textHeight = 0;
@@ -183,13 +231,17 @@ public final class TextPainter {
     }
 
     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);
+        AttributedString at = getAttributedString(run, graphics);
 
         AttributedCharacterIterator it = at.getIterator();
         int paragraphStart = it.getBeginIndex();
@@ -342,4 +394,25 @@ public final class TextPainter {
         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;
+      }
+    }
 }
index 8d9ca15196956e7f517264c33edcd01935142f0c..e24b093dea48782eff51c9c266c02a440249cf11 100644 (file)
 
 package org.apache.poi.hslf;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
-
 import org.apache.poi.hslf.extractor.TestCruddyExtractor;
 import org.apache.poi.hslf.extractor.TestExtractor;
 import org.apache.poi.hslf.model.AllHSLFModelTests;
 import org.apache.poi.hslf.record.AllHSLFRecordTests;
 import org.apache.poi.hslf.usermodel.AllHSLFUserModelTests;
 import org.apache.poi.hslf.util.TestSystemTimeUtils;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
 
 /**
  * Collects all tests from the package <tt>org.apache.poi.hslf</tt> and all sub-packages.
- * 
- * @author Josh Micich
  */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestEncryptedFile.class,
+    TestRecordCounts.class,
+    TestReWrite.class,
+    TestReWriteSanity.class,
+    TestCruddyExtractor.class,
+    TestExtractor.class,
+    AllHSLFModelTests.class,
+    AllHSLFRecordTests.class,
+    AllHSLFUserModelTests.class,
+    TestSystemTimeUtils.class
+})
 public class AllHSLFTests {
-       
-       public static Test suite() {
-               TestSuite result = new TestSuite(AllHSLFTests.class.getName());
-               result.addTestSuite(TestEncryptedFile.class);
-               result.addTestSuite(TestRecordCounts.class);
-               result.addTestSuite(TestReWrite.class);
-               result.addTestSuite(TestReWriteSanity.class);
-               result.addTestSuite(TestCruddyExtractor.class);
-               result.addTestSuite(TestExtractor.class);
-               result.addTest(AllHSLFModelTests.suite());
-               result.addTest(AllHSLFRecordTests.suite());
-               result.addTest(AllHSLFUserModelTests.suite());
-               result.addTestSuite(TestSystemTimeUtils.class);
-               return result;
-       }
 }
index 0a6b40e96cb072ec59110a5897539f1be4af08d1..bc6b6cd972d34f9ff9ceedd21dbb5bcbe718d819 100644 (file)
 
 package org.apache.poi.hslf.usermodel;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
 
 /**
  * Collects all tests from the package <tt>org.apache.poi.hslf.usermodel</tt>.
- * 
- * @author Josh Micich
  */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestAddingSlides.class,
+    TestBugs.class,
+    TestCounts.class,
+    TestMostRecentRecords.class,
+    TestNotesText.class,
+    TestPictures.class,
+    TestReOrderingSlides.class,
+    TestRecordSetup.class,
+    TestRichTextRun.class,
+    TestSheetText.class,
+    TestSlideOrdering.class,
+    TestSoundData.class,
+    TestFontRendering.class
+})
 public class AllHSLFUserModelTests {
-       
-       public static Test suite() {
-               TestSuite result = new TestSuite(AllHSLFUserModelTests.class.getName());
-               result.addTestSuite(TestAddingSlides.class);
-               result.addTestSuite(TestBugs.class);
-               result.addTestSuite(TestCounts.class);
-               result.addTestSuite(TestMostRecentRecords.class);
-               result.addTestSuite(TestNotesText.class);
-               result.addTestSuite(TestPictures.class);
-               result.addTestSuite(TestReOrderingSlides.class);
-               result.addTestSuite(TestRecordSetup.class);
-               result.addTestSuite(TestRichTextRun.class);
-               result.addTestSuite(TestSheetText.class);
-               result.addTestSuite(TestSlideOrdering.class);
-               result.addTestSuite(TestSoundData.class);
-               return result;
-       }
 }
diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java
new file mode 100644 (file)
index 0000000..e4fc7f9
--- /dev/null
@@ -0,0 +1,112 @@
+/* ====================================================================\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
+package org.apache.poi.hslf.usermodel;\r
+\r
+import static org.junit.Assert.assertTrue;\r
+import static org.junit.Assume.assumeTrue;\r
+\r
+import java.awt.Color;\r
+import java.awt.Dimension;\r
+import java.awt.Font;\r
+import java.awt.Graphics2D;\r
+import java.awt.GraphicsEnvironment;\r
+import java.awt.RenderingHints;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Rectangle2D;\r
+import java.awt.image.BufferedImage;\r
+import java.awt.image.DataBufferByte;\r
+import java.io.File;\r
+import java.io.InputStream;\r
+import java.util.Arrays;\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+\r
+import javax.imageio.ImageIO;\r
+\r
+import org.apache.poi.POIDataSamples;\r
+import org.apache.poi.hslf.model.Slide;\r
+import org.apache.poi.hslf.model.TextPainter;\r
+import org.junit.Test;\r
+\r
+/**\r
+ * Test font rendering of alternative and fallback fonts\r
+ */\r
+public class TestFontRendering {\r
+    private static POIDataSamples slTests = POIDataSamples.getSlideShowInstance();\r
+\r
+    @Test\r
+    public void bug55902mixedFontWithChineseCharacters() throws Exception {\r
+        // font files need to be downloaded first via\r
+        // ant test-scratchpad-download-resources\r
+        String fontFiles[][] = {\r
+            // Calibri is not available on *nix systems, so we need to use another similar free font\r
+            { "build/scratchpad-test-resources/Cabin-Regular.ttf", "mapped", "Calibri" },\r
+\r
+            // use "MS PGothic" if available (Windows only) ...\r
+            // for the junit test not all chars are rendered\r
+            { "build/scratchpad-test-resources/mona.ttf", "fallback", "Cabin" }\r
+        };\r
+        \r
+        // setup fonts (especially needed, when run under *nix systems)\r
+        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();\r
+        Map<String,String> fontMap = new HashMap<String,String>();\r
+        Map<String,String> fallbackMap = new HashMap<String,String>();\r
+        \r
+        for (String fontFile[] : fontFiles) {\r
+            File f = new File(fontFile[0]);\r
+            assumeTrue("necessary font file "+f.getName()+" not downloaded.", f.exists());\r
+            \r
+            Font font = Font.createFont(Font.TRUETYPE_FONT, f);\r
+            ge.registerFont(font);\r
+            \r
+            Map<String,String> map = ("mapped".equals(fontFile[1]) ? fontMap : fallbackMap);\r
+            map.put(fontFile[2], font.getFamily());\r
+        }\r
+        \r
+        InputStream is = slTests.openResourceAsStream("bug55902-mixedFontChineseCharacters.ppt");\r
+        SlideShow ss = new SlideShow(is);\r
+        is.close();\r
+        \r
+        Dimension pgsize = ss.getPageSize();\r
+        \r
+        Slide slide = ss.getSlides()[0];\r
+        \r
+        // render it\r
+        double zoom = 1;\r
+        AffineTransform at = new AffineTransform();\r
+        at.setToScale(zoom, zoom);\r
+        \r
+        BufferedImage imgActual = new BufferedImage((int)Math.ceil(pgsize.width*zoom), (int)Math.ceil(pgsize.height*zoom), BufferedImage.TYPE_3BYTE_BGR);\r
+        Graphics2D graphics = imgActual.createGraphics();\r
+        graphics.setRenderingHint(TextPainter.KEY_FONTFALLBACK, fallbackMap);\r
+        graphics.setRenderingHint(TextPainter.KEY_FONTMAP, fontMap);\r
+        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\r
+        graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);\r
+        graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);\r
+        graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);\r
+        graphics.setTransform(at);                \r
+        graphics.setPaint(Color.white);\r
+        graphics.fill(new Rectangle2D.Float(0, 0, pgsize.width, pgsize.height));\r
+        slide.draw(graphics);             \r
+        \r
+        BufferedImage imgExpected = ImageIO.read(slTests.getFile("bug55902-mixedChars.png"));\r
+        DataBufferByte expectedDB = (DataBufferByte)imgExpected.getRaster().getDataBuffer();\r
+        DataBufferByte actualDB = (DataBufferByte)imgActual.getRaster().getDataBuffer();\r
+        assertTrue(Arrays.equals(expectedDB.getData(0), actualDB.getData(0)));\r
+    }\r
+}\r
diff --git a/test-data/slideshow/bug55902-mixedChars.png b/test-data/slideshow/bug55902-mixedChars.png
new file mode 100644 (file)
index 0000000..4b17565
Binary files /dev/null and b/test-data/slideshow/bug55902-mixedChars.png differ
diff --git a/test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt b/test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt
new file mode 100644 (file)
index 0000000..4524e36
Binary files /dev/null and b/test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt differ