blob: dce045c2cac2cba9f37bd9912959d5d1262b4006 [file] [log] [blame]
/*
* 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
+ "}";
}
}