From: Andreas Beeker Date: Tue, 11 Feb 2014 23:16:54 +0000 (+0000) Subject: Bug 55902 - Mixed fonts issue with Chinese characters (unable to form images from... X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8c3785890a687f1d386de3e841264340987ef02f;p=poi.git Bug 55902 - Mixed fonts issue with Chinese characters (unable to form images from ppt) git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1567455 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/build.xml b/build.xml index 51bdf764fa..9deaa09cf5 100644 --- a/build.xml +++ b/build.xml @@ -834,7 +834,7 @@ under the License. - - @@ -1425,4 +1424,26 @@ under the License. + + + + + + + + + + + + + + + + + + + + + diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/TextPainter.java b/src/scratchpad/src/org/apache/poi/hslf/model/TextPainter.java index ca243ff290..6d0dbc82d0 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/TextPainter.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/TextPainter.java @@ -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 fontMap = (Map)graphics.getRenderingHint(KEY_FONTMAP); + if (fontMap != null && fontMap.containsKey(mappedFont)) { + mappedFont = fontMap.get(mappedFont); + } + @SuppressWarnings("unchecked") + Map fallbackMap = (Map)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; + } + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/AllHSLFTests.java b/src/scratchpad/testcases/org/apache/poi/hslf/AllHSLFTests.java index 8d9ca15196..e24b093dea 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/AllHSLFTests.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/AllHSLFTests.java @@ -17,35 +17,30 @@ 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 org.apache.poi.hslf 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; - } } diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/AllHSLFUserModelTests.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/AllHSLFUserModelTests.java index 0a6b40e96c..bc6b6cd972 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/AllHSLFUserModelTests.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/AllHSLFUserModelTests.java @@ -17,30 +17,27 @@ 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 org.apache.poi.hslf.usermodel. - * - * @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 index 0000000000..e4fc7f9310 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java @@ -0,0 +1,112 @@ +/* ==================================================================== + 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.hslf.usermodel; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.hslf.model.Slide; +import org.apache.poi.hslf.model.TextPainter; +import org.junit.Test; + +/** + * Test font rendering of alternative and fallback fonts + */ +public class TestFontRendering { + private static POIDataSamples slTests = POIDataSamples.getSlideShowInstance(); + + @Test + public void bug55902mixedFontWithChineseCharacters() throws Exception { + // font files need to be downloaded first via + // ant test-scratchpad-download-resources + String fontFiles[][] = { + // Calibri is not available on *nix systems, so we need to use another similar free font + { "build/scratchpad-test-resources/Cabin-Regular.ttf", "mapped", "Calibri" }, + + // use "MS PGothic" if available (Windows only) ... + // for the junit test not all chars are rendered + { "build/scratchpad-test-resources/mona.ttf", "fallback", "Cabin" } + }; + + // setup fonts (especially needed, when run under *nix systems) + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Map fontMap = new HashMap(); + Map fallbackMap = new HashMap(); + + for (String fontFile[] : fontFiles) { + File f = new File(fontFile[0]); + assumeTrue("necessary font file "+f.getName()+" not downloaded.", f.exists()); + + Font font = Font.createFont(Font.TRUETYPE_FONT, f); + ge.registerFont(font); + + Map map = ("mapped".equals(fontFile[1]) ? fontMap : fallbackMap); + map.put(fontFile[2], font.getFamily()); + } + + InputStream is = slTests.openResourceAsStream("bug55902-mixedFontChineseCharacters.ppt"); + SlideShow ss = new SlideShow(is); + is.close(); + + Dimension pgsize = ss.getPageSize(); + + Slide slide = ss.getSlides()[0]; + + // render it + double zoom = 1; + AffineTransform at = new AffineTransform(); + at.setToScale(zoom, zoom); + + BufferedImage imgActual = new BufferedImage((int)Math.ceil(pgsize.width*zoom), (int)Math.ceil(pgsize.height*zoom), BufferedImage.TYPE_3BYTE_BGR); + Graphics2D graphics = imgActual.createGraphics(); + graphics.setRenderingHint(TextPainter.KEY_FONTFALLBACK, fallbackMap); + graphics.setRenderingHint(TextPainter.KEY_FONTMAP, fontMap); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + graphics.setTransform(at); + graphics.setPaint(Color.white); + graphics.fill(new Rectangle2D.Float(0, 0, pgsize.width, pgsize.height)); + slide.draw(graphics); + + BufferedImage imgExpected = ImageIO.read(slTests.getFile("bug55902-mixedChars.png")); + DataBufferByte expectedDB = (DataBufferByte)imgExpected.getRaster().getDataBuffer(); + DataBufferByte actualDB = (DataBufferByte)imgActual.getRaster().getDataBuffer(); + assertTrue(Arrays.equals(expectedDB.getData(0), actualDB.getData(0))); + } +} diff --git a/test-data/slideshow/bug55902-mixedChars.png b/test-data/slideshow/bug55902-mixedChars.png new file mode 100644 index 0000000000..4b17565a66 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 index 0000000000..4524e361a3 Binary files /dev/null and b/test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt differ