From 8c3785890a687f1d386de3e841264340987ef02f Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Tue, 11 Feb 2014 23:16:54 +0000 Subject: [PATCH] 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 --- build.xml | 25 +++- .../apache/poi/hslf/model/TextPainter.java | 83 ++++++++++++- .../org/apache/poi/hslf/AllHSLFTests.java | 35 +++--- .../hslf/usermodel/AllHSLFUserModelTests.java | 39 +++--- .../poi/hslf/usermodel/TestFontRendering.java | 112 ++++++++++++++++++ test-data/slideshow/bug55902-mixedChars.png | Bin 0 -> 9092 bytes .../bug55902-mixedFontChineseCharacters.ppt | Bin 0 -> 72192 bytes 7 files changed, 246 insertions(+), 48 deletions(-) create mode 100644 src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java create mode 100644 test-data/slideshow/bug55902-mixedChars.png create mode 100644 test-data/slideshow/bug55902-mixedFontChineseCharacters.ppt 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 0000000000000000000000000000000000000000..4b17565a663eae1cab96f793a9da0533cb4a4811 GIT binary patch literal 9092 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 M{cEorp+$)Mf39iqfB*mh literal 0 HcmV?d00001 -- 2.39.5