aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--fop-core/pom.xml7
-rw-r--r--fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java17
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java14
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java11
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java12
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java33
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/Font.java110
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java13
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java96
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java63
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java5
-rw-r--r--fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java108
-rw-r--r--fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java6
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/PDFText.java18
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java30
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java13
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java5
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java17
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java24
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java88
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java4
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java26
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java13
-rw-r--r--fop-core/src/main/java/org/apache/fop/util/CharUtilities.java133
-rw-r--r--fop-core/src/main/java/org/apache/fop/util/HexEncoder.java15
-rw-r--r--fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java5
-rw-r--r--fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java18
-rw-r--r--fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java40
-rw-r--r--fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java7
-rw-r--r--fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java198
-rw-r--r--fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java139
-rw-r--r--fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java130
-rw-r--r--fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java119
-rw-r--r--fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java86
-rw-r--r--fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java74
-rw-r--r--fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java16
-rw-r--r--fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java73
-rw-r--r--fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java15
-rw-r--r--fop/build.xml2
-rw-r--r--fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml57
-rw-r--r--fop/test/resources/fonts/ttf/Aegean600.LICENSE3
-rw-r--r--fop/test/resources/fonts/ttf/Aegean600.ttfbin0 -> 2077252 bytes
-rw-r--r--fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE18
-rw-r--r--fop/test/resources/fonts/ttf/AndroidEmoji.ttfbin0 -> 448680 bytes
-rw-r--r--fop/test/xml/pdf-encoding/pdf-encoding-test.xconf9
-rw-r--r--fop/test/xml/pdf-encoding/test-custom-font.fo2
-rw-r--r--fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo37
47 files changed, 1677 insertions, 252 deletions
diff --git a/fop-core/pom.xml b/fop-core/pom.xml
index 1506ad82c..803b7c39c 100644
--- a/fop-core/pom.xml
+++ b/fop-core/pom.xml
@@ -137,6 +137,12 @@
<version>${xmlunit.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.pdfbox</groupId>
+ <artifactId>pdfbox</artifactId>
+ <version>2.0.3</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
@@ -308,6 +314,7 @@
<headerLocation>${project.baseUri}src/tools/resources/checkstyle/LICENSE.txt</headerLocation>
<includeResources>false</includeResources>
<includeTestResources>false</includeTestResources>
+ <includeTestSourceDirectory>true</includeTestSourceDirectory>
<linkXRef>false</linkXRef>
<logViolationsToConsole>true</logViolationsToConsole>
<suppressionsLocation>${project.baseUri}src/tools/resources/checkstyle/suppressions.xml</suppressionsLocation>
diff --git a/fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java b/fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java
index 5a1ac8b94..98dbfcdc2 100644
--- a/fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java
+++ b/fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java
@@ -147,6 +147,8 @@ public class GlyphSequence implements Cloneable {
/**
* Obtain the number of characters in character array, where
* each character constitutes a unicode scalar value.
+ * NB: Supplementary characters (non-BMP code points) count as 1
+ * character, not as two UTF-16 code units.
* @return number of characters available in character array
*/
public int getCharacterCount() {
@@ -154,6 +156,21 @@ public class GlyphSequence implements Cloneable {
}
/**
+ * Obtain the number of characters in character array, where
+ * each character constitutes a UTF-16 character. This means
+ * that every non-BMP character is counted as 2 characters.
+ * @return number of chars (UTF-16 code units) available in
+ * character array
+ */
+ public int getUTF16CharacterCount() {
+ int count = 0;
+ for (int ch : characters.array()) {
+ count += Character.charCount(ch);
+ }
+ return count;
+ }
+
+ /**
* Obtain glyph id at specified index.
* @param index to obtain glyph
* @return the glyph identifier of glyph at specified index
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java b/fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java
index 5aa7237e9..e1a329ef6 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java
@@ -71,6 +71,20 @@ public abstract class CIDFont extends CustomFont {
*/
public abstract CIDSet getCIDSet();
+ /**
+ * Determines whether this font contains a particular code point/glyph.
+ * @param cp character to check
+ * @return True if the character is supported, False otherwise
+ */
+ public abstract boolean hasCodePoint(int cp);
+
+ /**
+ * Map a Unicode code point to a code point in the font.
+ * @param cp code point to map
+ * @return the mapped code point
+ */
+ public abstract int mapCodePoint(int cp);
+
// ---- Optional ----
/**
* Returns the default width for this font.
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java b/fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java
index c108c4784..9d5184b02 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java
@@ -69,10 +69,10 @@ public class CIDFull implements CIDSet {
}
/** {@inheritDoc} */
- public char getUnicode(int index) {
+ public int getUnicode(int index) {
initGlyphIndices();
if (glyphIndices.get(index)) {
- return (char) index;
+ return index;
} else {
return CharUtilities.NOT_A_CHARACTER;
}
@@ -80,7 +80,12 @@ public class CIDFull implements CIDSet {
/** {@inheritDoc} */
public int mapChar(int glyphIndex, char unicode) {
- return (char) glyphIndex;
+ return glyphIndex;
+ }
+
+ /** {@inheritDoc} */
+ public int mapCodePoint(int glyphIndex, int codePoint) {
+ return glyphIndex;
}
/** {@inheritDoc} */
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java b/fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java
index acfc705c8..d89c8937b 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java
@@ -41,7 +41,7 @@ public interface CIDSet {
* @param index the subset index (character selector)
* @return the Unicode value or "NOT A CHARACTER" (0xFFFF)
*/
- char getUnicode(int index);
+ int getUnicode(int index);
/**
* Gets the unicode character from the original font glyph index
@@ -68,6 +68,16 @@ public interface CIDSet {
int mapChar(int glyphIndex, char unicode);
/**
+ * Maps a character to a character selector for a font subset. If the character isn't in the
+ * subset yet, it is added and a new character selector returned. Otherwise, the already
+ * allocated character selector is returned from the existing map/subset.
+ * @param glyphIndex the glyph index of the character
+ * @param codePoint the Unicode index of the character
+ * @return the subset index
+ */
+ int mapCodePoint(int glyphIndex, int codePoint);
+
+ /**
* Returns an unmodifiable Map of the font subset. It maps from glyph index to
* character selector (i.e. the subset index in this case).
* @return Map Map&lt;Integer, Integer&gt; of the font subset
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java b/fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java
index d2ebcfcb9..470e59a37 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java
@@ -52,12 +52,12 @@ public class CIDSubset implements CIDSet {
/**
* usedCharsIndex contains new glyph, original char (char selector -> Unicode)
*/
- private Map<Integer, Character> usedCharsIndex = new HashMap<Integer, Character>();
+ private Map<Integer, Integer> usedCharsIndex = new HashMap<Integer, Integer>();
/**
* A map between the original character and it's GID in the original font.
*/
- private Map<Character, Integer> charToGIDs = new HashMap<Character, Integer>();
+ private Map<Integer, Integer> charToGIDs = new HashMap<Integer, Integer>();
private final MultiByteFont font;
@@ -81,8 +81,8 @@ public class CIDSubset implements CIDSet {
}
/** {@inheritDoc} */
- public char getUnicode(int index) {
- Character mapValue = usedCharsIndex.get(index);
+ public int getUnicode(int index) {
+ Integer mapValue = usedCharsIndex.get(index);
if (mapValue != null) {
return mapValue;
} else {
@@ -92,6 +92,11 @@ public class CIDSubset implements CIDSet {
/** {@inheritDoc} */
public int mapChar(int glyphIndex, char unicode) {
+ return mapCodePoint(glyphIndex, unicode);
+ }
+
+ /** {@inheritDoc} */
+ public int mapCodePoint(int glyphIndex, int codePoint) {
// Reencode to a new subset font or get the reencoded value
// IOW, accumulate the accessed characters and build a character map for them
Integer subsetCharSelector = usedGlyphs.get(glyphIndex);
@@ -99,8 +104,8 @@ public class CIDSubset implements CIDSet {
int selector = usedGlyphsCount;
usedGlyphs.put(glyphIndex, selector);
usedGlyphsIndex.put(selector, glyphIndex);
- usedCharsIndex.put(selector, unicode);
- charToGIDs.put(unicode, glyphIndex);
+ usedCharsIndex.put(selector, codePoint);
+ charToGIDs.put(codePoint, glyphIndex);
usedGlyphsCount++;
return selector;
} else {
@@ -115,22 +120,28 @@ public class CIDSubset implements CIDSet {
/** {@inheritDoc} */
public char getUnicodeFromGID(int glyphIndex) {
+ // TODO this method is never called in the MultiByte font path.
+ // This is why we can safely cast the value of usedCharsIndex.get(selector)
+ // to int . BTW is a question if it should be changed to int as getUnicode
+ // or left like this.
int selector = usedGlyphs.get(glyphIndex);
- return usedCharsIndex.get(selector);
+ return (char) usedCharsIndex.get(selector).intValue();
}
/** {@inheritDoc} */
public int getGIDFromChar(char ch) {
- return charToGIDs.get(ch);
+ return charToGIDs.get((int) ch);
}
/** {@inheritDoc} */
public char[] getChars() {
- char[] charArray = new char[usedGlyphsCount];
+ StringBuilder buf = new StringBuilder();
+
for (int i = 0; i < usedGlyphsCount; i++) {
- charArray[i] = getUnicode(i);
+ buf.appendCodePoint(getUnicode(i));
}
- return charArray;
+
+ return buf.toString().toCharArray();
}
/** {@inheritDoc} */
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/Font.java b/fop-core/src/main/java/org/apache/fop/fonts/Font.java
index 93daa4b10..272019e96 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/Font.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/Font.java
@@ -28,6 +28,8 @@ import org.apache.commons.logging.LogFactory;
import org.apache.fop.complexscripts.fonts.Positionable;
import org.apache.fop.complexscripts.fonts.Substitutable;
+import org.apache.fop.render.java2d.CustomFontMetricsMapper;
+import org.apache.fop.util.CharUtilities;
/**
* This class holds font state information and provides access to the font
@@ -194,10 +196,17 @@ public class Font implements Substitutable, Positionable {
* @param ch2 second character
* @return the distance to adjust for kerning, 0 if there's no kerning
*/
- public int getKernValue(char ch1, char ch2) {
- Map<Integer, Integer> kernPair = getKerning().get((int) ch1);
+ public int getKernValue(int ch1, int ch2) {
+ // Isolate surrogate pair
+ if ((ch1 >= 0xD800) && (ch1 <= 0xE000)) {
+ return 0;
+ } else if ((ch2 >= 0xD800) && (ch2 <= 0xE000)) {
+ return 0;
+ }
+
+ Map<Integer, Integer> kernPair = getKerning().get(ch1);
if (kernPair != null) {
- Integer width = kernPair.get((int) ch2);
+ Integer width = kernPair.get(ch2);
if (width != null) {
return width * getFontSize() / 1000;
}
@@ -206,30 +215,6 @@ public class Font implements Substitutable, Positionable {
}
/**
- * Returns the amount of kerning between two characters.
- *
- * The value returned measures in pt. So it is already adjusted for font size.
- *
- * @param ch1 first character
- * @param ch2 second character
- * @return the distance to adjust for kerning, 0 if there's no kerning
- */
- public int getKernValue(int ch1, int ch2) {
- // TODO !BMP
- if (ch1 > 0x10000) {
- return 0;
- } else if ((ch1 >= 0xD800) && (ch1 <= 0xE000)) {
- return 0;
- } else if (ch2 > 0x10000) {
- return 0;
- } else if ((ch2 >= 0xD800) && (ch2 <= 0xE000)) {
- return 0;
- } else {
- return getKernValue((char) ch1, (char) ch2);
- }
- }
-
- /**
* Returns the width of a character
* @param charnum character to look up
* @return width of the character
@@ -264,9 +249,29 @@ public class Font implements Substitutable, Positionable {
}
/**
+ * Map a unicode code point to a font character.
+ * Default uses CodePointMapping.
+ * @param cp code point to map
+ * @return the mapped character
+ */
+ public int mapCodePoint(int cp) {
+ FontMetrics fontMetrics = getRealFontMetrics();
+
+ if (fontMetrics instanceof CIDFont) {
+ return ((CIDFont) fontMetrics).mapCodePoint(cp);
+ }
+
+ if (CharUtilities.isBmpCodePoint(cp)) {
+ return mapChar((char) cp);
+ }
+
+ return Typeface.NOT_FOUND;
+ }
+
+ /**
* Determines whether this font contains a particular character/glyph.
* @param c character to check
- * @return True if the character is supported, Falso otherwise
+ * @return True if the character is supported, False otherwise
*/
public boolean hasChar(char c) {
if (metric instanceof org.apache.fop.fonts.Typeface) {
@@ -278,6 +283,45 @@ public class Font implements Substitutable, Positionable {
}
/**
+ * Determines whether this font contains a particular code point/glyph.
+ * @param cp code point to check
+ * @return True if the code point is supported, False otherwise
+ */
+ public boolean hasCodePoint(int cp) {
+ FontMetrics realFont = getRealFontMetrics();
+
+ if (realFont instanceof CIDFont) {
+ return ((CIDFont) realFont).hasCodePoint(cp);
+ }
+
+ if (CharUtilities.isBmpCodePoint(cp)) {
+ return hasChar((char) cp);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the real underlying font if it is wrapped inside some container such as a {@link LazyFont} or a
+ * {@link CustomFontMetricsMapper}.
+ *
+ * @return instance of the font
+ */
+ private FontMetrics getRealFontMetrics() {
+ FontMetrics realFontMetrics = metric;
+
+ if (realFontMetrics instanceof CustomFontMetricsMapper) {
+ realFontMetrics = ((CustomFontMetricsMapper) realFontMetrics).getRealFont();
+ }
+
+ if (realFontMetrics instanceof LazyFont) {
+ return ((LazyFont) realFontMetrics).getRealFont();
+ }
+
+ return realFontMetrics;
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
@@ -380,10 +424,14 @@ public class Font implements Substitutable, Positionable {
public int getCharWidth(int c) {
if (c < 0x10000) {
return getCharWidth((char) c);
- } else {
- // TODO !BMP
- return -1;
}
+
+ if (hasCodePoint(c)) {
+ int mappedChar = mapCodePoint(c);
+ return getWidth(mappedChar);
+ }
+
+ return -1;
}
/**
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java b/fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java
index 66c30b21b..a19aae617 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java
@@ -24,6 +24,7 @@ import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.flow.Character;
import org.apache.fop.fo.properties.CommonFont;
+import org.apache.fop.util.CharUtilities;
/**
* Helper class for automatic font selection.
@@ -115,14 +116,18 @@ public final class FontSelector {
final Font font = fi.getFontInstance(fontkeys[fontnum],
commonFont.fontSize.getValue(context));
fonts[fontnum] = font;
- for (int pos = firstIndex; pos < breakIndex; pos++) {
- if (font.hasChar(charSeq.charAt(pos))) {
+
+ int numCodePoints = 0;
+ for (int cp : CharUtilities.codepointsIter(charSeq, firstIndex, breakIndex)) {
+ numCodePoints++;
+
+ if (font.hasCodePoint(cp)) {
fontCount[fontnum]++;
}
}
- // quick fall through if all characters can be displayed
- if (fontCount[fontnum] == (breakIndex - firstIndex)) {
+ // quick fall through if all codepoints can be displayed
+ if (fontCount[fontnum] == numCodePoints) {
return font;
}
}
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java b/fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java
index 95db0ff48..f79a04e97 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java
@@ -19,6 +19,7 @@
package org.apache.fop.fonts;
+import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
@@ -30,6 +31,8 @@ import org.apache.fop.complexscripts.util.CharScript;
import org.apache.fop.traits.MinOptMax;
import org.apache.fop.util.CharUtilities;
+import static org.apache.fop.fonts.type1.AdobeStandardEncoding.i;
+
/**
* Stores the mapping of a text fragment to glyphs, along with various information.
*/
@@ -57,7 +60,7 @@ public class GlyphMapping {
MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
Font font, int level, int[][] gposAdjustments) {
this(startIndex, endIndex, wordSpaceCount, letterSpaceCount, areaIPD, isHyphenated,
- isSpace, breakOppAfter, font, level, gposAdjustments, null, null);
+ isSpace, breakOppAfter, font, level, gposAdjustments, null, null);
}
public GlyphMapping(int startIndex, int endIndex, int wordSpaceCount, int letterSpaceCount,
@@ -87,11 +90,11 @@ public class GlyphMapping {
GlyphMapping mapping;
if (font.performsSubstitution() || font.performsPositioning()) {
mapping = processWordMapping(text, startIndex, endIndex, font,
- breakOpportunityChar, endsWithHyphen, level,
- dontOptimizeForIdentityMapping, retainAssociations, retainControls);
+ breakOpportunityChar, endsWithHyphen, level,
+ dontOptimizeForIdentityMapping, retainAssociations, retainControls);
} else {
mapping = processWordNoMapping(text, startIndex, endIndex, font,
- letterSpaceIPD, letterSpaceAdjustArray, precedingChar, breakOpportunityChar, endsWithHyphen, level);
+ letterSpaceIPD, letterSpaceAdjustArray, precedingChar, breakOpportunityChar, endsWithHyphen, level);
}
return mapping;
}
@@ -99,21 +102,20 @@ public class GlyphMapping {
private static GlyphMapping processWordMapping(TextFragment text, int startIndex,
int endIndex, final Font font, final char breakOpportunityChar,
final boolean endsWithHyphen, int level,
- boolean dontOptimizeForIdentityMapping, boolean retainAssociations, boolean retainControls) {
- int e = endIndex; // end index of word in FOText character buffer
+ boolean dontOptimizeForIdentityMapping, boolean retainAssociations, boolean retainControls) {
int nLS = 0; // # of letter spaces
String script = text.getScript();
String language = text.getLanguage();
if (LOG.isDebugEnabled()) {
LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {"
- + " +M"
- + ", level = " + level
- + " }");
+ + " +M"
+ + ", level = " + level
+ + " }");
}
// 1. extract unmapped character sequence.
- CharSequence ics = text.subSequence(startIndex, e);
+ CharSequence ics = text.subSequence(startIndex, endIndex);
// 2. if script is not specified (by FO property) or it is specified as 'auto',
// then compute dominant script.
@@ -126,7 +128,16 @@ public class GlyphMapping {
// 3. perform mapping of chars to glyphs ... to glyphs ... to chars, retaining
// associations if requested.
- List associations = retainAssociations ? new java.util.ArrayList() : null;
+ List associations = retainAssociations ? new ArrayList() : null;
+
+ // This is a workaround to read the ligature from the font even if the script
+ // does not match the one defined for the table.
+ // More info here: https://issues.apache.org/jira/browse/FOP-2638
+ // zyyy == SCRIPT_UNDEFINED
+ if ("zyyy".equals(script) || "auto".equals(script)) {
+ script = "*";
+ }
+
CharSequence mcs = font.performSubstitution(ics, script, language, associations, retainControls);
// 4. compute glyph position adjustments on (substituted) characters.
@@ -148,7 +159,11 @@ public class GlyphMapping {
MinOptMax ipd = MinOptMax.ZERO;
for (int i = 0, n = mcs.length(); i < n; i++) {
int c = mcs.charAt(i);
- // TODO !BMP
+
+ if (CharUtilities.containsSurrogatePairAt(mcs, i)) {
+ c = Character.toCodePoint((char) c, mcs.charAt(++i));
+ }
+
int w = font.getCharWidth(c);
if (w < 0) {
w = 0;
@@ -161,7 +176,7 @@ public class GlyphMapping {
// [TBD] - handle letter spacing
- return new GlyphMapping(startIndex, e, 0, nLS, ipd, endsWithHyphen, false,
+ return new GlyphMapping(startIndex, endIndex, 0, nLS, ipd, endsWithHyphen, false,
breakOpportunityChar != 0, font, level, gpa,
!dontOptimizeForIdentityMapping && CharUtilities.isSameSequence(mcs, ics) ? null : mcs.toString(),
associations);
@@ -180,21 +195,23 @@ public class GlyphMapping {
* @return glyph position adjustments (or null if no kerning)
*/
private static int[][] getKerningAdjustments(CharSequence mcs, final Font font, int[][] gpa) {
- int nc = mcs.length();
+ int numCodepoints = Character.codePointCount(mcs, 0, mcs.length());
// extract kerning array
- int[] ka = new int[nc]; // kerning array
- for (int i = 0, n = nc, cPrev = -1; i < n; i++) {
- int c = mcs.charAt(i);
- // TODO !BMP
- if (cPrev >= 0) {
- ka[i] = font.getKernValue(cPrev, c);
+ int[] kernings = new int[numCodepoints]; // kerning array
+
+ int prevCp = -1;
+ int i = 0;
+ for (int cp : CharUtilities.codepointsIter(mcs)) {
+ if (prevCp >= 0) {
+ kernings[i] = font.getKernValue(prevCp, cp);
}
- cPrev = c;
+ prevCp = cp;
+ i++;
}
// was there a non-zero kerning?
boolean hasKerning = false;
- for (int i = 0, n = nc; i < n; i++) {
- if (ka[i] != 0) {
+ for (int kerningValue : kernings) {
+ if (kerningValue != 0) {
hasKerning = true;
break;
}
@@ -202,11 +219,11 @@ public class GlyphMapping {
// if non-zero kerning, then create and return glyph position adjustment array
if (hasKerning) {
if (gpa == null) {
- gpa = new int[nc][4];
+ gpa = new int[numCodepoints][4];
}
- for (int i = 0, n = nc; i < n; i++) {
+ for (i = 0; i < numCodepoints; i++) {
if (i > 0) {
- gpa [i - 1][GlyphPositioningTable.Value.IDX_X_ADVANCE] += ka[i];
+ gpa [i - 1][GlyphPositioningTable.Value.IDX_X_ADVANCE] += kernings[i];
}
}
return gpa;
@@ -223,13 +240,14 @@ public class GlyphMapping {
if (LOG.isDebugEnabled()) {
LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {"
- + " -M"
- + ", level = " + level
- + " }");
+ + " -M"
+ + ", level = " + level
+ + " }");
}
- for (int i = startIndex; i < endIndex; i++) {
- char currentChar = text.charAt(i);
+ CharSequence ics = text.subSequence(startIndex, endIndex);
+ int offset = 0;
+ for (int currentChar : CharUtilities.codepointsIter(ics)) {
// character width
int charWidth = font.getCharWidth(currentChar);
@@ -238,24 +256,32 @@ public class GlyphMapping {
// kerning
if (kerning) {
int kern = 0;
- if (i > startIndex) {
- char previousChar = text.charAt(i - 1);
+ if (offset > 0) {
+ int previousChar = java.lang.Character.codePointAt(ics, offset - 1);
kern = font.getKernValue(previousChar, currentChar);
} else if (precedingChar != 0) {
kern = font.getKernValue(precedingChar, currentChar);
}
if (kern != 0) {
- addToLetterAdjust(letterSpaceAdjustArray, i, kern);
+ addToLetterAdjust(letterSpaceAdjustArray, startIndex + offset, kern);
wordIPD = wordIPD.plus(kern);
}
}
+ offset++;
}
if (kerning
&& (breakOpportunityChar != 0)
&& !isSpace(breakOpportunityChar)
&& endIndex > 0
&& endsWithHyphen) {
- int kern = font.getKernValue(text.charAt(endIndex - 1), breakOpportunityChar);
+ int endChar = text.charAt(endIndex - 1);
+
+ if (java.lang.Character.isLowSurrogate((char) endChar)) {
+ char highSurrogate = text.charAt(endIndex - 2);
+ endChar = java.lang.Character.toCodePoint(highSurrogate, (char) endChar);
+ }
+
+ int kern = font.getKernValue(endChar, (int) breakOpportunityChar);
if (kern != 0) {
addToLetterAdjust(letterSpaceAdjustArray, endIndex, kern);
// TODO: add kern to wordIPD?
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java b/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
index f54dec9a6..39adc4926 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
@@ -23,6 +23,7 @@ import java.awt.Rectangle;
import java.io.InputStream;
import java.nio.CharBuffer;
import java.nio.IntBuffer;
+import java.util.ArrayList;
import java.util.BitSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -379,8 +380,36 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
/** {@inheritDoc} */
@Override
+ public int mapCodePoint(int cp) {
+ notifyMapOperation();
+ int glyphIndex = findGlyphIndex(cp);
+ if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) {
+
+ for (char ch : Character.toChars(cp)) {
+ //TODO better handling for non BMP
+ warnMissingGlyph(ch);
+ }
+
+ if (!isOTFFile) {
+ glyphIndex = findGlyphIndex(Typeface.NOT_FOUND);
+ }
+ }
+ if (isEmbeddable()) {
+ glyphIndex = cidSet.mapCodePoint(glyphIndex, cp);
+ }
+ return (char) glyphIndex;
+ }
+
+ /** {@inheritDoc} */
+ @Override
public boolean hasChar(char c) {
- return (findGlyphIndex(c) != SingleByteEncoding.NOT_FOUND_CODE_POINT);
+ return hasCodePoint(c);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasCodePoint(int cp) {
+ return (findGlyphIndex(cp) != SingleByteEncoding.NOT_FOUND_CODE_POINT);
}
/**
@@ -528,6 +557,8 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
if (!retainControls) {
ogs = elideControls(ogs);
}
+ // ocs may not contains all the characters that were in cs.
+ // see: #createPrivateUseMapping(int gi)
CharSequence ocs = mapGlyphsToChars(ogs);
return ocs;
} else {
@@ -664,8 +695,9 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
*/
private CharSequence mapGlyphsToChars(GlyphSequence gs) {
int ng = gs.getGlyphCount();
- CharBuffer cb = CharBuffer.allocate(ng);
int ccMissing = Typeface.NOT_FOUND;
+ List<Character> chars = new ArrayList<Character>(gs.getUTF16CharacterCount());
+
for (int i = 0, n = ng; i < n; i++) {
int gi = gs.getGlyph(i);
int cc = findCharacterFromGlyphIndex(gi);
@@ -682,12 +714,19 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
cc -= 0x10000;
sh = ((cc >> 10) & 0x3FF) + 0xD800;
sl = ((cc >> 0) & 0x3FF) + 0xDC00;
- cb.put((char) sh);
- cb.put((char) sl);
+ chars.add((char) sh);
+ chars.add((char) sl);
} else {
- cb.put((char) cc);
+ chars.add((char) cc);
}
}
+
+ CharBuffer cb = CharBuffer.allocate(chars.size());
+
+ for (char c : chars) {
+ cb.put(c);
+ }
+
cb.flip();
return cb;
}
@@ -723,6 +762,14 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
return sb;
}
+ /**
+ * Removes the glyphs associated with elidable control characters.
+ * All the characters in an association must be elidable in order
+ * to remove the corresponding glyph.
+ *
+ * @param gs GlyphSequence that may contains the elidable glyphs
+ * @return GlyphSequence without the elidable glyphs
+ */
private static GlyphSequence elideControls(GlyphSequence gs) {
if (hasElidableControl(gs)) {
int[] ca = gs.getCharacterArray(false);
@@ -734,13 +781,15 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
int e = a.getEnd();
while (s < e) {
int ch = ca [ s ];
- if (isElidableControl(ch)) {
+ if (!isElidableControl(ch)) {
break;
} else {
++s;
}
}
- if (s == e) {
+ // If there is at least one non-elidable character in the char
+ // sequence then the glyph/association is kept.
+ if (s != e) {
ngb.put(gs.getGlyph(i));
nal.add(a);
}
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java b/fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java
index b52f03849..dcd901998 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java
@@ -19,6 +19,7 @@
package org.apache.fop.fonts.truetype;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -30,7 +31,7 @@ public class OFMtxEntry {
private int lsb;
private String name = "";
private int index;
- private List unicodeIndex = new java.util.ArrayList();
+ private List<Integer> unicodeIndex = new ArrayList<Integer>();
private int[] boundingBox = new int[4];
private long offset;
private byte found;
@@ -131,7 +132,7 @@ public class OFMtxEntry {
* Returns the unicodeIndex.
* @return List
*/
- public List getUnicodeIndex() {
+ public List<Integer> getUnicodeIndex() {
return unicodeIndex;
}
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java b/fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java
index fb6ebbd11..586943d58 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java
@@ -390,6 +390,10 @@ public abstract class OpenFont {
* tables are present. Currently only unicode cmaps are supported.
* Set the unicodeIndex in the TTFMtxEntries and fills in the
* cmaps vector.
+ *
+ * @see <a href="https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html">
+ * TrueType-Reference-Manual
+ * </a>
*/
protected boolean readCMAP() throws IOException {
@@ -401,6 +405,7 @@ public abstract class OpenFont {
int numCMap = fontFile.readTTFUShort(); // Number of cmap subtables
long cmapUniOffset = 0;
long symbolMapOffset = 0;
+ long surrogateMapOffset = 0;
if (log.isDebugEnabled()) {
log.debug(numCMap + " cmap tables");
@@ -422,9 +427,15 @@ public abstract class OpenFont {
if (cmapPID == 3 && cmapEID == 0) {
symbolMapOffset = cmapOffset;
}
+ if (cmapPID == 3 && cmapEID == 10) {
+ surrogateMapOffset = cmapOffset;
+ }
}
- if (cmapUniOffset > 0) {
+ if (surrogateMapOffset > 0) {
+ // TODO maybe for SingleByte fonts instances we should not reach this branch
+ return readUnicodeCmap(surrogateMapOffset, 10);
+ } else if (cmapUniOffset > 0) {
return readUnicodeCmap(cmapUniOffset, 1);
} else if (symbolMapOffset > 0) {
return readUnicodeCmap(symbolMapOffset, 0);
@@ -443,14 +454,21 @@ public abstract class OpenFont {
// Read unicode cmap
seekTab(fontFile, OFTableName.CMAP, cmapUniOffset);
int cmapFormat = fontFile.readTTFUShort();
- /*int cmap_length =*/ fontFile.readTTFUShort(); //skip cmap length
+
+ if (cmapFormat < 8) {
+ fontFile.readTTFUShort(); //skip cmap length
+ fontFile.readTTFUShort(); //skip cmap version
+ } else {
+ fontFile.readTTFUShort(); //skip 2 bytes to read a Fixed32
+ fontFile.readTTFULong(); //skip cmap length
+ fontFile.readTTFULong(); //skip cmap version
+ }
if (log.isDebugEnabled()) {
log.debug("CMAP format: " + cmapFormat);
}
if (cmapFormat == 4) {
- fontFile.skip(2); // Skip version number
int cmapSegCountX2 = fontFile.readTTFUShort();
int cmapSearchRange = fontFile.readTTFUShort();
int cmapEntrySelector = fontFile.readTTFUShort();
@@ -616,6 +634,90 @@ public abstract class OpenFont {
}
}
}
+ } else if (cmapFormat == 12) {
+ long nGroups = fontFile.readTTFULong();
+
+ for (long i = 0; i < nGroups; ++i) {
+ long startCharCode = fontFile.readTTFULong();
+ long endCharCode = fontFile.readTTFULong();
+ long startGlyphCode = fontFile.readTTFULong();
+
+ if (startCharCode < 0 || startCharCode > 0x10FFFFL) {
+ log.warn("startCharCode outside Unicode range");
+ continue;
+ }
+
+ if (startCharCode >= 0xD800 && startCharCode <= 0xDFFF) {
+ log.warn("startCharCode is a surrogate pair: " + startCharCode);
+ }
+
+ //endCharCode outside unicode range or is surrogate pair.
+ if (endCharCode > 0 && endCharCode < startCharCode || endCharCode > 0x10FFFFL) {
+ log.warn("startCharCode outside Unicode range");
+ continue;
+ }
+
+ if (endCharCode >= 0xD800 && endCharCode <= 0xDFFF) {
+ log.warn("endCharCode is a surrogate pair: " + startCharCode);
+ }
+
+ for (long offset = 0; offset <= endCharCode - startCharCode; ++offset) {
+ long glyphIndexL = startGlyphCode + offset;
+ long charCodeL = startCharCode + offset;
+
+ if (glyphIndexL >= numberOfGlyphs) {
+ log.warn("Format 12 cmap contains an invalid glyph index");
+ break;
+ }
+
+ if (charCodeL > 0x10FFFFL) {
+ log.warn("Format 12 cmap contains character beyond UCS-4");
+ }
+
+ if (glyphIndexL > Integer.MAX_VALUE) {
+ log.error("glyphIndex > Integer.MAX_VALUE");
+ continue;
+ }
+
+ if (charCodeL > Integer.MAX_VALUE) {
+ log.error("startCharCode + j > Integer.MAX_VALUE");
+ continue;
+ }
+
+ // Update lastChar
+ if (charCodeL < 0xFF && charCodeL > lastChar) {
+ lastChar = (short) charCodeL;
+ }
+
+ int charCode = (int) charCodeL;
+ int glyphIndex = (int) glyphIndexL;
+
+ // Also add winAnsiWidth.
+ List<Integer> ansiIndexes = null;
+
+ if (charCodeL <= java.lang.Character.MAX_VALUE) {
+ ansiIndexes = ansiIndex.get((int) charCodeL);
+ }
+
+ unicodeMappings.add(new UnicodeMapping(this, glyphIndex, charCode));
+ mtxTab[glyphIndex].getUnicodeIndex().add(charCode);
+
+ if (ansiIndexes == null) {
+ continue;
+ }
+
+ for (Integer aIdx : ansiIndexes) {
+ ansiWidth[aIdx] = mtxTab[glyphIndex].getWx();
+
+ if (log.isTraceEnabled()) {
+ log.trace("Added width "
+ + mtxTab[glyphIndex].getWx()
+ + " uni: " + offset
+ + " ansi: " + aIdx);
+ }
+ }
+ }
+ }
} else {
log.error("Cmap format not supported: " + cmapFormat);
return false;
diff --git a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java
index 65b91c3b6..27bd544dd 100644
--- a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java
+++ b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java
@@ -1023,8 +1023,10 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
//log.info("Word: " + new String(textArray, startIndex, stopIndex - startIndex));
for (int i = startIndex; i < stopIndex; i++) {
- char ch = foText.charAt(i);
- newIPD = newIPD.plus(font.getCharWidth(ch));
+ int cp = Character.codePointAt(foText, i);
+ i += Character.charCount(cp) - 1;
+
+ newIPD = newIPD.plus(font.getCharWidth(cp));
//if (i > startIndex) {
if (i < stopIndex) {
MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1];
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFText.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFText.java
index 927540f89..749bf5a30 100644
--- a/fop-core/src/main/java/org/apache/fop/pdf/PDFText.java
+++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFText.java
@@ -21,6 +21,10 @@ package org.apache.fop.pdf;
import java.io.ByteArrayOutputStream;
+import java.util.Locale;
+
+import org.apache.fop.util.CharUtilities;
+
/**
* This class represents a simple number object. It also contains contains some
* utility methods for outputting numbers to PDF.
@@ -205,13 +209,19 @@ public class PDFText extends PDFObject {
/**
* Convert a char to a multibyte hex representation appending to string buffer.
- * Since Java always stores strings in UTF-16, we don't have to do any conversion.
+ * The created string will be:
+ * <ul>
+ * <li>4-character string in case of non-BMP character</li>
+ * <li>6-character string in case of BMP character</li>
+ * </ul>
* @param c character to encode
* @param sb the string buffer to append output
*/
- public static final void toUnicodeHex(char c, StringBuffer sb) {
- for (int i = 0; i < 4; ++i) {
- sb.append(DIGITS[(c >> (12 - 4 * i)) & 0x0F]);
+ public static final void toUnicodeHex(int c, StringBuffer sb) {
+ if (CharUtilities.isBmpCodePoint(c)) {
+ sb.append(Integer.toHexString(c + 0x10000).substring(1).toUpperCase(Locale.US));
+ } else {
+ sb.append(Integer.toHexString(c + 0x1000000).substring(1).toUpperCase(Locale.US));
}
}
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java
index 2a1dc5039..552864d96 100644
--- a/fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java
+++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java
@@ -93,12 +93,12 @@ public abstract class PDFTextUtil {
PDFNumber.doubleOut(lt[5], DEC, sb);
}
- private static void writeChar(char ch, StringBuffer sb, boolean multibyte, boolean cid) {
+ private static void writeChar(int codePoint, StringBuffer sb, boolean multibyte, boolean cid) {
if (!multibyte) {
- if (cid || ch < 32 || ch > 127) {
- sb.append("\\").append(Integer.toOctalString(ch));
+ if (cid || codePoint < 32 || codePoint > 127) {
+ sb.append("\\").append(Integer.toOctalString(codePoint));
} else {
- switch (ch) {
+ switch (codePoint) {
case '(':
case ')':
case '\\':
@@ -106,15 +106,15 @@ public abstract class PDFTextUtil {
break;
default:
}
- sb.append(ch);
+ sb.appendCodePoint(codePoint);
}
} else {
- PDFText.toUnicodeHex(ch, sb);
+ PDFText.toUnicodeHex(codePoint, sb);
}
}
- private void writeChar(char ch, StringBuffer sb) {
- writeChar(ch, sb, useMultiByte, useCid);
+ private void writeChar(int codePoint, StringBuffer sb) {
+ writeChar(codePoint, sb, useMultiByte, useCid);
}
private void checkInTextObject() {
@@ -260,9 +260,17 @@ public abstract class PDFTextUtil {
/**
* Writes a char to the "TJ-Buffer".
- * @param codepoint the mapped character (code point/character code)
+ * @param ch the mapped character (code point/character code)
*/
- public void writeTJMappedChar(char codepoint) {
+ public void writeTJMappedChar(char ch) {
+ writeTJMappedCodePoint((int) ch);
+ }
+
+ /**
+ * Writes a codepoint to the "TJ-Buffer".
+ * @param codePoint the mapped character (code point/character code)
+ */
+ public void writeTJMappedCodePoint(int codePoint) {
if (bufTJ == null) {
bufTJ = new StringBuffer();
}
@@ -270,7 +278,7 @@ public abstract class PDFTextUtil {
bufTJ.append('[');
bufTJ.append(startText);
}
- writeChar(codepoint, bufTJ);
+ writeChar(codePoint, bufTJ);
}
/**
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java
index 56ca884f9..ee773dcec 100644
--- a/fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java
+++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java
@@ -129,8 +129,17 @@ public class PDFToUnicodeCMap extends PDFCMap {
charIndex++;
}
writer.write("<" + padCharIndex(charIndex) + "> ");
- writer.write("<" + padHexString(Integer.toHexString(charArray[charIndex]), 4)
- + ">\n");
+
+ if (Character.codePointAt(charArray, charIndex) > 0xFFFF) {
+ // Handle UTF-16 surrogate pairs
+ String pairs = Integer.toHexString(charArray[charIndex])
+ + Integer.toHexString(charArray[++charIndex]);
+ writer.write("<" + pairs + ">\n");
+ i++;
+ } else {
+ writer.write("<" + padHexString(Integer.toHexString(charArray[charIndex]), 4)
+ + ">\n");
+ }
charIndex++;
}
remainingEntries -= entriesThisSection;
diff --git a/fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java b/fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java
index c7f5da116..4e656cea8 100644
--- a/fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java
+++ b/fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java
@@ -221,6 +221,11 @@ public class CustomFontMetricsMapper extends Typeface implements FontMetricsMapp
return typeface.hasKerningInfo();
}
+ /** {@inheritDoc} */
+ public boolean isMultiByte() {
+ return typeface.isMultiByte();
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java
index 06f67e75b..23636de68 100644
--- a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java
+++ b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java
@@ -239,7 +239,7 @@ public class Java2DPainter extends AbstractIFPainter<IFDocumentHandler> {
g2dState.updateFont(font.getFontName(), state.getFontSize() * 1000);
Graphics2D g2d = this.g2dState.getGraph();
- GlyphVector gv = g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), text);
+ GlyphVector gv = Java2DUtil.createGlyphVector(text, g2d, font, fontInfo);
Point2D cursor = new Point2D.Float(0, 0);
int l = text.length();
@@ -248,8 +248,17 @@ public class Java2DPainter extends AbstractIFPainter<IFDocumentHandler> {
cursor.setLocation(cursor.getX() + dp[0][0], cursor.getY() - dp[0][1]);
gv.setGlyphPosition(0, cursor);
}
+
+ int currentIdx = 0;
for (int i = 0; i < l; i++) {
- char orgChar = text.charAt(i);
+ int orgChar = text.codePointAt(i);
+ // The dp (GPOS/kerning adjustment) is performed over glyphs and not
+ // characters (GlyphMapping.processWordMapping). The length of dp is
+ // adjusted later to fit the length of the String adding trailing 0.
+ // This means that it's probably ok to consume one of the 2 surrogate
+ // pairs.
+ i += CharUtilities.incrementIfNonBMP(orgChar);
+
float xGlyphAdjust = 0;
float yGlyphAdjust = 0;
int cw = font.getCharWidth(orgChar);
@@ -268,7 +277,7 @@ public class Java2DPainter extends AbstractIFPainter<IFDocumentHandler> {
}
cursor.setLocation(cursor.getX() + cw + xGlyphAdjust, cursor.getY() - yGlyphAdjust);
- gv.setGlyphPosition(i + 1, cursor);
+ gv.setGlyphPosition(++currentIdx, cursor);
}
g2d.drawGlyphVector(gv, x, y);
}
@@ -289,6 +298,4 @@ public class Java2DPainter extends AbstractIFPainter<IFDocumentHandler> {
g2dState.transform(transform);
}
-
-
}
diff --git a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java
index b12d708ba..825dc3754 100644
--- a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java
+++ b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java
@@ -732,7 +732,7 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem
AffineTransform at = new AffineTransform();
at.translate(rx / 1000f, bl / 1000f);
state.transform(at);
- renderText(text, state.getGraph(), font);
+ renderText(text, state.getGraph(), font, fontInfo);
restoreGraphicsState();
currentIPPosition = saveIP + text.getAllocIPD();
@@ -750,8 +750,9 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem
* @param text the TextArea
* @param g2d the Graphics2D to render to
* @param font the font to paint with
+ * @param fontInfo the font information
*/
- public static void renderText(TextArea text, Graphics2D g2d, Font font) {
+ public static void renderText(TextArea text, Graphics2D g2d, Font font, FontInfo fontInfo) {
Color col = (Color) text.getTrait(Trait.COLOR);
g2d.setColor(col);
@@ -763,7 +764,7 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem
WordArea word = (WordArea) child;
String s = word.getWord();
int[] letterAdjust = word.getLetterAdjustArray();
- GlyphVector gv = g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), s);
+ GlyphVector gv = Java2DUtil.createGlyphVector(s, g2d, font, fontInfo);
double additionalWidth = 0.0;
if (letterAdjust == null
&& text.getTextLetterSpaceAdjust() == 0
@@ -772,12 +773,21 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem
} else {
int[] offsets = getGlyphOffsets(s, font, text, letterAdjust);
float cursor = 0.0f;
- for (int i = 0; i < offsets.length; i++) {
+
+ if (offsets.length != gv.getNumGlyphs()) {
+ log.error(String.format("offsets length different from glyphNumber: %d != %d",
+ offsets.length, gv.getNumGlyphs()));
+ }
+
+ // If for any reason offsets.length != gv.getNumGlyphs() then we have to choose the minimum to avoid
+ // ArrayIndexOutOfBoundsException. This might happen when surrogate pairs are not correctly handled.
+ for (int i = 0; i < Math.min(offsets.length, gv.getNumGlyphs()); i++) {
Point2D pt = gv.getGlyphPosition(i);
pt.setLocation(cursor, pt.getY());
gv.setGlyphPosition(i, pt);
cursor += offsets[i] / 1000f;
}
+
additionalWidth = cursor - gv.getLogicalBounds().getWidth();
}
g2d.drawGlyphVector(gv, textCursor, 0);
@@ -800,11 +810,11 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem
private static int[] getGlyphOffsets(String s, Font font, TextArea text,
int[] letterAdjust) {
- int textLen = s.length();
+ int textLen = s.codePointCount(0, s.length());
int[] offsets = new int[textLen];
for (int i = 0; i < textLen; i++) {
- final char c = s.charAt(i);
- final char mapped = font.mapChar(c);
+ int c = s.codePointAt(i);
+ final int mapped = font.mapCodePoint(c);
int wordSpace;
if (CharUtilities.isAdjustableSpace(mapped)) {
diff --git a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java
index c17ad2994..bc8f05b00 100644
--- a/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java
+++ b/fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java
@@ -19,11 +19,20 @@
package org.apache.fop.render.java2d;
+import java.awt.Graphics2D;
+import java.awt.font.GlyphVector;
+import java.util.Arrays;
+
import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontCollection;
import org.apache.fop.fonts.FontEventAdapter;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontManager;
+import org.apache.fop.fonts.LazyFont;
+import org.apache.fop.fonts.MultiByteFont;
+import org.apache.fop.fonts.Typeface;
+import org.apache.fop.util.CharUtilities;
/**
* Rendering-related utilities for Java2D.
@@ -56,5 +65,84 @@ public final class Java2DUtil {
return fi;
}
+ /**
+ * Creates an instance of {@link GlyphVector} that correctly handle surrogate pairs and advanced font features such
+ * as GSUB/GPOS/GDEF.
+ *
+ * @param text Text to render
+ * @param g2d the target Graphics2D instance
+ * @param font the font instance
+ * @param fontInfo the font information
+ * @return an instance of {@link GlyphVector}
+ */
+ public static GlyphVector createGlyphVector(String text, Graphics2D g2d, Font font, FontInfo fontInfo) {
+ MultiByteFont multiByteFont = getMultiByteFont(font.getFontName(), fontInfo);
+
+ if (multiByteFont == null) {
+ return createGlyphVector(text, g2d);
+ }
+
+ return createGlyphVectorMultiByteFont(text, g2d, multiByteFont);
+ }
+
+ /**
+ * Creates a {@link GlyphVector} using characters. Filters out non-bmp characters.
+ */
+ private static GlyphVector createGlyphVector(String text, Graphics2D g2d) {
+ StringBuilder sb = new StringBuilder(text.length());
+ for (int cp : CharUtilities.codepointsIter(text)) {
+ // If we are here we probably do not support non-BMP codepoints
+ sb.appendCodePoint(cp <= 0xFFFF ? cp : Typeface.NOT_FOUND);
+ }
+ return g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), sb.toString());
+ }
+
+ /**
+ * Creates a {@link GlyphVector} using glyph indexes instead of characters. To correctly support the advanced font
+ * features we have to build the GlyphVector passing the glyph indexes instead of the characters. This because some
+ * of the chars in text might have been replaced by an internal font representation during
+ * GlyphMapping.processWordMapping. Eg 'fi' replaced with the corresponding character in the font ligatures table
+ * (GSUB).
+ */
+ private static GlyphVector createGlyphVectorMultiByteFont(String text, Graphics2D g2d,
+ MultiByteFont multiByteFont) {
+ int[] glyphCodes = new int[text.length()];
+ int currentIdx = 0;
+
+ for (int cp : CharUtilities.codepointsIter(text)) {
+ // mapChar is not working here because MultiByteFont.mapChar replaces the glyph index with
+ // CIDSet.mapChar when isEmbeddable == true.
+ glyphCodes[currentIdx++] = multiByteFont.findGlyphIndex(cp);
+ }
+
+ // Trims glyphCodes
+ if (currentIdx != text.length()) {
+ glyphCodes = Arrays.copyOf(glyphCodes, currentIdx);
+ }
+
+ return g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), glyphCodes);
+ }
+
+ /**
+ * Returns an instance of {@link MultiByteFont} for the given font name. This method will try to unwrap containers
+ * such as {@link CustomFontMetricsMapper} and {@link LazyFont}
+ *
+ * @param fontName font key
+ * @param fontInfo font information
+ * @return An instance of {@link MultiByteFont} or null if it
+ */
+ private static MultiByteFont getMultiByteFont(String fontName, FontInfo fontInfo) {
+ Typeface tf = fontInfo.getFonts().get(fontName);
+
+ if (tf instanceof CustomFontMetricsMapper) {
+ tf = ((CustomFontMetricsMapper) tf).getRealFont();
+ }
+
+ if (tf instanceof LazyFont) {
+ tf = ((LazyFont) tf).getRealFont();
+ }
+
+ return (tf instanceof MultiByteFont) ? (MultiByteFont) tf : null;
+ }
}
diff --git a/fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java b/fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java
index ff397fe1d..6b2fc23d2 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java
@@ -627,7 +627,7 @@ public class PCLTTFFontReader extends PCLFontReader {
int nextOffset = 0;
int charCode = 0;
if (entry.getUnicodeIndex().size() > 0) {
- charCode = (Integer) entry.getUnicodeIndex().get(0);
+ charCode = entry.getUnicodeIndex().get(0);
} else {
charCode = entry.getIndex();
}
@@ -743,7 +743,7 @@ public class PCLTTFFontReader extends PCLFontReader {
OFMtxEntry entry = mtx.get(i);
int charCode = 0;
if (entry.getUnicodeIndex().size() > 0) {
- charCode = (Integer) entry.getUnicodeIndex().get(0);
+ charCode = entry.getUnicodeIndex().get(0);
} else {
charCode = entry.getIndex();
}
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java
index 942027673..a47203d53 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java
@@ -479,11 +479,17 @@ public class PDFPainter extends AbstractIFPainter<PDFDocumentHandler> {
textutil.adjustGlyphTJ(-dx[0] / fontSize);
}
for (int i = 0; i < l; i++) {
- char orgChar = text.charAt(i);
- char ch;
+ int orgChar = text.charAt(i);
+ int ch;
+
+ // surrogate pairs have to be merged in a single code point
+ if (CharUtilities.containsSurrogatePairAt(text, i)) {
+ orgChar = Character.toCodePoint((char) orgChar, text.charAt(++i));
+ }
+
float glyphAdjust = 0;
- if (font.hasChar(orgChar)) {
- ch = font.mapChar(orgChar);
+ if (font.hasCodePoint(orgChar)) {
+ ch = font.mapCodePoint(orgChar);
ch = selectAndMapSingleByteFont(tf, fontName, fontSize, textutil, ch);
if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) {
glyphAdjust += wordSpacing;
@@ -495,14 +501,14 @@ public class PDFPainter extends AbstractIFPainter<PDFDocumentHandler> {
int spaceDiff = font.getCharWidth(CharUtilities.SPACE) - font.getCharWidth(orgChar);
glyphAdjust = -spaceDiff;
} else {
- ch = font.mapChar(orgChar);
+ ch = font.mapCodePoint(orgChar);
if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) {
glyphAdjust += wordSpacing;
}
}
ch = selectAndMapSingleByteFont(tf, fontName, fontSize, textutil, ch);
}
- textutil.writeTJMappedChar(ch);
+ textutil.writeTJMappedCodePoint(ch);
if (dx != null && i < dxl - 1) {
glyphAdjust += dx[i + 1];
@@ -551,9 +557,7 @@ public class PDFPainter extends AbstractIFPainter<PDFDocumentHandler> {
double xd = (xo - xoLast) / 1000f;
double yd = (yo - yoLast) / 1000f;
tu.writeTd(xd, yd);
- ch = f.mapChar(ch);
- ch = selectAndMapSingleByteFont(tf, f.getFontName(), fsPoints, tu, ch);
- tu.writeTj(ch, tf.isMultiByte(), true);
+ tu.writeTj(f.mapChar(ch), tf.isMultiByte(), true);
xc += xa + pa[2];
yc += ya + pa[3];
xoLast = xo;
@@ -584,8 +588,8 @@ public class PDFPainter extends AbstractIFPainter<PDFDocumentHandler> {
}
*/
- private char selectAndMapSingleByteFont(Typeface tf, String fontName, float fontSize, PDFTextUtil textutil,
- char ch) {
+ private int selectAndMapSingleByteFont(Typeface tf, String fontName, float fontSize, PDFTextUtil textutil,
+ int ch) {
if ((tf instanceof SingleByteFont && ((SingleByteFont)tf).hasAdditionalEncodings()) || tf.isCID()) {
int encoding = ch / 256;
if (encoding == 0) {
diff --git a/fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java b/fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java
index 2400ff7d3..34f57e42d 100644
--- a/fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java
+++ b/fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java
@@ -458,8 +458,8 @@ public class PSPainter extends AbstractIFPainter<PSDocumentHandler> {
StringBuffer sb = new StringBuffer(initialSize);
boolean isOTF = multiByte && ((MultiByteFont)tf).isOTFFile();
for (int i = start; i < end; i++) {
- char orgChar = text.charAt(i);
- char ch;
+ int orgChar = text.charAt(i);
+ int ch;
int cw;
int xGlyphAdjust = 0;
int yGlyphAdjust = 0;
@@ -473,8 +473,13 @@ public class PSPainter extends AbstractIFPainter<PSDocumentHandler> {
if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) {
xGlyphAdjust -= wordSpacing;
}
- ch = font.mapChar(orgChar);
- cw = font.getCharWidth(orgChar); // this is never used?
+
+ // surrogate pairs have to be merged in a single code point
+ if (CharUtilities.containsSurrogatePairAt(text, i)) {
+ orgChar = Character.toCodePoint((char) orgChar, text.charAt(++i));
+ }
+
+ ch = font.mapCodePoint(orgChar);
}
if (dp != null && i < dp.length && dp[i] != null) {
diff --git a/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java b/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java
index e0f5e1911..4be495209 100644
--- a/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java
+++ b/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java
@@ -19,6 +19,9 @@
package org.apache.fop.util;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
/**
* This class provides utilities to distinguish various kinds of Unicode
* whitespace and to get character widths in a given FontState.
@@ -354,4 +357,134 @@ public class CharUtilities {
}
}
+ /**
+ * Determine whether the specified character (Unicode code point) is in then Basic
+ * Multilingual Plane (BMP). Such code points can be represented using a single {@code char}.
+ *
+ * @see Character#isBmpCodePoint(int) from Java 1.7
+ * @param codePoint the character (Unicode code point) to be tested
+ * @return {@code true} if the specified code point is between Character#MIN_VALUE and
+ * Character#MAX_VALUE} inclusive; {@code false} otherwise
+ */
+ public static boolean isBmpCodePoint(int codePoint) {
+ return codePoint >>> 16 == 0;
+ }
+
+ /**
+ * Returns 1 if codePoint not in the BMP. This function is particularly useful in for
+ * loops over strings where, in presence of surrogate pairs, you need to skip one loop.
+ *
+ * @param codePoint 1 if codePoint &gt; 0xFFFF, 0 otherwise
+ * @return 1 if codePoint &gt; 0xFFFF, 0 otherwise
+ */
+ public static int incrementIfNonBMP(int codePoint) {
+ return isBmpCodePoint(codePoint) ? 0 : 1;
+ }
+
+ /**
+ * Determine if the given characters is part of a surrogate pair.
+ *
+ * @param ch character to be checked
+ * @return true if ch is an high surrogate or a low surrogate
+ */
+ public static boolean isSurrogatePair(char ch) {
+ return Character.isHighSurrogate(ch) || Character.isLowSurrogate(ch);
+ }
+
+ /**
+ * Tells whether there is a surrogate pair starting from the given index in the {@link CharSequence}. If the
+ * character at index is an high surrogate then the character at index+1 is checked to be a low surrogate. If a
+ * malformed surrogate pair is encountered then an {@link IllegalArgumentException} is thrown.
+ * <pre>
+ * high surrogate [0xD800 - 0xDC00]
+ * low surrogate [0xDC00 - 0xE000]
+ * </pre>
+ *
+ * @param chars CharSequence to check
+ * @param index index in the CharSequqnce where to start the check
+ * @throws IllegalArgumentException if there wrong usage of surrogate pairs
+ * @return true if there is a well-formed surrogate pair at index
+ */
+ public static boolean containsSurrogatePairAt(CharSequence chars, int index) {
+ char ch = chars.charAt(index);
+
+ if (Character.isHighSurrogate(ch)) {
+ if ((index + 1) > chars.length()) {
+ throw new IllegalArgumentException(
+ "ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence");
+ }
+
+ if (Character.isLowSurrogate(chars.charAt(index + 1))) {
+ return true;
+ }
+
+ throw new IllegalArgumentException(
+ "ill-formed UTF-16 sequence, contains isolated high surrogate at index " + index);
+
+ } else if (Character.isLowSurrogate(ch)) {
+ throw new IllegalArgumentException(
+ "ill-formed UTF-16 sequence, contains isolated low surrogate at index " + index);
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates an iterator to iter a {@link CharSequence} codepoints.
+ *
+ * @see #codepointsIter(CharSequence, int, int)
+ * @param s {@link CharSequence} to iter
+ * @return codepoint iterator for the given {@link CharSequence}.
+ */
+ public static Iterable<Integer> codepointsIter(final CharSequence s) {
+ return codepointsIter(s, 0, s.length());
+ }
+
+ /**
+ * Creates an iterator to iter a sub-CharSequence codepoints.
+ *
+ * @see <a href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5003547">Bug JDK-5003547</a>
+ * @param s {@link CharSequence} to iter
+ * @param beginIndex lower range
+ * @param endIndex upper range
+ * @return codepoint iterator for the given sub-CharSequence.
+ */
+ public static Iterable<Integer> codepointsIter(final CharSequence s, final int beginIndex, final int endIndex) {
+ if (beginIndex < 0) {
+ throw new StringIndexOutOfBoundsException(beginIndex);
+ }
+ if (endIndex > s.length()) {
+ throw new StringIndexOutOfBoundsException(endIndex);
+ }
+ int subLen = endIndex - beginIndex;
+ if (subLen < 0) {
+ throw new StringIndexOutOfBoundsException(subLen);
+ }
+
+ return new Iterable<Integer>() {
+ public Iterator<Integer> iterator() {
+ return new Iterator<Integer>() {
+ int nextIndex = beginIndex;
+
+ public boolean hasNext() {
+ return nextIndex < endIndex;
+ }
+
+ public Integer next() {
+ if (!hasNext()) {
+ // Findbugs wants this: IT_NO_SUCH_ELEMENT
+ throw new NoSuchElementException();
+ }
+ int result = Character.codePointAt(s, nextIndex);
+ nextIndex += Character.charCount(result);
+ return result;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
}
diff --git a/fop-core/src/main/java/org/apache/fop/util/HexEncoder.java b/fop-core/src/main/java/org/apache/fop/util/HexEncoder.java
index 9ca91f2d2..d220f0961 100644
--- a/fop-core/src/main/java/org/apache/fop/util/HexEncoder.java
+++ b/fop-core/src/main/java/org/apache/fop/util/HexEncoder.java
@@ -45,13 +45,20 @@ public final class HexEncoder {
}
/**
- * Returns an hex encoding of the given character as a four-character string.
+ * Returns an hex encoding of the given character as:
+ * <ul>
+ * <li>4-character string in case of non-BMP character</li>
+ * <li>6-character string in case of BMP character</li>
+ * </ul>
*
* @param c a character
* @return an hex-encoded representation of the character
*/
- public static String encode(char c) {
- return encode(c, 4);
+ public static String encode(int c) {
+ if (CharUtilities.isBmpCodePoint(c)) {
+ return encode(c, 4);
+ } else {
+ return encode(c, 6);
+ }
}
-
}
diff --git a/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java
index 49a5c9455..4ca3085f6 100644
--- a/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java
+++ b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
+import org.apache.commons.io.IOUtils;
/*
* !!! THIS IS A GENERATED FILE !!!
@@ -64,9 +65,7 @@ public final class BidiTestData {
} catch (ClassNotFoundException e) {
data = null;
} finally {
- if (is != null) {
- try { is.close(); } catch (Exception e) { /* NOP */ }
- }
+ IOUtils.closeQuietly(is);
}
return data;
}
diff --git a/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java b/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java
index abe4f513a..118961aa4 100644
--- a/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java
@@ -34,6 +34,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import org.apache.commons.io.IOUtils;
+
import org.apache.fop.complexscripts.fonts.GlyphPositioningTable;
import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable;
import org.apache.fop.complexscripts.fonts.ttx.TTXFile;
@@ -88,14 +90,12 @@ public class ArabicWordFormsTestCase implements ArabicWordFormsConstants {
FileInputStream fis = null;
try {
fis = new FileInputStream(dpn);
- if (fis != null) {
- ObjectInputStream ois = new ObjectInputStream(fis);
- List<Object[]> data = (List<Object[]>) ois.readObject();
- if (data != null) {
- processWordForms(data);
- }
- ois.close();
+ ObjectInputStream ois = new ObjectInputStream(fis);
+ List<Object[]> data = (List<Object[]>) ois.readObject();
+ if (data != null) {
+ processWordForms(data);
}
+ ois.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e.getMessage(), e);
} catch (IOException e) {
@@ -103,9 +103,7 @@ public class ArabicWordFormsTestCase implements ArabicWordFormsConstants {
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
- if (fis != null) {
- try { fis.close(); } catch (Exception e) { /* NOP */ }
- }
+ IOUtils.closeQuietly(fis);
}
}
diff --git a/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java b/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java
index 35bfb3128..9c2cd78e0 100644
--- a/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java
+++ b/fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java
@@ -32,6 +32,8 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
+import org.apache.commons.io.IOUtils;
+
import org.apache.fop.complexscripts.fonts.GlyphPositioningTable;
import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable;
import org.apache.fop.complexscripts.fonts.ttx.TTXFile;
@@ -102,20 +104,18 @@ public final class GenerateArabicTestData implements ArabicWordFormsConstants {
FileInputStream fis = null;
try {
fis = new FileInputStream(spn);
- if (fis != null) {
- LineNumberReader lr = new LineNumberReader(new InputStreamReader(fis, Charset.forName("UTF-8")));
- String wf;
- while ((wf = lr.readLine()) != null) {
- GlyphSequence igs = tf.mapCharsToGlyphs(wf);
- GlyphSequence ogs = gsub.substitute(igs, script, language);
- int[][] paa = new int [ ogs.getGlyphCount() ] [ 4 ];
- if (!gpos.position(ogs, script, language, 1000, widths, paa)) {
- paa = null;
- }
- data.add(new Object[] { wf, getGlyphs(igs), getGlyphs(ogs), paa });
+ LineNumberReader lr = new LineNumberReader(new InputStreamReader(fis, Charset.forName("UTF-8")));
+ String wf;
+ while ((wf = lr.readLine()) != null) {
+ GlyphSequence igs = tf.mapCharsToGlyphs(wf);
+ GlyphSequence ogs = gsub.substitute(igs, script, language);
+ int[][] paa = new int [ ogs.getGlyphCount() ] [ 4 ];
+ if (!gpos.position(ogs, script, language, 1000, widths, paa)) {
+ paa = null;
}
- lr.close();
+ data.add(new Object[] { wf, getGlyphs(igs), getGlyphs(ogs), paa });
}
+ lr.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e.getMessage(), e);
} catch (IOException e) {
@@ -123,9 +123,7 @@ public final class GenerateArabicTestData implements ArabicWordFormsConstants {
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
- if (fis != null) {
- try { fis.close(); } catch (Exception e) { /* NOP */ }
- }
+ IOUtils.closeQuietly(fis);
}
} else {
assert gsub != null;
@@ -161,11 +159,9 @@ public final class GenerateArabicTestData implements ArabicWordFormsConstants {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(dpn);
- if (fos != null) {
- ObjectOutputStream oos = new ObjectOutputStream(fos);
- oos.writeObject(data);
- oos.close();
- }
+ ObjectOutputStream oos = new ObjectOutputStream(fos);
+ oos.writeObject(data);
+ oos.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e.getMessage(), e);
} catch (IOException e) {
@@ -173,9 +169,7 @@ public final class GenerateArabicTestData implements ArabicWordFormsConstants {
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
- if (fos != null) {
- try { fos.close(); } catch (Exception e) { /* NOP */ }
- }
+ IOUtils.closeQuietly(fos);
}
}
diff --git a/fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java
index e73c29569..ed7a6b294 100644
--- a/fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java
@@ -89,6 +89,13 @@ public class CIDFullTestCase {
}
@Test
+ public void testMapCodePoint() {
+ // index 9 exists
+ char c = 'a';
+ assertEquals(cidFull.mapCodePoint(9, c), (char) 9);
+ }
+
+ @Test
public void testGetGlyphs() {
Map<Integer, Integer> fontGlyphs = cidFull.getGlyphs();
for (Map.Entry<Integer, Integer> integerIntegerEntry : fontGlyphs.entrySet()) {
diff --git a/fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java
new file mode 100644
index 000000000..b81964cf8
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.fonts;
+
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.fop.util.CharUtilities;
+
+public class CIDSubsetTestCase {
+
+ /** The surrogate pair is expected to be in the end of the string. Change it carefully. */
+ private static final String TEXT = "Hello CIDSubset \uD83D\uDCA9";
+
+ private CIDSubset cidSub;
+ private BitSet bs;
+ private int[] codepoints;
+ private int[] widths;
+ private Map<Integer, Integer> glyphToSelector;
+ private Map<Integer, Integer> charToSelector;
+ private HashMap<Integer, Integer> charToGlyph;
+
+ @Before
+ public void setup() {
+ bs = new BitSet();
+ glyphToSelector = new HashMap<Integer, Integer>();
+ charToSelector = new HashMap<Integer, Integer>();
+ charToGlyph = new HashMap<Integer, Integer>();
+
+ codepoints = new int[TEXT.length() - 1]; // skip one char because of surrogate pair
+ bs.set(0); // .notdef
+
+ int glyphIdx = 0;
+ for (int i = 0; i < TEXT.length(); i++) {
+ int cp = TEXT.codePointAt(i);
+ i += CharUtilities.incrementIfNonBMP(cp);
+
+ codepoints[glyphIdx] = cp;
+
+ glyphIdx++;
+
+ // Assign glyphIdx for each character
+ // glyphIndex 0 is reserved for .notdef
+ if (!charToGlyph.containsKey(cp)) {
+ charToGlyph.put(cp, glyphIdx);
+ bs.set(glyphIdx);
+ }
+ }
+
+ // fill widths up to max glyph index + 1 for .notdef
+ widths = new int[glyphIdx + 1];
+ for (int i = 0; i < widths.length; i++) {
+ widths[i] = 100 * i;
+ }
+
+ MultiByteFont mbFont = mock(MultiByteFont.class);
+ when(mbFont.getGlyphIndices()).thenReturn(bs);
+ when(mbFont.getWidths()).thenReturn(widths);
+ cidSub = new CIDSubset(mbFont);
+
+ for (int i = 0; i < codepoints.length; i++) {
+ int codepoint = codepoints[i];
+ int glyphIndex = charToGlyph.get(codepoint);
+ int subsetCharSelector = cidSub.mapCodePoint(glyphIndex, codepoint);
+ glyphToSelector.put(glyphIndex, subsetCharSelector);
+ charToSelector.put(codepoint, subsetCharSelector);
+ }
+ }
+
+ @Test
+ public void testGetOriginalGlyphIndex() {
+ // index 5 exists
+ int codepoint = (int) TEXT.charAt(0);
+ int subsetCharSelector = charToSelector.get(codepoint);
+ int originalIdx = charToGlyph.get(codepoint);
+ assertEquals(originalIdx, cidSub.getOriginalGlyphIndex(subsetCharSelector));
+ }
+
+ @Test
+ public void testGetUnicode() {
+ int bmpCodepoint = codepoints[5];
+ int nonBmpCodepoint = codepoints[codepoints.length - 1];
+
+ assertEquals(bmpCodepoint, cidSub.getUnicode(charToSelector.get(bmpCodepoint)));
+ assertEquals(nonBmpCodepoint, cidSub.getUnicode(charToSelector.get(nonBmpCodepoint)));
+
+ // not exist
+ assertEquals(CharUtilities.NOT_A_CHARACTER, cidSub.getUnicode(-1));
+ }
+
+ @Test
+ public void testMapChar() {
+ for (Map.Entry<Integer, Integer> entry : glyphToSelector.entrySet()) {
+ int glyphIndex = entry.getKey();
+ int subsetCharSelector = entry.getValue();
+ // the value of codepoint is not relevant for the purpose of this test: safe to take a random value.
+ int codepoint = 'a';
+ assertEquals(subsetCharSelector, cidSub.mapChar(glyphIndex, (char) codepoint));
+ }
+ }
+
+ @Test
+ public void testMapCodePoint() {
+ for (Map.Entry<Integer, Integer> entry : glyphToSelector.entrySet()) {
+ int glyphIndex = entry.getKey();
+ int subsetCharSelector = entry.getValue();
+ // the value of codepoint is not relevant for the purpose of this test: safe to take a random value.
+ int codepoint = 'a';
+ assertEquals(subsetCharSelector, cidSub.mapCodePoint(glyphIndex, codepoint));
+ }
+ }
+
+ @Test
+ public void testGetGlyphs() {
+ Map<Integer, Integer> fontGlyphs = cidSub.getGlyphs();
+
+ for (Integer key : fontGlyphs.keySet()) {
+ if (key == 0) {
+ // the entry 0 -> 0 is set in the CIDSubset constructor
+ assertEquals(0, fontGlyphs.get(key).intValue());
+ continue;
+ }
+ assertEquals(glyphToSelector.get(key), fontGlyphs.get(key));
+ }
+
+ assertEquals(glyphToSelector.size() + 1, fontGlyphs.size());
+ }
+
+ @Test
+ public void testGetChars() {
+ char[] chars = cidSub.getChars();
+ char[] expected = TEXT.toCharArray();
+
+ Arrays.sort(chars);
+ Arrays.sort(expected);
+
+ // checks if the returned arrays contains all the expected chars
+ for (char c : expected) {
+ assertTrue(Arrays.binarySearch(chars, c) >= 0);
+ }
+
+ // checks if the returned array do not contains unexpected chars
+ for (char c : chars) {
+ if (c == CharUtilities.NOT_A_CHARACTER) {
+ continue;
+ }
+ assertTrue(Arrays.binarySearch(expected, c) >= 0);
+ }
+ }
+
+ @Test
+ public void testGetNumberOfGlyphs() {
+ // +1 because of .notdef
+ assertEquals(glyphToSelector.size() + 1, cidSub.getNumberOfGlyphs());
+ }
+
+ @Test
+ public void testGetGlyphIndices() {
+ assertEquals(bs, cidSub.getGlyphIndices());
+ }
+
+ @Test
+ public void testGetWidths() {
+ Arrays.sort(widths);
+
+ for (int width : cidSub.getWidths()) {
+ assertTrue(Arrays.binarySearch(widths, width) >= 0);
+ }
+ }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java
new file mode 100644
index 000000000..0062e006d
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.fonts;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.fop.datatypes.PercentBaseContext;
+import org.apache.fop.fo.Constants;
+import org.apache.fop.fo.FOEventHandler;
+import org.apache.fop.fo.FOText;
+import org.apache.fop.fo.PropertyList;
+import org.apache.fop.fo.expr.PropertyException;
+import org.apache.fop.fo.properties.CommonFont;
+import org.apache.fop.fo.properties.EnumProperty;
+import org.apache.fop.fo.properties.FixedLength;
+import org.apache.fop.fo.properties.FontFamilyProperty;
+import org.apache.fop.fo.properties.NumberProperty;
+import org.apache.fop.fo.properties.Property;
+
+
+public class FontSelectorTestCase {
+
+ private static final FontTriplet LATIN_FONT_TRIPLET = new FontTriplet("Verdana", "normal", 400);
+ private static final FontTriplet EMOJI_FONT_TRIPLET = new FontTriplet("Emoji", "normal", 400);
+
+ private FOText foText;
+ private PercentBaseContext context;
+ private Font latinFont;
+ private Font emojiFont;
+
+ @Before
+ public void setUp() throws Exception {
+ FontTriplet[] fontState = new FontTriplet[] { LATIN_FONT_TRIPLET, EMOJI_FONT_TRIPLET };
+
+ foText = mock(FOText.class);
+ context = mock(PercentBaseContext.class);
+ FOEventHandler eventHandler = mock(FOEventHandler.class);
+ FontInfo fontInfo = mock(FontInfo.class);
+ CommonFont commonFont = makeCommonFont();
+ latinFont = mock(Font.class, "Latin Font");
+ emojiFont = mock(Font.class, "Emoji Font");
+
+ when(eventHandler.getFontInfo()).thenReturn(fontInfo);
+ when(foText.getFOEventHandler()).thenReturn(eventHandler);
+ when(foText.getCommonFont()).thenReturn(commonFont);
+ when(commonFont.getFontState(fontInfo)).thenReturn(fontState);
+ when(fontInfo.getFontInstance(eq(LATIN_FONT_TRIPLET), anyInt())).thenReturn(latinFont);
+ when(fontInfo.getFontInstance(eq(EMOJI_FONT_TRIPLET), anyInt())).thenReturn(emojiFont);
+ when(latinFont.hasCodePoint(anyInt())).thenAnswer(new LatinFontAnswer());
+ when(emojiFont.hasCodePoint(anyInt())).thenAnswer(new EmojiFontAnswer());
+ }
+
+ @Test
+ public void selectFontForCharactersInText() throws Exception {
+ String latinText = "Hello FontSelector";
+ String emojiText = "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A";
+ String mixedText = latinText + emojiText;
+
+
+ Font f = FontSelector.selectFontForCharactersInText(latinText, 0, latinText.length(), foText, context);
+ assertEquals(latinFont, f);
+
+ f = FontSelector.selectFontForCharactersInText(emojiText, 0, emojiText.length(), foText, context);
+ assertEquals(emojiFont, f);
+
+ // When the text is mixed the font that can cover most chars should be returned
+ f = FontSelector.selectFontForCharactersInText(mixedText, 0, mixedText.length(), foText, context);
+ assertEquals(latinFont, f);
+
+ f = FontSelector.selectFontForCharactersInText(mixedText, latinText.length() - 1, mixedText.length(), foText,
+ context);
+ assertEquals(emojiFont, f);
+ }
+
+ private static class LatinFontAnswer implements Answer<Boolean> {
+
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ int codepoint = (Integer) invocation.getArguments()[0];
+ return codepoint <= 0xFFFF;
+ }
+ }
+
+ private static class EmojiFontAnswer implements Answer<Boolean> {
+
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ int codepoint = (Integer) invocation.getArguments()[0];
+ return codepoint > 0xFFFF;
+ }
+ }
+
+ private CommonFont makeCommonFont() throws PropertyException {
+ PropertyList pList = mock(PropertyList.class);
+
+ String fontFamilyVal = LATIN_FONT_TRIPLET.getName() + "," + EMOJI_FONT_TRIPLET.getName();
+ Property fontFamilyProp = new FontFamilyProperty.Maker(Constants.PR_FONT_FAMILY).make(pList, fontFamilyVal,
+ null);
+ Property fontWeightProp = EnumProperty.getInstance(Constants.PR_FONT_WEIGHT, "400");
+ Property fontStyle = EnumProperty.getInstance(Constants.PR_FONT_STYLE, "normal");
+ Property fontSizeAdjustProp = NumberProperty.getInstance(1);
+ Property fontSizeProp = FixedLength.getInstance(12);
+
+ when(pList.get(Constants.PR_FONT_FAMILY)).thenReturn(fontFamilyProp);
+ when(pList.get(Constants.PR_FONT_WEIGHT)).thenReturn(fontWeightProp);
+ when(pList.get(Constants.PR_FONT_STYLE)).thenReturn(fontStyle);
+ when(pList.get(Constants.PR_FONT_SIZE_ADJUST)).thenReturn(fontSizeAdjustProp);
+ when(pList.get(Constants.PR_FONT_SIZE)).thenReturn(fontSizeProp);
+
+ return CommonFont.getInstance(pList);
+ }
+
+}
diff --git a/fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java
index a66c6d49e..325c32696 100644
--- a/fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java
@@ -22,6 +22,7 @@ package org.apache.fop.fonts.truetype;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.List;
import java.util.Map;
import org.junit.Test;
@@ -30,6 +31,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import org.apache.fop.fonts.CMapSegment;
import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion;
/**
@@ -45,6 +47,11 @@ public class TTFFileTestCase {
protected final TTFFile droidmonoTTFFile;
/** The FontFileReader for ttfFile (DroidSansMono) */
protected final FontFileReader droidmonoReader;
+ /** The truetype font file (AndroidEmoji) fonr non-BMP codepoints */
+ protected final TTFFile androidEmojiTTFFile;
+ /** The FontFileReader for ttfFile (AndroidEmoji) */
+ protected final FontFileReader androidEmojiReader;
+
/**
@@ -52,20 +59,27 @@ public class TTFFileTestCase {
* @throws IOException exception
*/
public TTFFileTestCase() throws IOException {
- dejavuTTFFile = new TTFFile();
InputStream dejaStream = new FileInputStream("test/resources/fonts/ttf/DejaVuLGCSerif.ttf");
+ dejavuTTFFile = new TTFFile();
dejavuReader = new FontFileReader(dejaStream);
String dejavuHeader = OFFontLoader.readHeader(dejavuReader);
dejavuTTFFile.readFont(dejavuReader, dejavuHeader);
dejaStream.close();
InputStream droidStream = new FileInputStream("test/resources/fonts/ttf/DroidSansMono.ttf");
-
droidmonoTTFFile = new TTFFile();
droidmonoReader = new FontFileReader(droidStream);
String droidmonoHeader = OFFontLoader.readHeader(droidmonoReader);
droidmonoTTFFile.readFont(droidmonoReader, droidmonoHeader);
droidStream.close();
+
+ InputStream emojiStream = new FileInputStream("test/resources/fonts/ttf/AndroidEmoji.ttf");
+ androidEmojiTTFFile = new TTFFile();
+ androidEmojiReader = new FontFileReader(emojiStream);
+ String androidEmojiHeader = OFFontLoader.readHeader(androidEmojiReader);
+ androidEmojiTTFFile.readFont(androidEmojiReader, androidEmojiHeader);
+ emojiStream.close();
+
}
/**
@@ -110,12 +124,11 @@ public class TTFFileTestCase {
*/
@Test
public void testGetAnsiKerning() {
- Map<Integer, Map<Integer, Integer>> ansiKerning = dejavuTTFFile.getKerning();
+ Map<Integer, Map<Integer, Integer>> ansiKerning = dejavuTTFFile.getAnsiKerning();
if (ansiKerning.isEmpty()) {
fail();
}
- Integer k1 = ansiKerning.get((int) 'A').get(
- (int) 'T');
+ Integer k1 = ansiKerning.get((int) 'A').get((int) 'T');
assertEquals(dejavuTTFFile.convertTTFUnit2PDFUnit(-112), k1.intValue());
Integer k2 = ansiKerning.get((int) 'Y').get((int) 'u');
assertEquals(dejavuTTFFile.convertTTFUnit2PDFUnit(-178), k2.intValue());
@@ -125,6 +138,12 @@ public class TTFFileTestCase {
if (!ansiKerning.isEmpty()) {
fail("DroidSansMono shouldn't have any kerning data.");
}
+
+ // AndroidEmoji doens't have kerning
+ ansiKerning = androidEmojiTTFFile.getAnsiKerning();
+ if (!ansiKerning.isEmpty()) {
+ fail("AndroidEmoji shouldn't have any kerning data.");
+ }
}
/**
@@ -145,6 +164,10 @@ public class TTFFileTestCase {
// height of "H" = 1462
assertEquals(droidmonoTTFFile.convertTTFUnit2PDFUnit(1462),
droidmonoTTFFile.getCapHeight());
+ // AndroidEmoji doesn't have a PCLT table either
+ // height of "H" = 1462
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(1462),
+ androidEmojiTTFFile.getCapHeight());
}
/**
@@ -154,6 +177,8 @@ public class TTFFileTestCase {
public void testGetCharSetName() {
assertTrue("WinAnsiEncoding".equals(dejavuTTFFile.getCharSetName()));
assertTrue("WinAnsiEncoding".equals(droidmonoTTFFile.getCharSetName()));
+ //Even though this pass I'm not sure whether is what we want
+ assertTrue("WinAnsiEncoding".equals(androidEmojiTTFFile.getCharSetName()));
}
/**
@@ -175,12 +200,24 @@ public class TTFFileTestCase {
for (int i = 0; i < 255; i++) {
assertEquals(charWidth, droidmonoTTFFile.getCharWidth(i));
}
+
+ // All the glyphs should be the same width in EmojiAndroid (mono-spaced)
+ charWidth = androidEmojiTTFFile.convertTTFUnit2PDFUnit(2600);
+ for (int i = 0; i < 255; i++) {
+ assertEquals(charWidth, androidEmojiTTFFile.getCharWidth(i));
+ }
}
/**
* TODO: add implementation to this test
*/
+ @Test
public void testGetCMaps() {
+ List<CMapSegment> cmaps = androidEmojiTTFFile.getCMaps();
+
+ for (CMapSegment seg : cmaps) {
+ System.out.println(seg.getUnicodeStart() + "-" + seg.getUnicodeEnd() + " -> " + seg.getGlyphStartIndex());
+ }
}
/**
@@ -196,6 +233,10 @@ public class TTFFileTestCase {
for (String name : droidmonoTTFFile.getFamilyNames()) {
assertEquals("Droid Sans Mono", name);
}
+ assertEquals(1, androidEmojiTTFFile.getFamilyNames().size());
+ for (String name : androidEmojiTTFFile.getFamilyNames()) {
+ assertEquals("Android Emoji", name);
+ }
}
/**
@@ -206,6 +247,7 @@ public class TTFFileTestCase {
// Not really sure how to test this intelligently
assertEquals(0, dejavuTTFFile.getFirstChar());
assertEquals(0, droidmonoTTFFile.getFirstChar());
+ assertEquals(0, androidEmojiTTFFile.getFirstChar());
}
/**
@@ -234,6 +276,17 @@ public class TTFFileTestCase {
assertEquals(32, flags & 32);
assertEquals(2, flags & 2);
assertEquals(1, flags & 1);
+ /*
+ * Android Emoji flags are:
+ * italic angle = 0
+ * fixed pitch = 0
+ * has serifs = true (default value; this font doesn't have a PCLT table)
+ */
+ flags = androidEmojiTTFFile.getFlags();
+ assertEquals(0, flags & 64);
+ assertEquals(32, flags & 32);
+ assertEquals(0, flags & 2);
+ assertEquals(1, flags & 1);
}
/**
@@ -259,6 +312,16 @@ public class TTFFileTestCase {
assertEquals(droidmonoTTFFile.convertTTFUnit2PDFUnit(-555), bBox[1]);
assertEquals(droidmonoTTFFile.convertTTFUnit2PDFUnit(1315), bBox[2]);
assertEquals(droidmonoTTFFile.convertTTFUnit2PDFUnit(2163), bBox[3]);
+
+ /*
+ * The head table has the following values (DroidSansMono):
+ * xMin = -50, yMin = -733, xMax = 2550, yMax = 2181
+ */
+ bBox = androidEmojiTTFFile.getFontBBox();
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(-50), bBox[0]);
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(-733), bBox[1]);
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(2550), bBox[2]);
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(2181), bBox[3]);
}
/**
@@ -268,6 +331,7 @@ public class TTFFileTestCase {
public void testGetFullName() {
assertEquals("DejaVu LGC Serif", dejavuTTFFile.getFullName());
assertEquals("Droid Sans Mono", droidmonoTTFFile.getFullName());
+ assertEquals("Android Emoji", androidEmojiTTFFile.getFullName());
}
/**
@@ -277,6 +341,7 @@ public class TTFFileTestCase {
public void testGetGlyphName() {
assertEquals("H", dejavuTTFFile.getGlyphName(43));
assertEquals("H", droidmonoTTFFile.getGlyphName(43));
+ assertEquals("smileface", androidEmojiTTFFile.getGlyphName(64));
}
/**
@@ -286,6 +351,7 @@ public class TTFFileTestCase {
public void testGetItalicAngle() {
assertEquals("0", dejavuTTFFile.getItalicAngle());
assertEquals("0", droidmonoTTFFile.getItalicAngle());
+ assertEquals("0", androidEmojiTTFFile.getItalicAngle());
}
/**
@@ -307,6 +373,12 @@ public class TTFFileTestCase {
if (!kerning.isEmpty()) {
fail("DroidSansMono shouldn't have any kerning data");
}
+
+ // AndroidEmoji doens't have kerning
+ kerning = androidEmojiTTFFile.getKerning();
+ if (!kerning.isEmpty()) {
+ fail("AndroidEmoji shouldn't have any kerning data.");
+ }
}
/**
@@ -316,6 +388,7 @@ public class TTFFileTestCase {
public void testLastChar() {
assertEquals(0xff, dejavuTTFFile.getLastChar());
assertEquals(0xff, droidmonoTTFFile.getLastChar());
+ assertEquals(0xae, androidEmojiTTFFile.getLastChar()); // Last ASCII mapped char is REGISTERED SIGN
}
/**
@@ -331,6 +404,11 @@ public class TTFFileTestCase {
// Curiously the same value
assertEquals(droidmonoTTFFile.convertTTFUnit2PDFUnit(1556),
droidmonoTTFFile.getLowerCaseAscent());
+ //TODO: Nedd to be fixed?
+ // 0 because the font miss letter glyph that are used in this method to guess the ascender:
+ // OpenFont.guessVerticalMetricsFromGlyphBBox
+ assertEquals(androidEmojiTTFFile.convertTTFUnit2PDFUnit(0),
+ androidEmojiTTFFile.getLowerCaseAscent());
}
/**
@@ -340,6 +418,7 @@ public class TTFFileTestCase {
public void testGetPostScriptName() {
assertEquals(PostScriptVersion.V2, dejavuTTFFile.getPostScriptVersion());
assertEquals(PostScriptVersion.V2, droidmonoTTFFile.getPostScriptVersion());
+ assertEquals(PostScriptVersion.V2, androidEmojiTTFFile.getPostScriptVersion());
}
/**
@@ -350,6 +429,7 @@ public class TTFFileTestCase {
// Undefined
assertEquals("0", dejavuTTFFile.getStemV());
assertEquals("0", droidmonoTTFFile.getStemV());
+ assertEquals("0", androidEmojiTTFFile.getStemV());
}
/**
@@ -359,6 +439,7 @@ public class TTFFileTestCase {
public void testGetSubFamilyName() {
assertEquals("Book", dejavuTTFFile.getSubFamilyName());
assertEquals("Regular", droidmonoTTFFile.getSubFamilyName());
+ assertEquals("Regular", androidEmojiTTFFile.getSubFamilyName());
}
/**
@@ -376,6 +457,7 @@ public class TTFFileTestCase {
// Retrieved from OS/2 table
assertEquals(400, dejavuTTFFile.getWeightClass());
assertEquals(400, droidmonoTTFFile.getWeightClass());
+ assertEquals(400, androidEmojiTTFFile.getWeightClass());
}
/**
@@ -388,14 +470,38 @@ public class TTFFileTestCase {
assertEquals(dejavuTTFFile.convertTTFUnit2PDFUnit(1479), widths[36]);
// using the width of '|' index = 95
assertEquals(dejavuTTFFile.convertTTFUnit2PDFUnit(690), widths[95]);
- widths = droidmonoTTFFile.getWidths();
+
// DroidSansMono should have all widths the same size (mono-spaced)
- int width = droidmonoTTFFile.convertTTFUnit2PDFUnit(1229);
- for (int i = 0; i < 255; i++) {
- assertEquals(width, widths[i]);
+ widths = droidmonoTTFFile.getWidths();
+ int charWidth = droidmonoTTFFile.convertTTFUnit2PDFUnit(1229);
+ for (OpenFont.UnicodeMapping unicodeMapping : droidmonoTTFFile.unicodeMappings) {
+ assertEquals(charWidth, widths[unicodeMapping.getGlyphIndex()]);
+ }
+
+ // All the glyphs should be the same width in EmojiAndroid (mono-spaced)
+ charWidth = androidEmojiTTFFile.convertTTFUnit2PDFUnit(2600);
+ widths = androidEmojiTTFFile.getWidths();
+ for (OpenFont.UnicodeMapping unicodeMapping : androidEmojiTTFFile.unicodeMappings) {
+ assertEquals(charWidth, widths[unicodeMapping.getGlyphIndex()]);
}
}
+ @Test
+ public void textUnicodeCoverage() {
+ int nonBMPcount = 0;
+ for (OpenFont.UnicodeMapping unicodeMapping : droidmonoTTFFile.unicodeMappings) {
+ nonBMPcount += unicodeMapping.getUnicodeIndex() > 0xFFFF ? 1 : 0;
+ }
+ assertEquals("The font DroidSansMono is supposed to have only BMP codepoints", 0, nonBMPcount);
+
+ nonBMPcount = 0;
+ for (OpenFont.UnicodeMapping unicodeMapping : androidEmojiTTFFile.unicodeMappings) {
+ nonBMPcount += unicodeMapping.getUnicodeIndex() > 0xFFFF ? 1 : 0;
+ }
+
+ assertTrue("The font AndroidEmoji is supposed to have non-BMP codepoints", 0 != nonBMPcount);
+ }
+
/**
* Test getXHeight() - There are several paths to test:
* 1) The PCLT table (if available)
@@ -418,6 +524,7 @@ public class TTFFileTestCase {
// Neither DejaVu nor DroidSansMono are a compact format font
assertEquals(false, dejavuTTFFile.isCFF());
assertEquals(false, droidmonoTTFFile.isCFF());
+ assertEquals(false, androidEmojiTTFFile.isCFF());
}
/**
@@ -428,6 +535,7 @@ public class TTFFileTestCase {
// Dejavu and DroidSansMono are both embeddable
assertEquals(true, dejavuTTFFile.isEmbeddable());
assertEquals(true, droidmonoTTFFile.isEmbeddable());
+ assertEquals(true, androidEmojiTTFFile.isEmbeddable());
}
/** Underline position and thickness. */
@@ -437,6 +545,8 @@ public class TTFFileTestCase {
assertEquals(43, dejavuTTFFile.getUnderlineThickness());
assertEquals(-75, droidmonoTTFFile.getUnderlinePosition());
assertEquals(49, droidmonoTTFFile.getUnderlineThickness());
+ assertEquals(-75, androidEmojiTTFFile.getUnderlinePosition());
+ assertEquals(49, androidEmojiTTFFile.getUnderlineThickness());
}
/** Strikeout position and thickness. */
@@ -446,6 +556,8 @@ public class TTFFileTestCase {
assertEquals(49, dejavuTTFFile.getStrikeoutThickness());
assertEquals(243, droidmonoTTFFile.getStrikeoutPosition());
assertEquals(49, droidmonoTTFFile.getStrikeoutThickness());
+ assertEquals(122, androidEmojiTTFFile.getStrikeoutPosition());
+ assertEquals(24, androidEmojiTTFFile.getStrikeoutThickness());
}
/**
diff --git a/fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java b/fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java
new file mode 100644
index 000000000..eac017436
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.render.java2d;
+
+import java.awt.Graphics2D;
+import java.awt.font.FontRenderContext;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.fop.fonts.Font;
+import org.apache.fop.fonts.FontInfo;
+import org.apache.fop.fonts.MultiByteFont;
+import org.apache.fop.fonts.SingleByteFont;
+import org.apache.fop.fonts.Typeface;
+import org.apache.fop.util.CharUtilities;
+
+
+public class Java2DUtilTestCase {
+
+ private static final String MULTI_BYTE_FONT_NAME = "multi";
+ private static final String SINGLE_BYTE_FONT_NAME = "single";
+
+ private static final String TEXT = "Hello World!\uD83D\uDCA9";
+ private static final String EXPECTED_TEXT_SINGLE = "Hello World!#";
+ private static final String EXPECTED_TEXT_MULTI = "Hello World!\uD83D\uDCA9";
+
+ @Test
+ public void createGlyphVectorMultiByte() throws Exception {
+ Graphics2D g2d = mock(Graphics2D.class);
+ java.awt.Font awtFont = mock(java.awt.Font.class);
+ Font font = makeFont(MULTI_BYTE_FONT_NAME);
+ FontInfo fontInfo = makeFontInfo();
+
+ int[] codepoints = new int[EXPECTED_TEXT_MULTI.codePointCount(0, EXPECTED_TEXT_MULTI.length())];
+
+ int i = 0;
+ for (int cp : CharUtilities.codepointsIter(EXPECTED_TEXT_MULTI)) {
+ codepoints[i++] = cp;
+ }
+
+ when(g2d.getFont()).thenReturn(awtFont);
+
+ Java2DUtil.createGlyphVector(TEXT, g2d, font, fontInfo);
+ verify(awtFont).createGlyphVector(any(FontRenderContext.class), eq(codepoints));
+ }
+
+ @Test
+ public void createGlyphVectorSingleByte() throws Exception {
+ Graphics2D g2d = mock(Graphics2D.class);
+ java.awt.Font awtFont = mock(java.awt.Font.class);
+ Font font = makeFont(SINGLE_BYTE_FONT_NAME);
+ FontInfo fontInfo = makeFontInfo();
+
+ when(g2d.getFont()).thenReturn(awtFont);
+
+ Java2DUtil.createGlyphVector(TEXT, g2d, font, fontInfo);
+ verify(awtFont).createGlyphVector(any(FontRenderContext.class), eq(EXPECTED_TEXT_SINGLE));
+ }
+
+
+ private FontInfo makeFontInfo() {
+ Map<String, Typeface> fonts = new HashMap<String, Typeface>();
+
+ SingleByteFont singleByteFont = mock(SingleByteFont.class);
+ MultiByteFont multiByteFont = mock(MultiByteFont.class);
+ FontInfo fontInfo = mock(FontInfo.class);
+
+ fonts.put(MULTI_BYTE_FONT_NAME, multiByteFont);
+ fonts.put(SINGLE_BYTE_FONT_NAME, singleByteFont);
+
+ when(multiByteFont.findGlyphIndex(anyInt())).thenAnswer(new FindGlyphIndexAnswer());
+ when(fontInfo.getFonts()).thenReturn(fonts);
+
+ return fontInfo;
+ }
+
+ private Font makeFont(String fontName) {
+ Font font = mock(Font.class);
+ when(font.getFontName()).thenReturn(fontName);
+ return font;
+ }
+
+
+ private static class FindGlyphIndexAnswer implements Answer<Integer> {
+
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return (Integer) invocation.getArguments()[0];
+ }
+ }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java
index fc6212dcd..544dd25a8 100644
--- a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java
@@ -19,19 +19,23 @@
package org.apache.fop.render.pdf;
+
+
import java.io.File;
import java.io.IOException;
-import java.util.StringTokenizer;
-import org.junit.Ignore;
import org.junit.Test;
import org.xml.sax.SAXException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+
import org.apache.fop.apps.FOUserAgent;
+
/** Test that characters are correctly encoded in a generated PDF file */
public class PDFEncodingTestCase extends BasePDFTest {
private File foBaseDir = new File("test/xml/pdf-encoding");
@@ -67,24 +71,21 @@ public class PDFEncodingTestCase extends BasePDFTest {
*/
final String[] testPatterns = {
TEST_MARKER + "1", "Standard",
- TEST_MARKER + "2", "XX_\\351_XX",
- TEST_MARKER + "3", "XX_\\342\\352\\356\\364\\373_XX"
+ TEST_MARKER + "2", "XX_é_XX",
+ TEST_MARKER + "3", "XX_âêîôû_XX"
};
runTest("test-standard-font.fo", testPatterns);
}
/**
- * TODO test disabled for now, fails due (probably) do different PDF
- * encoding when custom font is used.
- * TODO This should be tested using PDFBox. If PDFBox can extract the text correctly,
- * everything is fine. The tests here are too unstable.
+ * Test encoding with a Custom Font using BMP characters.
+ *
+ * NB: The Gladiator font do not contain '_' Glyph
*
* @throws Exception
* checkstyle wants a comment here, even a silly one
*/
- @Ignore("This should be tested using PDFBox. If PDFBox can extract the text correctly,"
- + "everything is fine. The tests here are too unstable.")
@Test
public void testPDFEncodingWithCustomFont() throws Exception {
@@ -94,14 +95,31 @@ public class PDFEncodingTestCase extends BasePDFTest {
* The following array is used to look for these patterns
*/
final String[] testPatterns = {
- TEST_MARKER + "1", "(Gladiator)",
- TEST_MARKER + "2", "XX_\\351_XX",
- TEST_MARKER + "3", "XX_\\342\\352\\356\\364\\373_XX"
+ TEST_MARKER + "1", "Gladiator",
+ TEST_MARKER + "2", "XX_é_XX",
+ TEST_MARKER + "3", "XX_âêîôû_XX"
};
runTest("test-custom-font.fo", testPatterns);
}
+ /**
+ * Test encoding with a Custom Font using non-BMP characters
+ *
+ * @throws Exception
+ * checkstyle wants a comment here, even a silly one
+ */
+ @Test
+ public void testPDFEncodingWithNonBMPFont() throws Exception {
+
+ final String[] testPatterns = {
+ TEST_MARKER + "1", "AndroidEmoji",
+ TEST_MARKER + "2", "\uD800\uDF00",
+ };
+
+ runTest("test-custom-non-bmp-font.fo", testPatterns);
+ }
+
/** Test encoding using specified input file and test patterns array */
private void runTest(String inputFile, String[] testPatterns)
throws Exception {
@@ -119,26 +137,26 @@ public class PDFEncodingTestCase extends BasePDFTest {
private void checkEncoding(byte[] pdf, String[] testPattern)
throws IOException {
+ String s = extractTextFromPDF(pdf);
+
int markersFound = 0;
- final String input = new String(pdf);
- int pos = 0;
- if ((pos = input.indexOf(TEST_MARKER)) >= 0) {
- final StringTokenizer tk = new StringTokenizer(
- input.substring(pos), "\n");
-
- while (tk.hasMoreTokens()) {
- final String line = tk.nextToken();
- if (line.indexOf(TEST_MARKER) >= 0) {
- markersFound++;
- for (int i = 0; i < testPattern.length; i += 2) {
- if (line.indexOf(testPattern[i]) >= 0) {
- final String ref = testPattern[i + 1];
- final boolean patternFound = line.indexOf(ref) >= 0;
- assertTrue("line containing '" + testPattern[i]
- + "' must contain '" + ref, patternFound);
- }
- }
+ for (String line : s.split("\n")) {
+ if (!line.contains(TEST_MARKER)) {
+ continue;
+ }
+
+ markersFound++;
+
+ for (int i = 0; i < testPattern.length; i++) {
+ String marker = testPattern[i];
+ String pattern = testPattern[++i];
+
+ if (!line.contains(marker)) {
+ continue;
}
+
+ String msg = String.format("line containing '%s' must contain '%s'", marker, pattern);
+ assertTrue(msg, line.contains(pattern));
}
}
@@ -146,4 +164,10 @@ public class PDFEncodingTestCase extends BasePDFTest {
assertEquals(nMarkers + " " + TEST_MARKER + " markers must be found",
nMarkers, markersFound);
}
+
+ private static String extractTextFromPDF(byte[] pdfContent) throws IOException {
+ PDFTextStripper pdfStripper = new PDFTextStripper();
+ PDDocument pdDoc = PDDocument.load(pdfContent);
+ return pdfStripper.getText(pdDoc);
+ }
}
diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java
index b5c7263be..04509398b 100644
--- a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java
@@ -28,9 +28,14 @@ import java.io.File;
import javax.xml.transform.stream.StreamResult;
import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.endsWith;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -50,8 +55,6 @@ import org.apache.fop.render.intermediate.IFContext;
import org.apache.fop.render.intermediate.IFException;
import org.apache.fop.traits.BorderProps;
-import junit.framework.Assert;
-
public class PDFPainterTestCase {
private FOUserAgent foUserAgent;
@@ -122,7 +125,7 @@ public class PDFPainterTestCase {
pdfDocumentHandler.getContext().setPageNumber(3);
MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null);
pdfPainter.drawImage("test/resources/images/cmyk.jpg", new Rectangle());
- Assert.assertEquals(pdfPainter.renderingContext.getHints().get("page-number"), 3);
+ assertEquals(pdfPainter.renderingContext.getHints().get("page-number"), 3);
}
class MyPDFPainter extends PDFPainter {
@@ -139,10 +142,52 @@ public class PDFPainterTestCase {
@Test
public void testSimulateStyle() throws IFException {
+ final StringBuilder sb = new StringBuilder();
+ pdfDocumentHandler = makePDFDocumentHandler(sb);
+
+ FontInfo fi = new FontInfo();
+ fi.addFontProperties("f1", new FontTriplet("a", "italic", 700));
+ MultiByteFont font = new MultiByteFont(null, null);
+ font.setSimulateStyle(true);
+ fi.addMetrics("f1", font);
+ pdfDocumentHandler.setFontInfo(fi);
+ MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null);
+ pdfPainter.setFont("a", "italic", 700, null, 12, null);
+ pdfPainter.drawText(0, 0, 0, 0, null, "test");
+
+ assertEquals(sb.toString(), "BT\n/f1 0.012 Tf\n1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n");
+ verify(pdfContentGenerator).add("q\n");
+ verify(pdfContentGenerator).add("2 Tr 0.31543 w\n");
+ verify(pdfContentGenerator).add("Q\n");
+ }
+
+ @Test
+ public void testDrawTextWithMultiByteFont() throws IFException {
+ StringBuilder output = new StringBuilder();
+ PDFDocumentHandler pdfDocumentHandler = makePDFDocumentHandler(output);
+ //0x48 0x65 0x6C 0x6C 0x6F 0x20 0x4D 0x6F 0x63 0x6B 0x21 0x1F4A9
+ String text = "Hello Mock!\uD83D\uDCA9";
+ String expectedHex = "00480065006C006C006F0020004D006F0063006B002101F4A9";
+
+ MultiByteFont font = spy(new MultiByteFont(null, null));
+ when(font.mapCodePoint(anyInt())).thenAnswer(new FontMapCodepointAnswer());
+
+ FontInfo fi = new FontInfo();
+ fi.addFontProperties("f1", new FontTriplet("a", "normal", 400));
+ fi.addMetrics("f1", font);
+ pdfDocumentHandler.setFontInfo(fi);
+
+ MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null);
+ pdfPainter.setFont("a", "normal", 400, null, 12, null);
+ pdfPainter.drawText(0, 0, 0, 0, null, text);
+
+ assertEquals("BT\n/f1 0.012 Tf\n1 0 0 -1 0 0 Tm [<" + expectedHex + ">] TJ\n", output.toString());
+ }
+
+ private PDFDocumentHandler makePDFDocumentHandler(final StringBuilder sb) throws IFException {
FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
foUserAgent = fopFactory.newFOUserAgent();
mockPDFContentGenerator();
- final StringBuilder sb = new StringBuilder();
PDFTextUtil pdfTextUtil = new PDFTextUtil() {
protected void write(String code) {
sb.append(code);
@@ -163,19 +208,14 @@ public class PDFPainterTestCase {
pdfDocumentHandler.setResult(new StreamResult(new ByteArrayOutputStream()));
pdfDocumentHandler.startDocument();
pdfDocumentHandler.startPage(0, "", "", new Dimension());
- FontInfo fi = new FontInfo();
- fi.addFontProperties("f1", new FontTriplet("a", "italic", 700));
- MultiByteFont font = new MultiByteFont(null, null);
- font.setSimulateStyle(true);
- fi.addMetrics("f1", font);
- pdfDocumentHandler.setFontInfo(fi);
- MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null);
- pdfPainter.setFont("a", "italic", 700, null, 12, null);
- pdfPainter.drawText(0, 0, 0, 0, null, "test");
+ return pdfDocumentHandler;
+ }
- Assert.assertEquals(sb.toString(), "BT\n/f1 0.012 Tf\n1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n");
- verify(pdfContentGenerator).add("q\n");
- verify(pdfContentGenerator).add("2 Tr 0.31543 w\n");
- verify(pdfContentGenerator).add("Q\n");
+ private static class FontMapCodepointAnswer implements Answer<Integer> {
+
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return (Integer) invocation.getArguments()[0];
+ }
}
}
diff --git a/fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java b/fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java
index c2bc2017f..7cc0fd7af 100644
--- a/fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java
@@ -58,6 +58,7 @@ import org.apache.fop.render.intermediate.IFContext;
import org.apache.fop.render.intermediate.IFException;
import org.apache.fop.render.intermediate.IFState;
import org.apache.fop.traits.BorderProps;
+import org.apache.fop.util.CharUtilities;
public class PSPainterTestCase {
@@ -126,7 +127,7 @@ public class PSPainterTestCase {
}
@Test
- public void testDrawText() {
+ public void testDrawText() throws IOException {
int fontSize = 12000;
String fontName = "MockFont";
PSGenerator psGenerator = mock(PSGenerator.class);
@@ -160,12 +161,19 @@ public class PSPainterTestCase {
double yAsDouble = (y - dp[0][1]) / 1000.0;
when(psGenerator.formatDouble(xAsDouble)).thenReturn("100.100");
when(psGenerator.formatDouble(yAsDouble)).thenReturn("99.900");
- String text = "Hello Mock!";
+
+ //0x48 0x65 0x6C 0x6C 0x6F 0x20 0x4D 0x6F 0x63 0x6B 0x21 0x1F4A9
+ String text = "Hello Mock!\uD83D\uDCA9";
+
+ for (int cp : CharUtilities.codepointsIter(text)) {
+ when(font.mapCodePoint(cp)).thenReturn(cp);
+ }
+
try {
psPainter.drawText(x, y, letterSpacing, wordSpacing, dp, text);
verify(psGenerator).writeln("1 0 0 -1 100.100 99.900 Tm");
- verify(psGenerator).writeln("[<0000> [-100 100] <00000000> [200 -200] <0000> [-300 300] "
- + "<0000000000000000000000000000>] TJ");
+ verify(psGenerator).writeln("[<0048> [-100 100] <0065006C> [200 -200] <006C> [-300 300] "
+ + "<006F0020004D006F0063006B002101F4A9>] TJ");
} catch (Exception e) {
fail("something broke...");
}
diff --git a/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java b/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java
new file mode 100644
index 000000000..6a97c4442
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+
+public class CharUtilitiesTestCase {
+
+ @Test
+ public void testIsBmpCodePoint() {
+ for (int i = 0; i < 0x10FFFF; i++) {
+ assertEquals(i <= 0xFFFF, CharUtilities.isBmpCodePoint(i));
+ }
+ }
+
+ @Test
+ public void testIncrementIfNonBMP() {
+ for (int i = 0; i < 0x10FFFF; i++) {
+ if (i <= 0xFFFF) {
+ assertEquals(0, CharUtilities.incrementIfNonBMP(i));
+ } else {
+ assertEquals(1, CharUtilities.incrementIfNonBMP(i));
+ }
+ }
+ }
+
+ @Test
+ public void testIsSurrogatePair() {
+ for (char i = 0; i < 0xFFFF; i++) {
+ if (i < 0xD800 || i > 0xDFFF) {
+ assertFalse(CharUtilities.isSurrogatePair(i));
+ } else {
+ assertTrue(CharUtilities.isSurrogatePair(i));
+ }
+ }
+ }
+
+ @Test
+ public void testContainsSurrogatePairAt() {
+ String withSurrogatePair = "012\uD83D\uDCA94";
+
+ assertTrue(CharUtilities.containsSurrogatePairAt(withSurrogatePair, 3));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testContainsSurrogatePairAtWithMalformedUTF8Sequence() {
+ String malformedUTF8Sequence = "012\uD83D4";
+
+ CharUtilities.containsSurrogatePairAt(malformedUTF8Sequence, 3);
+ }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java b/fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java
index cb366abdf..b989ebea5 100644
--- a/fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java
@@ -40,8 +40,21 @@ public class HexEncoderTestCase {
}
}
+
+ /**
+ * Tests that codepoints are properly encoded into hex strings.
+ */
+ @Test
+ public void testEncodeCodepoints() {
+ char[] digits = new char[] {'0', '1', '0', '0', '0', '0'};
+ for (int c = 0x10000; c <= 0x1FFFF; c++) {
+ assertEquals(new String(digits), HexEncoder.encode(c));
+ increment(digits);
+ }
+ }
+
private static void increment(char[] digits) {
- int d = 4;
+ int d = digits.length;
do {
d--;
digits[d] = successor(digits[d]);
diff --git a/fop/build.xml b/fop/build.xml
index 177dbbaf3..8acb727d9 100644
--- a/fop/build.xml
+++ b/fop/build.xml
@@ -638,7 +638,7 @@ list of possible build targets.
<include name="org/apache/fop/util/*OutputStream.class"/>
<include name="org/apache/fop/util/SubInputStream.class"/>
<include name="org/apache/fop/util/Finalizable.class"/>
- <include name="org/apache/fop/util/CharUtilities.class"/>
+ <include name="org/apache/fop/util/CharUtilities*.class"/>
<include name="org/apache/fop/util/DecimalFormatCache*.class"/>
<include name="org/apache/fop/util/ImageObject.class"/>
<include name="org/apache/fop/util/HexEncoder.class"/>
diff --git a/fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml b/fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml
new file mode 100644
index 000000000..d4734600a
--- /dev/null
+++ b/fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml
@@ -0,0 +1,57 @@
+<?xml version="1.1" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<!-- $Id$ -->
+<testcase>
+ <info>
+ <p>
+ Checks hyphenation in combination with kerning.
+ </p>
+ </info>
+ <cfg>
+ <base14kerning>true</base14kerning>
+ </cfg>
+ <fo>
+ <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" language="en" hyphenate="true" font-family="Aegean600">
+ <fo:layout-master-set>
+ <fo:simple-page-master master-name="simple" page-height="5in" page-width="5in">
+ <fo:region-body/>
+ </fo:simple-page-master>
+ </fo:layout-master-set>
+ <fo:page-sequence master-reference="simple">
+ <fo:flow flow-name="xsl-region-body">
+ <fo:block font-size="20pt" line-height="1.0" text-align="justify" text-align-last="justify" background-color="lightgray" start-indent="10pt" end-indent="10pt" border="solid 1pt">
+ hyphenation&#66310;&#66311;&#66315;&#66316; regression advantage foundation&#66311;&#66312;&#66313;&#66314; &#66315;&#66316; vandavanda
+ </fo:block>
+ </fo:flow>
+ </fo:page-sequence>
+ </fo:root>
+ </fo>
+ <checks>
+ <eval expected="1" xpath="count(//pageViewport)"/>
+ <eval expected="en" xpath="/areaTree/pageSequence/@xml:lang"/>
+
+ <eval expected="47702" xpath="//flow/block[1]/lineArea[1]/text[1]/@twsadjust"/>
+ <eval expected="36968" xpath="//flow/block[1]/lineArea[2]/text[1]/@twsadjust"/>
+
+ <eval expected="advan-" xpath="//flow/block[1]/lineArea[1]/text[1]/word[3]"/>
+ <eval expected="0 0 0 -500 0 0" xpath="//flow/block[1]/lineArea[1]/text[1]/word[3]/@letter-adjust"/>
+ <eval expected="tage" xpath="//flow/block[1]/lineArea[2]/text[1]/word[1]"/>
+ <eval expected="vandavanda" xpath="//flow/block[1]/lineArea[2]/text[1]/word[4]"/>
+ <eval expected="0 -500 0 0 0 -400 -500 0 0 0" xpath="//flow/block[1]/lineArea[2]/text[1]/word[4]/@letter-adjust"/>
+ </checks>
+</testcase>
diff --git a/fop/test/resources/fonts/ttf/Aegean600.LICENSE b/fop/test/resources/fonts/ttf/Aegean600.LICENSE
new file mode 100644
index 000000000..9c4c180c3
--- /dev/null
+++ b/fop/test/resources/fonts/ttf/Aegean600.LICENSE
@@ -0,0 +1,3 @@
+http://users.teilar.gr/~g1951d/
+
+In lieu of a licence; fonts and documents in this site are free for any use;
diff --git a/fop/test/resources/fonts/ttf/Aegean600.ttf b/fop/test/resources/fonts/ttf/Aegean600.ttf
new file mode 100644
index 000000000..6f3abb059
--- /dev/null
+++ b/fop/test/resources/fonts/ttf/Aegean600.ttf
Binary files differ
diff --git a/fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE b/fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE
new file mode 100644
index 000000000..0a010886a
--- /dev/null
+++ b/fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE
@@ -0,0 +1,18 @@
+Copyright (C) 2008 The Android Open Source Project
+
+Licensed 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.
+
+##########
+
+This directory contains the fonts for the platform. They are licensed
+under the Apache 2 license.
diff --git a/fop/test/resources/fonts/ttf/AndroidEmoji.ttf b/fop/test/resources/fonts/ttf/AndroidEmoji.ttf
new file mode 100644
index 000000000..92bf047b8
--- /dev/null
+++ b/fop/test/resources/fonts/ttf/AndroidEmoji.ttf
Binary files differ
diff --git a/fop/test/xml/pdf-encoding/pdf-encoding-test.xconf b/fop/test/xml/pdf-encoding/pdf-encoding-test.xconf
index 27f96c950..ec087359b 100644
--- a/fop/test/xml/pdf-encoding/pdf-encoding-test.xconf
+++ b/fop/test/xml/pdf-encoding/pdf-encoding-test.xconf
@@ -29,19 +29,22 @@
<renderers>
<renderer mime="application/pdf">
<!-- disable PDF text compression -->
- <filterList>
+ <filterList>
<value>null</value>
</filterList>
<filterList type="image">
<value>flate</value>
<value>ascii-85</value>
</filterList>
-
+
<!-- use a custom font to show encoding problems -->
<fonts>
- <font metrics-url="test/resources/fonts/ttf/glb12.ttf.xml" embed-url="test/resources/fonts/ttf/glb12.ttf">
+ <font metrics-url="../../resources/fonts/ttf/glb12.ttf.xml" embed-url="../../resources/fonts/ttf/glb12.ttf">
<font-triplet name="Gladiator" style="normal" weight="normal"/>
</font>
+ <font embed-url="../../resources/fonts/ttf/Aegean600.ttf" >
+ <font-triplet name="Aegean600" style="normal" weight="normal"/>
+ </font>
</fonts>
</renderer>
</renderers>
diff --git a/fop/test/xml/pdf-encoding/test-custom-font.fo b/fop/test/xml/pdf-encoding/test-custom-font.fo
index 86689f343..ee4c0406a 100644
--- a/fop/test/xml/pdf-encoding/test-custom-font.fo
+++ b/fop/test/xml/pdf-encoding/test-custom-font.fo
@@ -21,7 +21,7 @@
Minimal FO document used to test PDF encoding
-->
-<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" font-family="Gladiator">
+<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" font-family="Gladiator" font-size="10px">
<fo:layout-master-set>
<fo:simple-page-master master-name="A4" page-height="29.7cm" page-width="21cm" margin="2cm">
<fo:region-body/>
diff --git a/fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo b/fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo
new file mode 100644
index 000000000..2793d737c
--- /dev/null
+++ b/fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<!--
+ Minimal FO document used to test PDF encoding
+-->
+
+<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" font-family="Helvetica, Aegean600">
+ <fo:layout-master-set>
+ <fo:simple-page-master master-name="A4" page-height="29.7cm" page-width="21cm" margin="2cm">
+ <fo:region-body/>
+ </fo:simple-page-master>
+ </fo:layout-master-set>
+ <fo:page-sequence master-reference="A4">
+ <fo:flow flow-name="xsl-region-body">
+ <!--<fo:block>Testing PDF text encoding using the user-specified AndroidEmoji font which cover non BMP codepoints</fo:block>-->
+ <fo:block>PDFE_TEST_MARK_1: Hello AndroidEmoji World!</fo:block>
+ <fo:block>PDFE_TEST_MARK_2: This is a OLD ITALIC LETTER A: XX_ &#x10300; _XX</fo:block>
+ </fo:flow>
+ </fo:page-sequence>
+</fo:root>