You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GlyphMapping.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.fonts;
  19. import java.util.ArrayList;
  20. import java.util.List;
  21. import org.apache.commons.logging.Log;
  22. import org.apache.commons.logging.LogFactory;
  23. import org.apache.fop.complexscripts.fonts.GlyphPositioningTable;
  24. import org.apache.fop.complexscripts.fonts.GlyphTable;
  25. import org.apache.fop.complexscripts.util.CharScript;
  26. import org.apache.fop.traits.MinOptMax;
  27. import org.apache.fop.util.CharUtilities;
  28. import static org.apache.fop.fonts.type1.AdobeStandardEncoding.i;
  29. /**
  30. * Stores the mapping of a text fragment to glyphs, along with various information.
  31. */
  32. public class GlyphMapping {
  33. private static final Log LOG = LogFactory.getLog(GlyphMapping.class);
  34. /** Inclusive. */
  35. public final int startIndex;
  36. /** Exclusive. */
  37. public final int endIndex;
  38. private int wordCharLength;
  39. public final int wordSpaceCount;
  40. public int letterSpaceCount;
  41. public MinOptMax areaIPD;
  42. public final boolean isHyphenated;
  43. public final boolean isSpace;
  44. public boolean breakOppAfter;
  45. public final Font font;
  46. public final int level;
  47. public final int[][] gposAdjustments;
  48. public String mapping;
  49. public List associations;
  50. public GlyphMapping(int startIndex, int endIndex, int wordSpaceCount, int letterSpaceCount,
  51. MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
  52. Font font, int level, int[][] gposAdjustments) {
  53. this(startIndex, endIndex, wordSpaceCount, letterSpaceCount, areaIPD, isHyphenated,
  54. isSpace, breakOppAfter, font, level, gposAdjustments, null, null);
  55. }
  56. public GlyphMapping(int startIndex, int endIndex, int wordSpaceCount, int letterSpaceCount,
  57. MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
  58. Font font, int level, int[][] gposAdjustments, String mapping, List associations) {
  59. assert startIndex <= endIndex;
  60. this.startIndex = startIndex;
  61. this.endIndex = endIndex;
  62. this.wordCharLength = -1;
  63. this.wordSpaceCount = wordSpaceCount;
  64. this.letterSpaceCount = letterSpaceCount;
  65. this.areaIPD = areaIPD;
  66. this.isHyphenated = isHyphenated;
  67. this.isSpace = isSpace;
  68. this.breakOppAfter = breakOppAfter;
  69. this.font = font;
  70. this.level = level;
  71. this.gposAdjustments = gposAdjustments;
  72. this.mapping = mapping;
  73. this.associations = associations;
  74. }
  75. public static GlyphMapping doGlyphMapping(TextFragment text, int startIndex, int endIndex,
  76. Font font, MinOptMax letterSpaceIPD, MinOptMax[] letterSpaceAdjustArray,
  77. char precedingChar, char breakOpportunityChar, final boolean endsWithHyphen, int level,
  78. boolean dontOptimizeForIdentityMapping, boolean retainAssociations, boolean retainControls) {
  79. GlyphMapping mapping;
  80. if (font.performsSubstitution() || font.performsPositioning()) {
  81. mapping = processWordMapping(text, startIndex, endIndex, font,
  82. breakOpportunityChar, endsWithHyphen, level,
  83. dontOptimizeForIdentityMapping, retainAssociations, retainControls);
  84. } else {
  85. mapping = processWordNoMapping(text, startIndex, endIndex, font,
  86. letterSpaceIPD, letterSpaceAdjustArray, precedingChar, breakOpportunityChar, endsWithHyphen, level);
  87. }
  88. return mapping;
  89. }
  90. private static GlyphMapping processWordMapping(TextFragment text, int startIndex,
  91. int endIndex, final Font font, final char breakOpportunityChar,
  92. final boolean endsWithHyphen, int level,
  93. boolean dontOptimizeForIdentityMapping, boolean retainAssociations, boolean retainControls) {
  94. int nLS = 0; // # of letter spaces
  95. String script = text.getScript();
  96. String language = text.getLanguage();
  97. if (LOG.isDebugEnabled()) {
  98. LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {"
  99. + " +M"
  100. + ", level = " + level
  101. + " }");
  102. }
  103. // 1. extract unmapped character sequence.
  104. CharSequence ics = text.subSequence(startIndex, endIndex);
  105. // 2. if script is not specified (by FO property) or it is specified as 'auto',
  106. // then compute dominant script.
  107. if ((script == null) || "auto".equals(script)) {
  108. script = CharScript.scriptTagFromCode(CharScript.dominantScript(ics));
  109. }
  110. if ((language == null) || "none".equals(language)) {
  111. language = "dflt";
  112. }
  113. // 3. perform mapping of chars to glyphs ... to glyphs ... to chars, retaining
  114. // associations if requested.
  115. List associations = retainAssociations ? new ArrayList() : null;
  116. // This is a workaround to read the ligature from the font even if the script
  117. // does not match the one defined for the table.
  118. // More info here: https://issues.apache.org/jira/browse/FOP-2638
  119. // zyyy == SCRIPT_UNDEFINED
  120. if ("zyyy".equals(script) || "auto".equals(script)) {
  121. script = "*";
  122. }
  123. CharSequence mcs = font.performSubstitution(ics, script, language, associations, retainControls);
  124. // 4. compute glyph position adjustments on (substituted) characters.
  125. int[][] gpa = null;
  126. if (font.performsPositioning()) {
  127. // handle GPOS adjustments
  128. gpa = font.performPositioning(mcs, script, language);
  129. }
  130. if (useKerningAdjustments(font, script, language)) {
  131. // handle standard (non-GPOS) kerning adjustments
  132. gpa = getKerningAdjustments(mcs, font, gpa);
  133. }
  134. // 5. reorder combining marks so that they precede (within the mapped char sequence) the
  135. // base to which they are applied; N.B. position adjustments (gpa) are reordered in place.
  136. mcs = font.reorderCombiningMarks(mcs, gpa, script, language, associations);
  137. // 6. compute word ipd based on final position adjustments.
  138. MinOptMax ipd = MinOptMax.ZERO;
  139. for (int i = 0, n = mcs.length(); i < n; i++) {
  140. int c = mcs.charAt(i);
  141. if (CharUtilities.containsSurrogatePairAt(mcs, i)) {
  142. c = Character.toCodePoint((char) c, mcs.charAt(++i));
  143. }
  144. int w = font.getCharWidth(c);
  145. if (w < 0) {
  146. w = 0;
  147. }
  148. if (gpa != null) {
  149. w += gpa[i][GlyphPositioningTable.Value.IDX_X_ADVANCE];
  150. }
  151. ipd = ipd.plus(w);
  152. }
  153. // [TBD] - handle letter spacing
  154. return new GlyphMapping(startIndex, endIndex, 0, nLS, ipd, endsWithHyphen, false,
  155. breakOpportunityChar != 0, font, level, gpa,
  156. !dontOptimizeForIdentityMapping && CharUtilities.isSameSequence(mcs, ics) ? null : mcs.toString(),
  157. associations);
  158. }
  159. private static boolean useKerningAdjustments(final Font font, String script, String language) {
  160. return font.hasKerning() && !font.hasFeature(GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, script, language, "kern");
  161. }
  162. /**
  163. * Given a mapped character sequence MCS, obtain glyph position adjustments from the
  164. * font's kerning data.
  165. *
  166. * @param mcs mapped character sequence
  167. * @param font applicable font
  168. * @return glyph position adjustments (or null if no kerning)
  169. */
  170. private static int[][] getKerningAdjustments(CharSequence mcs, final Font font, int[][] gpa) {
  171. int numCodepoints = Character.codePointCount(mcs, 0, mcs.length());
  172. // extract kerning array
  173. int[] kernings = new int[numCodepoints]; // kerning array
  174. int prevCp = -1;
  175. int i = 0;
  176. for (int cp : CharUtilities.codepointsIter(mcs)) {
  177. if (prevCp >= 0) {
  178. kernings[i] = font.getKernValue(prevCp, cp);
  179. }
  180. prevCp = cp;
  181. i++;
  182. }
  183. // was there a non-zero kerning?
  184. boolean hasKerning = false;
  185. for (int kerningValue : kernings) {
  186. if (kerningValue != 0) {
  187. hasKerning = true;
  188. break;
  189. }
  190. }
  191. // if non-zero kerning, then create and return glyph position adjustment array
  192. if (hasKerning) {
  193. if (gpa == null) {
  194. gpa = new int[numCodepoints][4];
  195. }
  196. for (i = 0; i < numCodepoints; i++) {
  197. if (i > 0) {
  198. gpa [i - 1][GlyphPositioningTable.Value.IDX_X_ADVANCE] += kernings[i];
  199. }
  200. }
  201. return gpa;
  202. } else {
  203. return null;
  204. }
  205. }
  206. private static GlyphMapping processWordNoMapping(TextFragment text, int startIndex, int endIndex,
  207. final Font font, MinOptMax letterSpaceIPD, MinOptMax[] letterSpaceAdjustArray,
  208. char precedingChar, final char breakOpportunityChar, final boolean endsWithHyphen, int level) {
  209. boolean kerning = font.hasKerning();
  210. MinOptMax wordIPD = MinOptMax.ZERO;
  211. if (LOG.isDebugEnabled()) {
  212. LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {"
  213. + " -M"
  214. + ", level = " + level
  215. + " }");
  216. }
  217. CharSequence ics = text.subSequence(startIndex, endIndex);
  218. int offset = 0;
  219. for (int currentChar : CharUtilities.codepointsIter(ics)) {
  220. // character width
  221. int charWidth = font.getCharWidth(currentChar);
  222. wordIPD = wordIPD.plus(charWidth);
  223. // kerning
  224. if (kerning) {
  225. int kern = 0;
  226. if (offset > 0) {
  227. int previousChar = java.lang.Character.codePointAt(ics, offset - 1);
  228. kern = font.getKernValue(previousChar, currentChar);
  229. } else if (precedingChar != 0) {
  230. kern = font.getKernValue(precedingChar, currentChar);
  231. }
  232. if (kern != 0) {
  233. addToLetterAdjust(letterSpaceAdjustArray, startIndex + offset, kern);
  234. wordIPD = wordIPD.plus(kern);
  235. }
  236. }
  237. offset++;
  238. }
  239. if (kerning
  240. && (breakOpportunityChar != 0)
  241. && !isSpace(breakOpportunityChar)
  242. && endIndex > 0
  243. && endsWithHyphen) {
  244. int endChar = text.charAt(endIndex - 1);
  245. if (java.lang.Character.isLowSurrogate((char) endChar)) {
  246. char highSurrogate = text.charAt(endIndex - 2);
  247. endChar = java.lang.Character.toCodePoint(highSurrogate, (char) endChar);
  248. }
  249. int kern = font.getKernValue(endChar, (int) breakOpportunityChar);
  250. if (kern != 0) {
  251. addToLetterAdjust(letterSpaceAdjustArray, endIndex, kern);
  252. // TODO: add kern to wordIPD?
  253. }
  254. }
  255. // shy+chars at start of word: wordLength == 0 && breakOpportunity
  256. // shy only characters in word: wordLength == 0 && !breakOpportunity
  257. int wordLength = endIndex - startIndex;
  258. int letterSpaces = 0;
  259. if (wordLength != 0) {
  260. letterSpaces = wordLength - 1;
  261. // if there is a break opportunity and the next one (break character)
  262. // is not a space, it could be used as a line end;
  263. // add one more letter space, in case other text follows
  264. if ((breakOpportunityChar != 0) && !isSpace(breakOpportunityChar)) {
  265. letterSpaces++;
  266. }
  267. }
  268. assert letterSpaces >= 0;
  269. wordIPD = wordIPD.plus(letterSpaceIPD.mult(letterSpaces));
  270. // create and return the AreaInfo object
  271. return new GlyphMapping(startIndex, endIndex, 0, letterSpaces, wordIPD, endsWithHyphen, false,
  272. (breakOpportunityChar != 0) && !isSpace(breakOpportunityChar), font, level, null);
  273. }
  274. private static void addToLetterAdjust(MinOptMax[] letterSpaceAdjustArray, int index, int width) {
  275. if (letterSpaceAdjustArray[index] == null) {
  276. letterSpaceAdjustArray[index] = MinOptMax.getInstance(width);
  277. } else {
  278. letterSpaceAdjustArray[index] = letterSpaceAdjustArray[index].plus(width);
  279. }
  280. }
  281. /**
  282. * Indicates whether a character is a space in terms of this layout manager.
  283. *
  284. * @param ch the character
  285. * @return true if it's a space
  286. */
  287. public static boolean isSpace(final char ch) {
  288. return ch == CharUtilities.SPACE
  289. || CharUtilities.isNonBreakableSpace(ch)
  290. || CharUtilities.isFixedWidthSpace(ch);
  291. }
  292. /**
  293. * Obtain number of 'characters' contained in word. If word is mapped, then this
  294. * number may be less than or greater than the original length (breakIndex -
  295. * startIndex). We compute and memoize thius length upon first invocation of this
  296. * method.
  297. */
  298. public int getWordLength() {
  299. if (wordCharLength == -1) {
  300. if (mapping != null) {
  301. wordCharLength = mapping.length();
  302. } else {
  303. assert endIndex >= startIndex;
  304. wordCharLength = endIndex - startIndex;
  305. }
  306. }
  307. return wordCharLength;
  308. }
  309. public void addToAreaIPD(MinOptMax idp) {
  310. areaIPD = areaIPD.plus(idp);
  311. }
  312. public String toString() {
  313. return super.toString() + "{"
  314. + "interval = [" + startIndex + "," + endIndex + "]"
  315. + ", isSpace = " + isSpace
  316. + ", level = " + level
  317. + ", areaIPD = " + areaIPD
  318. + ", letterSpaceCount = " + letterSpaceCount
  319. + ", wordSpaceCount = " + wordSpaceCount
  320. + ", isHyphenated = " + isHyphenated
  321. + ", font = " + font
  322. + "}";
  323. }
  324. }