Browse Source

FOP-1969: Support for unicode Surrogate pairs

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_SurrogatePairs@1826330 13f79535-47bb-0310-9956-ffa450edef68
Temp_SurrogatePairs
Simon Steiner 6 years ago
parent
commit
a6fc886c8d
47 changed files with 1677 additions and 252 deletions
  1. 7
    0
      fop-core/pom.xml
  2. 17
    0
      fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java
  3. 14
    0
      fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java
  4. 8
    3
      fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java
  5. 11
    1
      fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java
  6. 22
    11
      fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java
  7. 79
    31
      fop-core/src/main/java/org/apache/fop/fonts/Font.java
  8. 9
    4
      fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java
  9. 61
    35
      fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java
  10. 56
    7
      fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
  11. 3
    2
      fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java
  12. 105
    3
      fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java
  13. 4
    2
      fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java
  14. 14
    4
      fop-core/src/main/java/org/apache/fop/pdf/PDFText.java
  15. 19
    11
      fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java
  16. 11
    2
      fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java
  17. 5
    0
      fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java
  18. 12
    5
      fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java
  19. 17
    7
      fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java
  20. 88
    0
      fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java
  21. 2
    2
      fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java
  22. 15
    11
      fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java
  23. 9
    4
      fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java
  24. 133
    0
      fop-core/src/main/java/org/apache/fop/util/CharUtilities.java
  25. 11
    4
      fop-core/src/main/java/org/apache/fop/util/HexEncoder.java
  26. 2
    3
      fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java
  27. 8
    10
      fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java
  28. 17
    23
      fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java
  29. 7
    0
      fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java
  30. 198
    0
      fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java
  31. 139
    0
      fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java
  32. 121
    9
      fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java
  33. 119
    0
      fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java
  34. 55
    31
      fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java
  35. 57
    17
      fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java
  36. 12
    4
      fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java
  37. 73
    0
      fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java
  38. 14
    1
      fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java
  39. 1
    1
      fop/build.xml
  40. 57
    0
      fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml
  41. 3
    0
      fop/test/resources/fonts/ttf/Aegean600.LICENSE
  42. BIN
      fop/test/resources/fonts/ttf/Aegean600.ttf
  43. 18
    0
      fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE
  44. BIN
      fop/test/resources/fonts/ttf/AndroidEmoji.ttf
  45. 6
    3
      fop/test/xml/pdf-encoding/pdf-encoding-test.xconf
  46. 1
    1
      fop/test/xml/pdf-encoding/test-custom-font.fo
  47. 37
    0
      fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo

+ 7
- 0
fop-core/pom.xml View File

@@ -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>

+ 17
- 0
fop-core/src/main/java/org/apache/fop/complexscripts/util/GlyphSequence.java View File

@@ -147,12 +147,29 @@ 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() {
return characters.limit();
}

/**
* 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

+ 14
- 0
fop-core/src/main/java/org/apache/fop/fonts/CIDFont.java View File

@@ -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.

+ 8
- 3
fop-core/src/main/java/org/apache/fop/fonts/CIDFull.java View File

@@ -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} */

+ 11
- 1
fop-core/src/main/java/org/apache/fop/fonts/CIDSet.java View File

@@ -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
@@ -67,6 +67,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).

+ 22
- 11
fop-core/src/main/java/org/apache/fop/fonts/CIDSubset.java View File

@@ -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} */

+ 79
- 31
fop-core/src/main/java/org/apache/fop/fonts/Font.java View File

@@ -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;
}
@@ -205,30 +214,6 @@ public class Font implements Substitutable, Positionable {
return 0;
}

/**
* 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
@@ -263,10 +248,30 @@ public class Font implements Substitutable, Positionable {
return c;
}

/**
* 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) {
@@ -277,6 +282,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}
*/
@@ -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;
}

/**

+ 9
- 4
fop-core/src/main/java/org/apache/fop/fonts/FontSelector.java View File

@@ -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;
}
}

+ 61
- 35
fop-core/src/main/java/org/apache/fop/fonts/GlyphMapping.java View File

@@ -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?

+ 56
- 7
fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java View File

@@ -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;
@@ -377,10 +378,38 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
return (char) glyphIndex;
}

/** {@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);
}

+ 3
- 2
fop-core/src/main/java/org/apache/fop/fonts/truetype/OFMtxEntry.java View File

@@ -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;
}


+ 105
- 3
fop-core/src/main/java/org/apache/fop/fonts/truetype/OpenFont.java View File

@@ -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;

+ 4
- 2
fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java View File

@@ -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];

+ 14
- 4
fop-core/src/main/java/org/apache/fop/pdf/PDFText.java View File

@@ -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));
}
}


+ 19
- 11
fop-core/src/main/java/org/apache/fop/pdf/PDFTextUtil.java View File

@@ -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);
}

/**

+ 11
- 2
fop-core/src/main/java/org/apache/fop/pdf/PDFToUnicodeCMap.java View File

@@ -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;

+ 5
- 0
fop-core/src/main/java/org/apache/fop/render/java2d/CustomFontMetricsMapper.java View File

@@ -221,6 +221,11 @@ public class CustomFontMetricsMapper extends Typeface implements FontMetricsMapp
return typeface.hasKerningInfo();
}

/** {@inheritDoc} */
public boolean isMultiByte() {
return typeface.isMultiByte();
}

/**
* {@inheritDoc}
*/

+ 12
- 5
fop-core/src/main/java/org/apache/fop/render/java2d/Java2DPainter.java View File

@@ -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);
}



}

+ 17
- 7
fop-core/src/main/java/org/apache/fop/render/java2d/Java2DRenderer.java View File

@@ -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)) {

+ 88
- 0
fop-core/src/main/java/org/apache/fop/render/java2d/Java2DUtil.java View File

@@ -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;
}

}

+ 2
- 2
fop-core/src/main/java/org/apache/fop/render/pcl/fonts/truetype/PCLTTFFontReader.java View File

@@ -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();
}

+ 15
- 11
fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java View File

@@ -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) {

+ 9
- 4
fop-core/src/main/java/org/apache/fop/render/ps/PSPainter.java View File

@@ -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) {

+ 133
- 0
fop-core/src/main/java/org/apache/fop/util/CharUtilities.java View File

@@ -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 > 0xFFFF, 0 otherwise
* @return 1 if codePoint > 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 haref="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();
}
};
}
};
}
}

+ 11
- 4
fop-core/src/main/java/org/apache/fop/util/HexEncoder.java View File

@@ -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);
}
}

}

+ 2
- 3
fop-core/src/test/java/org/apache/fop/complexscripts/bidi/BidiTestData.java View File

@@ -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;
}

+ 8
- 10
fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/ArabicWordFormsTestCase.java View File

@@ -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);
}
}


+ 17
- 23
fop-core/src/test/java/org/apache/fop/complexscripts/scripts/arabic/GenerateArabicTestData.java View File

@@ -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);
}
}


+ 7
- 0
fop-core/src/test/java/org/apache/fop/fonts/CIDFullTestCase.java View File

@@ -88,6 +88,13 @@ public class CIDFullTestCase {
assertEquals(cidFull.mapChar(9, c), (char) 9);
}

@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();

+ 198
- 0
fop-core/src/test/java/org/apache/fop/fonts/CIDSubsetTestCase.java View File

@@ -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);
}
}
}

+ 139
- 0
fop-core/src/test/java/org/apache/fop/fonts/FontSelectorTestCase.java View File

@@ -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);
}

}

+ 121
- 9
fop-core/src/test/java/org/apache/fop/fonts/truetype/TTFFileTestCase.java View File

@@ -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());
}

/**

+ 119
- 0
fop-core/src/test/java/org/apache/fop/render/java2d/Java2DUtilTestCase.java View File

@@ -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];
}
}
}

+ 55
- 31
fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java View File

@@ -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);
}
}

+ 57
- 17
fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java View File

@@ -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];
}
}
}

+ 12
- 4
fop-core/src/test/java/org/apache/fop/render/ps/PSPainterTestCase.java View File

@@ -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...");
}

+ 73
- 0
fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java View File

@@ -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);
}
}

+ 14
- 1
fop-core/src/test/java/org/apache/fop/util/HexEncoderTestCase.java View File

@@ -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]);

+ 1
- 1
fop/build.xml View File

@@ -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"/>

+ 57
- 0
fop/test/layoutengine/hyphenation-testcases/block_hyphenation_kerning_non_bmp.xml View File

@@ -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>

+ 3
- 0
fop/test/resources/fonts/ttf/Aegean600.LICENSE View File

@@ -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;

BIN
fop/test/resources/fonts/ttf/Aegean600.ttf View File


+ 18
- 0
fop/test/resources/fonts/ttf/AndroidEmoji.LICENSE View File

@@ -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.

BIN
fop/test/resources/fonts/ttf/AndroidEmoji.ttf View File


+ 6
- 3
fop/test/xml/pdf-encoding/pdf-encoding-test.xconf View File

@@ -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>

+ 1
- 1
fop/test/xml/pdf-encoding/test-custom-font.fo View File

@@ -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/>

+ 37
- 0
fop/test/xml/pdf-encoding/test-custom-non-bmp-font.fo View File

@@ -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>

Loading…
Cancel
Save