git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1827168 13f79535-47bb-0310-9956-ffa450edef68tags/fop-2_3
@@ -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> |
@@ -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 |
@@ -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. |
@@ -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} */ |
@@ -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). |
@@ -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} */ |
@@ -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; | |||
} | |||
/** |
@@ -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; | |||
} | |||
} |
@@ -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? |
@@ -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); | |||
} |
@@ -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; | |||
} | |||
@@ -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; |
@@ -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]; |
@@ -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)); | |||
} | |||
} | |||
@@ -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); | |||
} | |||
/** |
@@ -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; |
@@ -221,6 +221,11 @@ public class CustomFontMetricsMapper extends Typeface implements FontMetricsMapp | |||
return typeface.hasKerningInfo(); | |||
} | |||
/** {@inheritDoc} */ | |||
public boolean isMultiByte() { | |||
return typeface.isMultiByte(); | |||
} | |||
/** | |||
* {@inheritDoc} | |||
*/ |
@@ -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); | |||
} | |||
} |
@@ -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)) { |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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) { |
@@ -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) { |
@@ -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 href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5003547">Bug JDK-5003547</a> | |||
* @param s {@link CharSequence} to iter | |||
* @param beginIndex lower range | |||
* @param endIndex upper range | |||
* @return codepoint iterator for the given sub-CharSequence. | |||
*/ | |||
public static Iterable<Integer> codepointsIter(final CharSequence s, final int beginIndex, final int endIndex) { | |||
if (beginIndex < 0) { | |||
throw new StringIndexOutOfBoundsException(beginIndex); | |||
} | |||
if (endIndex > s.length()) { | |||
throw new StringIndexOutOfBoundsException(endIndex); | |||
} | |||
int subLen = endIndex - beginIndex; | |||
if (subLen < 0) { | |||
throw new StringIndexOutOfBoundsException(subLen); | |||
} | |||
return new Iterable<Integer>() { | |||
public Iterator<Integer> iterator() { | |||
return new Iterator<Integer>() { | |||
int nextIndex = beginIndex; | |||
public boolean hasNext() { | |||
return nextIndex < endIndex; | |||
} | |||
public Integer next() { | |||
if (!hasNext()) { | |||
// Findbugs wants this: IT_NO_SUCH_ELEMENT | |||
throw new NoSuchElementException(); | |||
} | |||
int result = Character.codePointAt(s, nextIndex); | |||
nextIndex += Character.charCount(result); | |||
return result; | |||
} | |||
public void remove() { | |||
throw new UnsupportedOperationException(); | |||
} | |||
}; | |||
} | |||
}; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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); | |||
} | |||
} | |||
@@ -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); | |||
} | |||
} | |||
@@ -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(); |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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()); | |||
} | |||
/** |
@@ -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]; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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]; | |||
} | |||
} | |||
} |
@@ -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..."); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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]); |
@@ -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"/> |
@@ -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𐌆𐌇𐌋𐌌 regression advantage foundation𐌇𐌈𐌉𐌊 𐌋𐌌 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> |
@@ -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; |
@@ -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. |
@@ -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> |
@@ -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/> |
@@ -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_ 𐌀 _XX</fo:block> | |||
</fo:flow> | |||
</fo:page-sequence> | |||
</fo:root> |