| /* |
| * 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.ArrayList; |
| import java.util.List; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; |
| import org.apache.fop.complexscripts.fonts.GlyphTable; |
| 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. |
| */ |
| public class GlyphMapping { |
| |
| private static final Log LOG = LogFactory.getLog(GlyphMapping.class); |
| /** Inclusive. */ |
| public final int startIndex; |
| /** Exclusive. */ |
| public final int endIndex; |
| private int wordCharLength; |
| public final int wordSpaceCount; |
| public int letterSpaceCount; |
| public MinOptMax areaIPD; |
| public final boolean isHyphenated; |
| public final boolean isSpace; |
| public boolean breakOppAfter; |
| public final Font font; |
| public final int level; |
| public final int[][] gposAdjustments; |
| public String mapping; |
| public List associations; |
| |
| public GlyphMapping(int startIndex, int endIndex, int wordSpaceCount, int letterSpaceCount, |
| 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); |
| } |
| |
| public GlyphMapping(int startIndex, int endIndex, int wordSpaceCount, int letterSpaceCount, |
| MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter, |
| Font font, int level, int[][] gposAdjustments, String mapping, List associations) { |
| assert startIndex <= endIndex; |
| this.startIndex = startIndex; |
| this.endIndex = endIndex; |
| this.wordCharLength = -1; |
| this.wordSpaceCount = wordSpaceCount; |
| this.letterSpaceCount = letterSpaceCount; |
| this.areaIPD = areaIPD; |
| this.isHyphenated = isHyphenated; |
| this.isSpace = isSpace; |
| this.breakOppAfter = breakOppAfter; |
| this.font = font; |
| this.level = level; |
| this.gposAdjustments = gposAdjustments; |
| this.mapping = mapping; |
| this.associations = associations; |
| } |
| |
| public static GlyphMapping doGlyphMapping(TextFragment text, int startIndex, int endIndex, |
| Font font, MinOptMax letterSpaceIPD, MinOptMax[] letterSpaceAdjustArray, |
| char precedingChar, char breakOpportunityChar, final boolean endsWithHyphen, int level, |
| boolean dontOptimizeForIdentityMapping, boolean retainAssociations, boolean retainControls) { |
| GlyphMapping mapping; |
| if (font.performsSubstitution() || font.performsPositioning()) { |
| mapping = processWordMapping(text, startIndex, endIndex, font, |
| breakOpportunityChar, endsWithHyphen, level, |
| dontOptimizeForIdentityMapping, retainAssociations, retainControls); |
| } else { |
| mapping = processWordNoMapping(text, startIndex, endIndex, font, |
| letterSpaceIPD, letterSpaceAdjustArray, precedingChar, breakOpportunityChar, endsWithHyphen, level); |
| } |
| return mapping; |
| } |
| |
| 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 nLS = 0; // # of letter spaces |
| String script = text.getScript(); |
| String language = text.getLanguage(); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {" |
| + " +M" |
| + ", level = " + level |
| + " }"); |
| } |
| |
| // 1. extract unmapped character sequence. |
| 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. |
| if ((script == null) || "auto".equals(script)) { |
| script = CharScript.scriptTagFromCode(CharScript.dominantScript(ics)); |
| } |
| if ((language == null) || "none".equals(language)) { |
| language = "dflt"; |
| } |
| |
| // 3. perform mapping of chars to glyphs ... to glyphs ... to chars, retaining |
| // associations if requested. |
| 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. |
| int[][] gpa = null; |
| if (font.performsPositioning()) { |
| // handle GPOS adjustments |
| gpa = font.performPositioning(mcs, script, language); |
| } |
| if (useKerningAdjustments(font, script, language)) { |
| // handle standard (non-GPOS) kerning adjustments |
| gpa = getKerningAdjustments(mcs, font, gpa); |
| } |
| |
| // 5. reorder combining marks so that they precede (within the mapped char sequence) the |
| // base to which they are applied; N.B. position adjustments (gpa) are reordered in place. |
| mcs = font.reorderCombiningMarks(mcs, gpa, script, language, associations); |
| |
| // 6. compute word ipd based on final position adjustments. |
| MinOptMax ipd = MinOptMax.ZERO; |
| for (int i = 0, n = mcs.length(); i < n; i++) { |
| int c = mcs.charAt(i); |
| |
| if (CharUtilities.containsSurrogatePairAt(mcs, i)) { |
| c = Character.toCodePoint((char) c, mcs.charAt(++i)); |
| } |
| |
| int w = font.getCharWidth(c); |
| if (w < 0) { |
| w = 0; |
| } |
| if (gpa != null) { |
| w += gpa[i][GlyphPositioningTable.Value.IDX_X_ADVANCE]; |
| } |
| ipd = ipd.plus(w); |
| } |
| |
| // [TBD] - handle letter spacing |
| |
| return new GlyphMapping(startIndex, endIndex, 0, nLS, ipd, endsWithHyphen, false, |
| breakOpportunityChar != 0, font, level, gpa, |
| !dontOptimizeForIdentityMapping && CharUtilities.isSameSequence(mcs, ics) ? null : mcs.toString(), |
| associations); |
| } |
| |
| private static boolean useKerningAdjustments(final Font font, String script, String language) { |
| return font.hasKerning() && !font.hasFeature(GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, script, language, "kern"); |
| } |
| |
| /** |
| * Given a mapped character sequence MCS, obtain glyph position adjustments from the |
| * font's kerning data. |
| * |
| * @param mcs mapped character sequence |
| * @param font applicable font |
| * @return glyph position adjustments (or null if no kerning) |
| */ |
| private static int[][] getKerningAdjustments(CharSequence mcs, final Font font, int[][] gpa) { |
| int numCodepoints = Character.codePointCount(mcs, 0, mcs.length()); |
| // extract kerning array |
| 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); |
| } |
| prevCp = cp; |
| i++; |
| } |
| // was there a non-zero kerning? |
| boolean hasKerning = false; |
| for (int kerningValue : kernings) { |
| if (kerningValue != 0) { |
| hasKerning = true; |
| break; |
| } |
| } |
| // if non-zero kerning, then create and return glyph position adjustment array |
| if (hasKerning) { |
| if (gpa == null) { |
| gpa = new int[numCodepoints][4]; |
| } |
| for (i = 0; i < numCodepoints; i++) { |
| if (i > 0) { |
| gpa [i - 1][GlyphPositioningTable.Value.IDX_X_ADVANCE] += kernings[i]; |
| } |
| } |
| return gpa; |
| } else { |
| return null; |
| } |
| } |
| |
| private static GlyphMapping processWordNoMapping(TextFragment text, int startIndex, int endIndex, |
| final Font font, MinOptMax letterSpaceIPD, MinOptMax[] letterSpaceAdjustArray, |
| char precedingChar, final char breakOpportunityChar, final boolean endsWithHyphen, int level) { |
| boolean kerning = font.hasKerning(); |
| MinOptMax wordIPD = MinOptMax.ZERO; |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("PW: [" + startIndex + "," + endIndex + "]: {" |
| + " -M" |
| + ", level = " + level |
| + " }"); |
| } |
| |
| CharSequence ics = text.subSequence(startIndex, endIndex); |
| int offset = 0; |
| for (int currentChar : CharUtilities.codepointsIter(ics)) { |
| |
| // character width |
| int charWidth = font.getCharWidth(currentChar); |
| wordIPD = wordIPD.plus(charWidth); |
| |
| // kerning |
| if (kerning) { |
| int kern = 0; |
| if (offset > 0) { |
| int previousChar = Character.codePointAt(ics, offset - 1); |
| kern = font.getKernValue(previousChar, currentChar); |
| } else if (precedingChar != 0) { |
| kern = font.getKernValue(precedingChar, currentChar); |
| } |
| if (kern != 0) { |
| addToLetterAdjust(letterSpaceAdjustArray, startIndex + offset, kern); |
| wordIPD = wordIPD.plus(kern); |
| } |
| } |
| offset++; |
| } |
| if (kerning |
| && (breakOpportunityChar != 0) |
| && !isSpace(breakOpportunityChar) |
| && endIndex > 0 |
| && endsWithHyphen) { |
| int endChar = text.charAt(endIndex - 1); |
| |
| if (Character.isLowSurrogate((char) endChar)) { |
| char highSurrogate = text.charAt(endIndex - 2); |
| endChar = Character.toCodePoint(highSurrogate, (char) endChar); |
| } |
| |
| int kern = font.getKernValue(endChar, (int) breakOpportunityChar); |
| if (kern != 0) { |
| addToLetterAdjust(letterSpaceAdjustArray, endIndex, kern); |
| // TODO: add kern to wordIPD? |
| } |
| } |
| // shy+chars at start of word: wordLength == 0 && breakOpportunity |
| // shy only characters in word: wordLength == 0 && !breakOpportunity |
| int wordLength = endIndex - startIndex; |
| int letterSpaces = 0; |
| if (wordLength != 0) { |
| letterSpaces = wordLength - 1; |
| // if there is a break opportunity and the next one (break character) |
| // is not a space, it could be used as a line end; |
| // add one more letter space, in case other text follows |
| if ((breakOpportunityChar != 0) && !isSpace(breakOpportunityChar)) { |
| letterSpaces++; |
| } |
| } |
| assert letterSpaces >= 0; |
| wordIPD = wordIPD.plus(letterSpaceIPD.mult(letterSpaces)); |
| |
| // create and return the AreaInfo object |
| return new GlyphMapping(startIndex, endIndex, 0, letterSpaces, wordIPD, endsWithHyphen, false, |
| (breakOpportunityChar != 0) && !isSpace(breakOpportunityChar), font, level, null); |
| } |
| |
| private static void addToLetterAdjust(MinOptMax[] letterSpaceAdjustArray, int index, int width) { |
| if (letterSpaceAdjustArray[index] == null) { |
| letterSpaceAdjustArray[index] = MinOptMax.getInstance(width); |
| } else { |
| letterSpaceAdjustArray[index] = letterSpaceAdjustArray[index].plus(width); |
| } |
| } |
| |
| /** |
| * Indicates whether a character is a space in terms of this layout manager. |
| * |
| * @param ch the character |
| * @return true if it's a space |
| */ |
| public static boolean isSpace(final char ch) { |
| return ch == CharUtilities.SPACE |
| || CharUtilities.isNonBreakableSpace(ch) |
| || CharUtilities.isFixedWidthSpace(ch); |
| } |
| |
| /** |
| * Obtain number of 'characters' contained in word. If word is mapped, then this |
| * number may be less than or greater than the original length (breakIndex - |
| * startIndex). We compute and memoize thius length upon first invocation of this |
| * method. |
| */ |
| public int getWordLength() { |
| if (wordCharLength == -1) { |
| if (mapping != null) { |
| wordCharLength = mapping.length(); |
| } else { |
| assert endIndex >= startIndex; |
| wordCharLength = endIndex - startIndex; |
| } |
| } |
| return wordCharLength; |
| } |
| |
| public void addToAreaIPD(MinOptMax idp) { |
| areaIPD = areaIPD.plus(idp); |
| } |
| |
| public String toString() { |
| return super.toString() + "{" |
| + "interval = [" + startIndex + "," + endIndex + "]" |
| + ", isSpace = " + isSpace |
| + ", level = " + level |
| + ", areaIPD = " + areaIPD |
| + ", letterSpaceCount = " + letterSpaceCount |
| + ", wordSpaceCount = " + wordSpaceCount |
| + ", isHyphenated = " + isHyphenated |
| + ", font = " + font |
| + "}"; |
| } |
| |
| } |