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