blob: 54fa7d4753410a1700f3dbd4b416c998acef9bd5 [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.layoutmgr.inline;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.area.Trait;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.flow.Character;
import org.apache.fop.fo.properties.StructurePointerPropertySet;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontSelector;
import org.apache.fop.fonts.GlyphPositioningTable;
import org.apache.fop.layoutmgr.InlineKnuthSequence;
import org.apache.fop.layoutmgr.KnuthBox;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.KnuthPenalty;
import org.apache.fop.layoutmgr.KnuthSequence;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.LeafPosition;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.layoutmgr.PositionIterator;
import org.apache.fop.layoutmgr.TraitSetter;
import org.apache.fop.text.linebreak.LineBreakStatus;
import org.apache.fop.traits.MinOptMax;
import org.apache.fop.traits.SpaceVal;
import org.apache.fop.util.CharUtilities;
import org.apache.fop.util.ListUtil;
/**
* LayoutManager for text (a sequence of characters) which generates one
* or more inline areas.
*/
public class TextLayoutManager extends LeafNodeLayoutManager {
//TODO: remove all final modifiers at local variables
// static final int SOFT_HYPHEN_PENALTY = KnuthPenalty.FLAGGED_PENALTY / 10;
private static final int SOFT_HYPHEN_PENALTY = 1;
/**
* Store information about each potential text area.
* Index of character which ends the area, IPD of area, including
* any word-space and letter-space.
* Number of word-spaces?
*/
private class AreaInfo {
private final int startIndex;
private final int breakIndex;
private int wordCharLength;
private final int wordSpaceCount;
private int letterSpaceCount;
private MinOptMax areaIPD;
private final boolean isHyphenated;
private final boolean isSpace;
private boolean breakOppAfter;
private final Font font;
private final int level;
private final int[][] gposAdjustments;
AreaInfo // CSOK: ParameterNumber
(int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount,
MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
Font font, int level, int[][] gposAdjustments) {
assert startIndex <= breakIndex;
this.startIndex = startIndex;
this.breakIndex = breakIndex;
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;
}
/**
* 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.
*/
private int getWordLength() {
if ( wordCharLength == -1 ) {
if ( foText.hasMapping ( startIndex, breakIndex ) ) {
wordCharLength = foText.getMapping ( startIndex, breakIndex ).length();
} else {
assert breakIndex >= startIndex;
wordCharLength = breakIndex - startIndex;
}
}
return wordCharLength;
}
private void addToAreaIPD(MinOptMax idp) {
areaIPD = areaIPD.plus(idp);
}
public String toString() {
return super.toString() + "{"
+ "interval = [" + startIndex + "," + breakIndex + "]"
+ ", isSpace = " + isSpace
+ ", level = " + level
+ ", areaIPD = " + areaIPD
+ ", letterSpaceCount = " + letterSpaceCount
+ ", wordSpaceCount = " + wordSpaceCount
+ ", isHyphenated = " + isHyphenated
+ ", font = " + font
+ "}";
}
}
/**
* this class stores information about changes in vecAreaInfo which are not yet applied
*/
private final class PendingChange {
private final AreaInfo areaInfo;
private final int index;
private PendingChange(final AreaInfo areaInfo, final int index) {
this.areaInfo = areaInfo;
this.index = index;
}
}
/**
* logging instance
*/
private static final Log LOG = LogFactory.getLog(TextLayoutManager.class);
// Hold all possible breaks for the text in this LM's FO.
private final List areaInfos;
/** Non-space characters on which we can end a line. */
private static final String BREAK_CHARS = "-/";
private final FOText foText;
/**
* Contains an array of widths to adjust for kerning. The first entry can
* be used to influence the start position of the first letter. The entry i+1 defines the
* cursor advancement after the character i. A null entry means no special advancement.
*/
private final MinOptMax[] letterSpaceAdjustArray; //size = textArray.length + 1
/** Font used for the space between words. */
private Font spaceFont = null;
/** Start index of next TextArea */
private int nextStart = 0;
/** size of a space character (U+0020) glyph in current font */
private int spaceCharIPD;
private MinOptMax wordSpaceIPD;
private MinOptMax letterSpaceIPD;
/** size of the hyphen character glyph in current font */
private int hyphIPD;
private boolean hasChanged = false;
private int[] returnedIndices = {0, 0};
private int changeOffset = 0;
private int thisStart = 0;
private int tempStart = 0;
private List changeList = new LinkedList();
private AlignmentContext alignmentContext = null;
/**
* The width to be reserved for border and padding at the start of the line.
*/
private int lineStartBAP = 0;
/**
* The width to be reserved for border and padding at the end of the line.
*/
private int lineEndBAP = 0;
private boolean keepTogether;
private final Position auxiliaryPosition = new LeafPosition(this, -1);
/**
* Create a Text layout manager.
*
* @param node The FOText object to be rendered
*/
public TextLayoutManager(FOText node) {
foText = node;
letterSpaceAdjustArray = new MinOptMax[node.length() + 1];
areaInfos = new ArrayList();
}
private KnuthPenalty makeZeroWidthPenalty(int penaltyValue) {
return new KnuthPenalty(0, penaltyValue, false, auxiliaryPosition, true);
}
private KnuthBox makeAuxiliaryZeroWidthBox() {
return new KnuthInlineBox(0, null, notifyPos(new LeafPosition(this, -1)), true);
}
/** {@inheritDoc} */
public void initialize() {
foText.resetBuffer();
spaceFont = FontSelector.selectFontForCharacterInText(' ', foText, this);
// With CID fonts, space isn't necessary currentFontState.width(32)
spaceCharIPD = spaceFont.getCharWidth(' ');
// Use hyphenationChar property
// TODO: Use hyphen based on actual font used!
hyphIPD = foText.getCommonHyphenation().getHyphIPD(spaceFont);
SpaceVal letterSpacing = SpaceVal.makeLetterSpacing(foText.getLetterSpacing());
SpaceVal wordSpacing = SpaceVal.makeWordSpacing(foText.getWordSpacing(), letterSpacing,
spaceFont);
// letter space applies only to consecutive non-space characters,
// while word space applies to space characters;
// i.e. the spaces in the string "A SIMPLE TEST" are:
// A<<ws>>S<ls>I<ls>M<ls>P<ls>L<ls>E<<ws>>T<ls>E<ls>S<ls>T
// there is no letter space after the last character of a word,
// nor after a space character
// NOTE: The above is not quite correct. Read on in XSL 1.0, 7.16.2, letter-spacing
// set letter space and word space dimension;
// the default value "normal" was converted into a MinOptMax value
// in the SpaceVal.makeWordSpacing() method
letterSpaceIPD = letterSpacing.getSpace();
wordSpaceIPD = MinOptMax.getInstance(spaceCharIPD).plus(wordSpacing.getSpace());
keepTogether = foText.getKeepTogether().getWithinLine().getEnum() == Constants.EN_ALWAYS;
}
/**
* Generate and add areas to parent area.
* This can either generate an area for each TextArea and each space, or
* an area containing all text with a parameter controlling the size of
* the word space. The latter is most efficient for PDF generation.
* Set size of each area.
* @param posIter Iterator over Position information returned
* by this LayoutManager.
* @param context LayoutContext for adjustments
*/
public void addAreas(final PositionIterator posIter, final LayoutContext context) {
// Add word areas
AreaInfo areaInfo;
int wordSpaceCount = 0;
int letterSpaceCount = 0;
int firstAreaInfoIndex = -1;
int lastAreaInfoIndex = 0;
MinOptMax realWidth = MinOptMax.ZERO;
/* On first area created, add any leading space.
* Calculate word-space stretch value.
*/
AreaInfo lastAreaInfo = null;
while (posIter.hasNext()) {
final LeafPosition tbpNext = (LeafPosition) posIter.next();
if (tbpNext == null) {
continue; //Ignore elements without Positions
}
if (tbpNext.getLeafPos() != -1) {
areaInfo = (AreaInfo) areaInfos.get(tbpNext.getLeafPos());
if (lastAreaInfo == null
|| ( areaInfo.font != lastAreaInfo.font )
|| ( areaInfo.level != lastAreaInfo.level ) ) {
if (lastAreaInfo != null) {
addAreaInfoAreas(lastAreaInfo, wordSpaceCount,
letterSpaceCount, firstAreaInfoIndex,
lastAreaInfoIndex, realWidth, context);
}
firstAreaInfoIndex = tbpNext.getLeafPos();
wordSpaceCount = 0;
letterSpaceCount = 0;
realWidth = MinOptMax.ZERO;
}
wordSpaceCount += areaInfo.wordSpaceCount;
letterSpaceCount += areaInfo.letterSpaceCount;
realWidth = realWidth.plus(areaInfo.areaIPD);
lastAreaInfoIndex = tbpNext.getLeafPos();
lastAreaInfo = areaInfo;
}
}
if (lastAreaInfo != null) {
addAreaInfoAreas(lastAreaInfo, wordSpaceCount, letterSpaceCount, firstAreaInfoIndex,
lastAreaInfoIndex, realWidth, context);
}
}
private void addAreaInfoAreas(AreaInfo areaInfo, int wordSpaceCount, int letterSpaceCount,
int firstAreaInfoIndex, int lastAreaInfoIndex,
MinOptMax realWidth, LayoutContext context) {
// TODO: These two statements (if, for) were like this before my recent
// changes. However, it seems as if they should use the AreaInfo from
// firstAreaInfoIndex.. lastAreaInfoIndex rather than just the last areaInfo.
// This needs to be checked.
int textLength = areaInfo.getWordLength();
if (areaInfo.letterSpaceCount == textLength && !areaInfo.isHyphenated
&& context.isLastArea()) {
// the line ends at a character like "/" or "-";
// remove the letter space after the last character
realWidth = realWidth.minus(letterSpaceIPD);
letterSpaceCount--;
}
for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) {
MinOptMax letterSpaceAdjustment = letterSpaceAdjustArray[i + 1];
if (letterSpaceAdjustment != null && letterSpaceAdjustment.isElastic()) {
letterSpaceCount++;
}
}
// add hyphenation character if the last word is hyphenated
if (context.isLastArea() && areaInfo.isHyphenated) {
realWidth = realWidth.plus(hyphIPD);
}
/* Calculate adjustments */
double ipdAdjust = context.getIPDAdjust();
// calculate total difference between real and available width
int difference;
if (ipdAdjust > 0.0) {
difference = (int) (realWidth.getStretch() * ipdAdjust);
} else {
difference = (int) (realWidth.getShrink() * ipdAdjust);
}
// set letter space adjustment
int letterSpaceDim = letterSpaceIPD.getOpt();
if (ipdAdjust > 0.0) {
letterSpaceDim += (int) (letterSpaceIPD.getStretch() * ipdAdjust);
} else {
letterSpaceDim += (int) (letterSpaceIPD.getShrink() * ipdAdjust);
}
int totalAdjust = (letterSpaceDim - letterSpaceIPD.getOpt()) * letterSpaceCount;
// set word space adjustment
int wordSpaceDim = wordSpaceIPD.getOpt();
if (wordSpaceCount > 0) {
wordSpaceDim += (difference - totalAdjust) / wordSpaceCount;
}
totalAdjust += (wordSpaceDim - wordSpaceIPD.getOpt()) * wordSpaceCount;
if (totalAdjust != difference) {
// the applied adjustment is greater or smaller than the needed one
TextLayoutManager.LOG
.trace("TextLM.addAreas: error in word / letter space adjustment = "
+ (totalAdjust - difference));
// set totalAdjust = difference, so that the width of the TextArea
// will counterbalance the error and the other inline areas will be
// placed correctly
totalAdjust = difference;
}
TextArea textArea = new TextAreaBuilder(realWidth, totalAdjust, context, firstAreaInfoIndex,
lastAreaInfoIndex, context.isLastArea(), areaInfo.font).build();
// wordSpaceDim is computed in relation to wordSpaceIPD.opt
// but the renderer needs to know the adjustment in relation
// to the size of the space character in the current font;
// moreover, the pdf renderer adds the character spacing even to
// the last character of a word and to space characters: in order
// to avoid this, we must subtract the letter space width twice;
// the renderer will compute the space width as:
// space width =
// = "normal" space width + letterSpaceAdjust + wordSpaceAdjust
// = spaceCharIPD + letterSpaceAdjust +
// + (wordSpaceDim - spaceCharIPD - 2 * letterSpaceAdjust)
// = wordSpaceDim - letterSpaceAdjust
textArea.setTextLetterSpaceAdjust(letterSpaceDim);
textArea.setTextWordSpaceAdjust(wordSpaceDim - spaceCharIPD
- 2 * textArea.getTextLetterSpaceAdjust());
if (context.getIPDAdjust() != 0) {
// add information about space width
textArea.setSpaceDifference(wordSpaceIPD.getOpt() - spaceCharIPD
- 2 * textArea.getTextLetterSpaceAdjust());
}
parentLayoutManager.addChildArea(textArea);
}
private final class TextAreaBuilder {
// constructor initialized state
private final MinOptMax width; // content ipd
private final int adjust; // content ipd adjustment
private final LayoutContext context; // layout context
private final int firstIndex; // index of first AreaInfo
private final int lastIndex; // index of last AreaInfo
private final boolean isLastArea; // true if last inline area in line area
private final Font font; // applicable font
// other, non-constructor state
private TextArea textArea; // text area being constructed
private int blockProgressionDimension; // calculated bpd
private AreaInfo areaInfo; // current area info when iterating over words
private StringBuffer wordChars; // current word's character buffer
private int[] letterSpaceAdjust; // current word's letter space adjustments
private int letterSpaceAdjustIndex; // last written letter space adjustment index
private int[] wordLevels; // current word's bidi levels
private int wordLevelsIndex; // last written bidi level index
private int wordIPD; // accumulated ipd of current word
private int[][] gposAdjustments; // current word's glyph position adjustments
private int gposAdjustmentsIndex; // last written glyph position adjustment index
/**
* Creates a new <code>TextAreaBuilder</code> which itself builds an inline word area. This
* creates a TextArea and sets up the various attributes.
*
* @param width the MinOptMax width of the content
* @param adjust the total ipd adjustment with respect to the optimal width
* @param context the layout context
* @param firstIndex the index of the first AreaInfo used for the TextArea
* @param lastIndex the index of the last AreaInfo used for the TextArea
* @param isLastArea is this TextArea the last in a line?
* @param font Font to be used in this particular TextArea
*/
private TextAreaBuilder(MinOptMax width, int adjust, LayoutContext context,
int firstIndex, int lastIndex, boolean isLastArea, Font font) {
this.width = width;
this.adjust = adjust;
this.context = context;
this.firstIndex = firstIndex;
this.lastIndex = lastIndex;
this.isLastArea = isLastArea;
this.font = font;
}
private TextArea build() {
createTextArea();
setInlineProgressionDimension();
calcBlockProgressionDimension();
setBlockProgressionDimension();
setBaselineOffset();
setBlockProgressionOffset();
setText();
TraitSetter.addFontTraits(textArea, font);
textArea.addTrait(Trait.COLOR, foText.getColor());
TraitSetter.addPtr(textArea, getPtr()); // used for accessibility
TraitSetter.addTextDecoration(textArea, foText.getTextDecoration());
TraitSetter.addFontTraits(textArea, font);
return textArea;
}
/**
* Creates an plain <code>TextArea</code> or a justified <code>TextArea</code> with
* additional information.
*/
private void createTextArea() {
if (context.getIPDAdjust() == 0.0) {
textArea = new TextArea();
} else {
textArea = new TextArea(width.getStretch(), width.getShrink(),
adjust);
}
}
private void setInlineProgressionDimension() {
textArea.setIPD(width.getOpt() + adjust);
}
private void calcBlockProgressionDimension() {
blockProgressionDimension = font.getAscender() - font.getDescender();
}
private void setBlockProgressionDimension() {
textArea.setBPD(blockProgressionDimension);
}
private void setBaselineOffset() {
textArea.setBaselineOffset(font.getAscender());
}
private void setBlockProgressionOffset() {
if (blockProgressionDimension == alignmentContext.getHeight()) {
textArea.setBlockProgressionOffset(0);
} else {
textArea.setBlockProgressionOffset(alignmentContext.getOffset());
}
}
/**
* Sets the text of the TextArea, split into words and spaces.
*/
private void setText() {
int areaInfoIndex = -1;
int wordCharLength = 0;
for (int wordIndex = firstIndex; wordIndex <= lastIndex; wordIndex++) {
areaInfo = getAreaInfo(wordIndex);
if (areaInfo.isSpace) {
addSpaces();
} else {
// areaInfo stores information about a word fragment
if (areaInfoIndex == -1) {
// here starts a new word
areaInfoIndex = wordIndex;
wordCharLength = 0;
}
wordCharLength += areaInfo.getWordLength();
if (isWordEnd(wordIndex)) {
addWord(areaInfoIndex, wordIndex, wordCharLength);
areaInfoIndex = -1;
}
}
}
}
private boolean isWordEnd(int areaInfoIndex) {
return areaInfoIndex == lastIndex || getAreaInfo(areaInfoIndex + 1).isSpace;
}
/**
* Add word with fragments from STARTINDEX to ENDINDEX, where
* total length of (possibly mapped) word is CHARLENGTH.
* A word is composed from one or more word fragments, where each
* fragment corresponds to distinct instance in a sequence of
* area info instances starting at STARTINDEX continuing through (and
* including) ENDINDEX.
* @param startIndex index of first area info of word to add
* @param endIndex index of last area info of word to add
* @param wordLength number of (mapped) characters in word
*/
private void addWord(int startIndex, int endIndex, int wordLength) {
int blockProgressionOffset = 0;
boolean gposAdjusted = false;
if (isHyphenated(endIndex)) {
// TODO may be problematic in some I18N contexts [GA]
wordLength++;
}
initWord(wordLength);
// iterate over word's fragments, adding word chars (with bidi
// levels), letter space adjustments, and glyph position adjustments
for (int i = startIndex; i <= endIndex; i++) {
AreaInfo wordAreaInfo = getAreaInfo(i);
addWordChars(wordAreaInfo);
addLetterAdjust(wordAreaInfo);
if ( addGlyphPositionAdjustments(wordAreaInfo) ) {
gposAdjusted = true;
}
}
if (isHyphenated(endIndex)) {
// TODO may be problematic in some I18N contexts [GA]
addHyphenationChar();
}
if ( !gposAdjusted ) {
gposAdjustments = null;
}
textArea.addWord(wordChars.toString(), wordIPD, letterSpaceAdjust,
getNonEmptyLevels(), gposAdjustments, blockProgressionOffset);
}
private int[] getNonEmptyLevels() {
if ( wordLevels != null ) {
assert wordLevelsIndex <= wordLevels.length;
boolean empty = true;
for ( int i = 0, n = wordLevelsIndex; i < n; i++ ) {
if ( wordLevels [ i ] >= 0 ) {
empty = false;
break;
}
}
return empty ? null : wordLevels;
} else {
return null;
}
}
/**
* Fully allocate word character buffer, letter space adjustments
* array, bidi levels array, and glyph position adjustments array.
* based on full word length, including all (possibly mapped) fragments.
* @param wordLength length of word including all (possibly mapped) fragments
*/
private void initWord(int wordLength) {
wordChars = new StringBuffer(wordLength);
letterSpaceAdjust = new int[wordLength];
letterSpaceAdjustIndex = 0;
wordLevels = new int[wordLength];
wordLevelsIndex = 0;
Arrays.fill ( wordLevels, -1 );
gposAdjustments = new int[wordLength][4];
gposAdjustmentsIndex = 0;
wordIPD = 0;
}
private boolean isHyphenated(int endIndex) {
return isLastArea && endIndex == lastIndex && areaInfo.isHyphenated;
}
private void addHyphenationChar() {
wordChars.append(foText.getCommonHyphenation().getHyphChar(font));
// [TBD] expand bidi word levels, letter space adjusts, gpos adjusts
// [TBD] [GA] problematic in bidi context... what is level of hyphen?
}
/**
* Given a word area info associated with a word fragment,
* (1) concatenate (possibly mapped) word characters to word character buffer;
* (2) concatenante (possibly mapped) word bidi levels to levels buffer;
* (3) update word's IPD with optimal IPD of fragment.
* @param wordAreaInfo fragment info
*/
private void addWordChars(AreaInfo wordAreaInfo) {
int s = wordAreaInfo.startIndex;
int e = wordAreaInfo.breakIndex;
if ( foText.hasMapping ( s, e ) ) {
wordChars.append ( foText.getMapping ( s, e ) );
addWordLevels ( foText.getMappingBidiLevels ( s, e ) );
} else {
for (int i = s; i < e; i++) {
wordChars.append(foText.charAt(i));
}
addWordLevels ( foText.getBidiLevels ( s, e ) );
}
wordIPD += wordAreaInfo.areaIPD.getOpt();
}
/**
* Given a (possibly null) bidi levels array associated with a word fragment,
* concatenante (possibly mapped) word bidi levels to levels buffer.
* @param levels bidi levels array or null
*/
private void addWordLevels ( int[] levels ) {
int numLevels = ( levels != null ) ? levels.length : 0;
if ( numLevels > 0 ) {
int need = wordLevelsIndex + numLevels;
if ( need <= wordLevels.length ) {
System.arraycopy ( levels, 0, wordLevels, wordLevelsIndex, numLevels );
} else {
throw new IllegalStateException
( "word levels array too short: expect at least "
+ need + " entries, but has only " + wordLevels.length + " entries" );
}
}
wordLevelsIndex += numLevels;
}
/**
* Given a word area info associated with a word fragment,
* concatenate letter space adjustments for each (possibly mapped) character.
* @param wordAreaInfo fragment info
*/
private void addLetterAdjust(AreaInfo wordAreaInfo) {
int letterSpaceCount = wordAreaInfo.letterSpaceCount;
int wordLength = wordAreaInfo.getWordLength();
int taAdjust = textArea.getTextLetterSpaceAdjust();
for ( int i = 0, n = wordLength; i < n; i++ ) {
int j = letterSpaceAdjustIndex + i;
if ( j > 0 ) {
int k = wordAreaInfo.startIndex + i;
MinOptMax adj = ( k < letterSpaceAdjustArray.length )
? letterSpaceAdjustArray [ k ] : null;
letterSpaceAdjust [ j ] = ( adj == null ) ? 0 : adj.getOpt();
}
if ( letterSpaceCount > 0 ) {
letterSpaceAdjust [ j ] += taAdjust;
letterSpaceCount--;
}
}
letterSpaceAdjustIndex += wordLength;
}
/**
* Given a word area info associated with a word fragment,
* concatenate glyph position adjustments for each (possibly mapped) character.
* @param wordAreaInfo fragment info
* @return true if an adjustment was non-zero
*/
private boolean addGlyphPositionAdjustments(AreaInfo wordAreaInfo) {
boolean adjusted = false;
int[][] gpa = wordAreaInfo.gposAdjustments;
int numAdjusts = ( gpa != null ) ? gpa.length : 0;
int wordLength = wordAreaInfo.getWordLength();
if ( numAdjusts > 0 ) {
int need = gposAdjustmentsIndex + numAdjusts;
if ( need <= gposAdjustments.length ) {
for ( int i = 0, n = wordLength, j = 0; i < n; i++ ) {
if ( i < numAdjusts ) {
int[] wpa1 = gposAdjustments [ gposAdjustmentsIndex + i ];
int[] wpa2 = gpa [ j++ ];
for ( int k = 0; k < 4; k++ ) {
int a = wpa2 [ k ];
if ( a != 0 ) {
wpa1 [ k ] += a; adjusted = true;
}
}
}
}
} else {
throw new IllegalStateException
( "gpos adjustments array too short: expect at least "
+ need + " entries, but has only " + gposAdjustments.length
+ " entries" );
}
}
gposAdjustmentsIndex += wordLength;
return adjusted;
}
/**
* The <code>AreaInfo</code> stores information about spaces.
* <p/>
* Add the spaces - except zero-width spaces - to the TextArea.
*/
private void addSpaces() {
int blockProgressionOffset = 0;
// [TBD] need to better handling of spaceIPD assignment, for now,
// divide the area info's allocated IPD evenly among the
// non-zero-width space characters
int numZeroWidthSpaces = 0;
for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) {
char spaceChar = foText.charAt(i);
if (CharUtilities.isZeroWidthSpace(spaceChar)) {
numZeroWidthSpaces++;
}
}
int numSpaces = areaInfo.breakIndex - areaInfo.startIndex - numZeroWidthSpaces;
int spaceIPD = areaInfo.areaIPD.getOpt() / ( ( numSpaces > 0 ) ? numSpaces : 1 );
// add space area children, one for each non-zero-width space character
for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) {
char spaceChar = foText.charAt(i);
int level = foText.bidiLevelAt(i);
if (!CharUtilities.isZeroWidthSpace(spaceChar)) {
textArea.addSpace
( spaceChar, spaceIPD,
CharUtilities.isAdjustableSpace(spaceChar),
blockProgressionOffset, level );
}
}
}
}
/**
* used for accessibility
* @return ptr of fobj
*/
private String getPtr() {
FObj fobj = parentLayoutManager.getFObj();
if (fobj instanceof StructurePointerPropertySet) {
return (((StructurePointerPropertySet) fobj).getPtr());
} else {
//No structure pointer applicable
return null;
}
}
private void addAreaInfo ( AreaInfo ai ) {
addAreaInfo ( areaInfos.size(), ai );
}
private void addAreaInfo ( int index, AreaInfo ai ) {
areaInfos.add ( index, ai );
}
private void removeAreaInfo ( int index ) {
areaInfos.remove ( index );
}
private AreaInfo getAreaInfo(int index) {
return (AreaInfo) areaInfos.get(index);
}
private void addToLetterAdjust(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
*/
private static boolean isSpace(final char ch) {
return ch == CharUtilities.SPACE
|| CharUtilities.isNonBreakableSpace(ch)
|| CharUtilities.isFixedWidthSpace(ch);
}
/** {@inheritDoc} */
public List getNextKnuthElements(final LayoutContext context, final int alignment) {
lineStartBAP = context.getLineStartBorderAndPaddingWidth();
lineEndBAP = context.getLineEndBorderAndPaddingWidth();
alignmentContext = context.getAlignmentContext();
final List returnList = new LinkedList();
KnuthSequence sequence = new InlineKnuthSequence();
AreaInfo areaInfo = null;
AreaInfo prevAreaInfo = null;
returnList.add(sequence);
if (LOG.isDebugEnabled()) {
LOG.debug ( "GK: [" + nextStart + "," + foText.length() + "]" );
}
LineBreakStatus lineBreakStatus = new LineBreakStatus();
thisStart = nextStart;
boolean inWord = false;
boolean inWhitespace = false;
char ch = 0;
int level = -1;
int prevLevel = -1;
while (nextStart < foText.length()) {
ch = foText.charAt(nextStart);
level = foText.bidiLevelAt(nextStart);
boolean breakOpportunity = false;
byte breakAction = keepTogether
? LineBreakStatus.PROHIBITED_BREAK
: lineBreakStatus.nextChar(ch);
switch (breakAction) {
case LineBreakStatus.COMBINING_PROHIBITED_BREAK:
case LineBreakStatus.PROHIBITED_BREAK:
break;
case LineBreakStatus.EXPLICIT_BREAK:
break;
case LineBreakStatus.COMBINING_INDIRECT_BREAK:
case LineBreakStatus.DIRECT_BREAK:
case LineBreakStatus.INDIRECT_BREAK:
breakOpportunity = true;
break;
default:
TextLayoutManager.LOG.error("Unexpected breakAction: " + breakAction);
}
if (LOG.isDebugEnabled()) {
LOG.debug ( "GK: {"
+ " index = " + nextStart
+ ", char = " + CharUtilities.charToNCRef ( ch )
+ ", level = " + level
+ ", levelPrev = " + prevLevel
+ ", inWord = " + inWord
+ ", inSpace = " + inWhitespace
+ "}" );
}
if (inWord) {
if ( breakOpportunity
|| TextLayoutManager.isSpace(ch)
|| CharUtilities.isExplicitBreak(ch)
|| ( ( prevLevel != -1 ) && ( level != prevLevel ) ) ) {
// this.foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN
prevAreaInfo = processWord(alignment, sequence, prevAreaInfo, ch,
breakOpportunity, true, prevLevel);
}
} else if (inWhitespace) {
if (ch != CharUtilities.SPACE || breakOpportunity) {
prevAreaInfo = processWhitespace(alignment, sequence,
breakOpportunity, prevLevel);
}
} else {
if (areaInfo != null) {
prevAreaInfo = areaInfo;
processLeftoverAreaInfo(alignment, sequence, areaInfo,
ch == CharUtilities.SPACE || breakOpportunity);
areaInfo = null;
}
if (breakAction == LineBreakStatus.EXPLICIT_BREAK) {
sequence = processLinebreak(returnList, sequence);
}
}
if (ch == CharUtilities.SPACE
&& foText.getWhitespaceTreatment() == Constants.EN_PRESERVE
|| ch == CharUtilities.NBSPACE) {
// preserved space or non-breaking space:
// create the AreaInfo object
areaInfo = new AreaInfo(nextStart, nextStart + 1, 1, 0, wordSpaceIPD, false, true,
breakOpportunity, spaceFont, level, null);
thisStart = nextStart + 1;
} else if (CharUtilities.isFixedWidthSpace(ch) || CharUtilities.isZeroWidthSpace(ch)) {
// create the AreaInfo object
Font font = FontSelector.selectFontForCharacterInText(ch, foText, this);
MinOptMax ipd = MinOptMax.getInstance(font.getCharWidth(ch));
areaInfo = new AreaInfo(nextStart, nextStart + 1, 0, 0, ipd, false, true,
breakOpportunity, font, level, null);
thisStart = nextStart + 1;
} else if (CharUtilities.isExplicitBreak(ch)) {
//mandatory break-character: only advance index
thisStart = nextStart + 1;
}
inWord = !TextLayoutManager.isSpace(ch) && !CharUtilities.isExplicitBreak(ch);
inWhitespace = ch == CharUtilities.SPACE
&& foText.getWhitespaceTreatment() != Constants.EN_PRESERVE;
prevLevel = level;
nextStart++;
}
// Process any last elements
if (inWord) {
processWord(alignment, sequence, prevAreaInfo, ch, false, false, prevLevel);
} else if (inWhitespace) {
processWhitespace(alignment, sequence, !keepTogether, prevLevel);
} else if (areaInfo != null) {
processLeftoverAreaInfo(alignment, sequence, areaInfo,
ch == CharUtilities.ZERO_WIDTH_SPACE);
} else if (CharUtilities.isExplicitBreak(ch)) {
this.processLinebreak(returnList, sequence);
}
if (((List) ListUtil.getLast(returnList)).isEmpty()) {
//Remove an empty sequence because of a trailing newline
ListUtil.removeLast(returnList);
}
setFinished(true);
if (returnList.isEmpty()) {
return null;
} else {
return returnList;
}
}
private KnuthSequence processLinebreak(List returnList, KnuthSequence sequence) {
if (lineEndBAP != 0) {
sequence.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, true));
}
sequence.endSequence();
sequence = new InlineKnuthSequence();
returnList.add(sequence);
return sequence;
}
private void processLeftoverAreaInfo(int alignment,
KnuthSequence sequence, AreaInfo areaInfo,
boolean breakOpportunityAfter) {
addAreaInfo(areaInfo);
areaInfo.breakOppAfter = breakOpportunityAfter;
addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1);
}
private AreaInfo processWhitespace(final int alignment,
final KnuthSequence sequence, final boolean breakOpportunity, int level) {
if (LOG.isDebugEnabled()) {
LOG.debug ( "PS: [" + thisStart + "," + nextStart + "]" );
}
// End of whitespace
// create the AreaInfo object
assert nextStart >= thisStart;
AreaInfo areaInfo = new AreaInfo
( thisStart, nextStart, nextStart - thisStart, 0,
wordSpaceIPD.mult(nextStart - thisStart),
false, true, breakOpportunity, spaceFont, level, null );
addAreaInfo(areaInfo);
// create the elements
addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1);
thisStart = nextStart;
return areaInfo;
}
private AreaInfo processWordMapping
( int lastIndex, final Font font, AreaInfo prevAreaInfo, final char breakOpportunityChar,
final boolean endsWithHyphen, int level ) {
int s = this.thisStart; // start index of word in FOText character buffer
int e = lastIndex; // end index of word in FOText character buffer
int nLS = 0; // # of letter spaces
String script = foText.getScript();
String language = foText.getLanguage();
if (LOG.isDebugEnabled()) {
LOG.debug ( "PW: [" + thisStart + "," + lastIndex + "]: {"
+ " +M"
+ ", level = " + level
+ " }" );
}
// 1. extract unmapped character sequence
CharSequence ics = foText.subSequence ( s, e );
// 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 = CharUtilities.scriptTagFromCode ( CharUtilities.dominantScript ( ics ) );
}
if ( ( language == null ) || "none".equals(language) ) {
language = "dflt";
}
// 3. perform mapping of chars to glyphs ... to glyphs ... to chars
CharSequence mcs = font.performSubstitution ( ics, script, language );
// 4. compute glyph position adjustments on (substituted) characters
int[][] gpa;
if ( font.performsPositioning() ) {
gpa = font.performPositioning ( mcs, script, language );
} else {
gpa = null;
}
// 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 );
// 6. if mapped sequence differs from input sequence, then memoize mapped sequence
if ( !CharUtilities.isSameSequence ( mcs, ics ) ) {
foText.addMapping ( s, e, mcs );
}
// 7. compute word ipd based on final position adjustments
MinOptMax ipd = MinOptMax.ZERO;
for ( int i = 0, n = mcs.length(); i < n; i++ ) {
char c = mcs.charAt ( i );
int w = font.getCharWidth ( c );
if ( gpa != null ) {
w += gpa [ i ] [ GlyphPositioningTable.Value.IDX_X_ADVANCE ];
}
ipd = ipd.plus ( w );
}
// [TBD] - handle kerning - note that standard kerning would only apply in
// the off-chance that a font supports substitution, but does not support
// positioning and yet has kerning data
// if ( ! font.performsPositioning() ) {
// // do standard kerning
// }
// [TBD] - handle letter spacing
return new AreaInfo
( s, e, 0, nLS, ipd, endsWithHyphen, false,
breakOpportunityChar != 0, font, level, gpa );
}
private AreaInfo processWordNoMapping(int lastIndex, final Font font, AreaInfo prevAreaInfo,
final char breakOpportunityChar, final boolean endsWithHyphen, int level) {
boolean kerning = font.hasKerning();
MinOptMax wordIPD = MinOptMax.ZERO;
if (LOG.isDebugEnabled()) {
LOG.debug ( "PW: [" + thisStart + "," + lastIndex + "]: {"
+ " -M"
+ ", level = " + level
+ " }" );
}
for (int i = thisStart; i < lastIndex; i++) {
char currentChar = foText.charAt(i);
//character width
int charWidth = font.getCharWidth(currentChar);
wordIPD = wordIPD.plus(charWidth);
//kerning
if (kerning) {
int kern = 0;
if (i > thisStart) {
char previousChar = foText.charAt(i - 1);
kern = font.getKernValue(previousChar, currentChar);
} else if (prevAreaInfo != null
&& !prevAreaInfo.isSpace && prevAreaInfo.breakIndex > 0) {
char previousChar = foText.charAt(prevAreaInfo.breakIndex - 1);
kern = font.getKernValue(previousChar, currentChar);
}
if (kern != 0) {
addToLetterAdjust(i, kern);
wordIPD = wordIPD.plus(kern);
}
}
}
if (kerning
&& ( breakOpportunityChar != 0 )
&& !TextLayoutManager.isSpace(breakOpportunityChar)
&& lastIndex > 0
&& endsWithHyphen) {
int kern = font.getKernValue(foText.charAt(lastIndex - 1), breakOpportunityChar);
if (kern != 0) {
addToLetterAdjust(lastIndex, 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 = lastIndex - thisStart;
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 ) && !TextLayoutManager.isSpace(breakOpportunityChar)) {
letterSpaces++;
}
}
assert letterSpaces >= 0;
wordIPD = wordIPD.plus(letterSpaceIPD.mult(letterSpaces));
// create and return the AreaInfo object
return new AreaInfo(thisStart, lastIndex, 0,
letterSpaces, wordIPD,
endsWithHyphen,
false, breakOpportunityChar != 0, font, level, null);
}
private AreaInfo processWord(final int alignment, final KnuthSequence sequence,
AreaInfo prevAreaInfo, final char ch, final boolean breakOpportunity,
final boolean checkEndsWithHyphen, int level) {
//Word boundary found, process widths and kerning
int lastIndex = nextStart;
while (lastIndex > 0 && foText.charAt(lastIndex - 1) == CharUtilities.SOFT_HYPHEN) {
lastIndex--;
}
final boolean endsWithHyphen = checkEndsWithHyphen
&& foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN;
Font font = FontSelector.selectFontForCharactersInText
( foText, thisStart, lastIndex, foText, this );
AreaInfo areaInfo;
if ( font.performsSubstitution() || font.performsPositioning() ) {
areaInfo = processWordMapping
( lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level );
} else {
areaInfo = processWordNoMapping
( lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level );
}
prevAreaInfo = areaInfo;
addAreaInfo(areaInfo);
tempStart = nextStart;
//add the elements
addElementsForAWordFragment(sequence, alignment, areaInfo, areaInfos.size() - 1);
thisStart = nextStart;
return prevAreaInfo;
}
/** {@inheritDoc} */
public List addALetterSpaceTo(List oldList) {
return addALetterSpaceTo(oldList, 0);
}
/** {@inheritDoc} */
public List addALetterSpaceTo(final List oldList, int depth) {
// old list contains only a box, or the sequence: box penalty glue box;
// look at the Position stored in the first element in oldList
// which is always a box
ListIterator oldListIterator = oldList.listIterator();
KnuthElement knuthElement = (KnuthElement) oldListIterator.next();
Position pos = knuthElement.getPosition();
LeafPosition leafPos = (LeafPosition) pos.getPosition(depth);
int index = leafPos.getLeafPos();
//element could refer to '-1' position, for non-collapsed spaces (?)
if (index > -1) {
AreaInfo areaInfo = getAreaInfo(index);
areaInfo.letterSpaceCount++;
areaInfo.addToAreaIPD(letterSpaceIPD);
if (TextLayoutManager.BREAK_CHARS.indexOf(foText.charAt(tempStart - 1)) >= 0) {
// the last character could be used as a line break
// append new elements to oldList
oldListIterator = oldList.listIterator(oldList.size());
oldListIterator.add(new KnuthPenalty(0, KnuthPenalty.FLAGGED_PENALTY, true,
auxiliaryPosition, false));
oldListIterator.add(new KnuthGlue(letterSpaceIPD, auxiliaryPosition, false));
} else if (letterSpaceIPD.isStiff()) {
// constant letter space: replace the box
// give it the unwrapped position of the replaced element
oldListIterator.set(new KnuthInlineBox(areaInfo.areaIPD.getOpt(),
alignmentContext, pos, false));
} else {
// adjustable letter space: replace the glue
oldListIterator.next(); // this would return the penalty element
oldListIterator.next(); // this would return the glue element
oldListIterator.set(new KnuthGlue(letterSpaceIPD.mult(areaInfo.letterSpaceCount),
auxiliaryPosition, true));
}
}
return oldList;
}
/** {@inheritDoc} */
public void hyphenate(Position pos, HyphContext hyphContext) {
AreaInfo areaInfo = getAreaInfo(((LeafPosition) pos).getLeafPos() + changeOffset);
int startIndex = areaInfo.startIndex;
int stopIndex;
boolean nothingChanged = true;
Font font = areaInfo.font;
while (startIndex < areaInfo.breakIndex) {
MinOptMax newIPD = MinOptMax.ZERO;
boolean hyphenFollows;
stopIndex = startIndex + hyphContext.getNextHyphPoint();
if (hyphContext.hasMoreHyphPoints() && stopIndex <= areaInfo.breakIndex) {
// stopIndex is the index of the first character
// after a hyphenation point
hyphenFollows = true;
} else {
// there are no more hyphenation points,
// or the next one is after areaInfo.breakIndex
hyphenFollows = false;
stopIndex = areaInfo.breakIndex;
}
hyphContext.updateOffset(stopIndex - startIndex);
//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));
//if (i > startIndex) {
if (i < stopIndex) {
MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1];
if (i == stopIndex - 1 && hyphenFollows) {
//the letter adjust here needs to be handled further down during
//element generation because it depends on hyph/no-hyph condition
letterSpaceAdjust = null;
}
if (letterSpaceAdjust != null) {
newIPD = newIPD.plus(letterSpaceAdjust);
}
}
}
// add letter spaces
boolean isWordEnd
= (stopIndex == areaInfo.breakIndex)
&& (areaInfo.letterSpaceCount < areaInfo.getWordLength());
int letterSpaceCount = isWordEnd ? stopIndex - startIndex - 1 : stopIndex - startIndex;
assert letterSpaceCount >= 0;
newIPD = newIPD.plus(letterSpaceIPD.mult(letterSpaceCount));
if (!(nothingChanged && stopIndex == areaInfo.breakIndex && !hyphenFollows)) {
// the new AreaInfo object is not equal to the old one
changeList.add
( new PendingChange
( new AreaInfo(startIndex, stopIndex, 0,
letterSpaceCount, newIPD, hyphenFollows,
false, false, font, -1, null),
((LeafPosition) pos).getLeafPos() + changeOffset));
nothingChanged = false;
}
startIndex = stopIndex;
}
hasChanged |= !nothingChanged;
}
/** {@inheritDoc} */
public boolean applyChanges(final List oldList) {
return applyChanges(oldList, 0);
}
/** {@inheritDoc} */
public boolean applyChanges(final List oldList, int depth) {
// make sure the LM appears unfinished in between this call
// and the next call to getChangedKnuthElements()
setFinished(false);
if (oldList.isEmpty()) {
return false;
}
// Find the first and last positions in oldList that point to an AreaInfo
// (i.e. getLeafPos() != -1)
LeafPosition startPos = null, endPos = null;
ListIterator oldListIter;
for (oldListIter = oldList.listIterator(); oldListIter.hasNext();) {
Position pos = ((KnuthElement) oldListIter.next()).getPosition();
startPos = (LeafPosition) pos.getPosition(depth);
if (startPos != null && startPos.getLeafPos() != -1) {
break;
}
}
for (oldListIter = oldList.listIterator(oldList.size()); oldListIter.hasPrevious();) {
Position pos = ((KnuthElement) oldListIter.previous()).getPosition();
endPos = (LeafPosition) pos.getPosition(depth);
if (endPos != null && endPos.getLeafPos() != -1) {
break;
}
}
// set start/end index, taking into account any offset due to
// changes applied to previous paragraphs
returnedIndices[0] = (startPos != null ? startPos.getLeafPos() : -1) + changeOffset;
returnedIndices[1] = (endPos != null ? endPos.getLeafPos() : -1) + changeOffset;
int areaInfosAdded = 0;
int areaInfosRemoved = 0;
if (!changeList.isEmpty()) {
int oldIndex = -1, changeIndex;
PendingChange currChange;
ListIterator changeListIterator = changeList.listIterator();
while (changeListIterator.hasNext()) {
currChange = (PendingChange) changeListIterator.next();
if (currChange.index == oldIndex) {
areaInfosAdded++;
changeIndex = currChange.index + areaInfosAdded - areaInfosRemoved;
} else {
areaInfosRemoved++;
areaInfosAdded++;
oldIndex = currChange.index;
changeIndex = currChange.index + areaInfosAdded - areaInfosRemoved;
removeAreaInfo(changeIndex);
}
addAreaInfo(changeIndex, currChange.areaInfo);
}
changeList.clear();
}
// increase the end index for getChangedKnuthElements()
returnedIndices[1] += (areaInfosAdded - areaInfosRemoved);
// increase offset to use for subsequent paragraphs
changeOffset += (areaInfosAdded - areaInfosRemoved);
return hasChanged;
}
/** {@inheritDoc} */
public List getChangedKnuthElements(final List oldList, final int alignment) {
if (isFinished()) {
return null;
}
final LinkedList returnList = new LinkedList();
for (; returnedIndices[0] <= returnedIndices[1]; returnedIndices[0]++) {
AreaInfo areaInfo = getAreaInfo(returnedIndices[0]);
if (areaInfo.wordSpaceCount == 0) {
// areaInfo refers either to a word or a word fragment
addElementsForAWordFragment(returnList, alignment, areaInfo, returnedIndices[0]);
} else {
// areaInfo refers to a space
addElementsForASpace(returnList, alignment, areaInfo, returnedIndices[0]);
}
}
setFinished(returnedIndices[0] == areaInfos.size() - 1);
//ElementListObserver.observe(returnList, "text-changed", null);
return returnList;
}
/** {@inheritDoc} */
public String getWordChars(Position pos) {
int leafValue = ((LeafPosition) pos).getLeafPos() + changeOffset;
if (leafValue != -1) {
AreaInfo areaInfo = getAreaInfo(leafValue);
StringBuffer buffer = new StringBuffer(areaInfo.getWordLength());
for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) {
buffer.append(foText.charAt(i));
}
return buffer.toString();
} else {
return "";
}
}
private void addElementsForASpace(List baseList, int alignment, AreaInfo areaInfo,
int leafValue) {
LeafPosition mainPosition = new LeafPosition(this, leafValue);
if (!areaInfo.breakOppAfter) {
// a non-breaking space
if (alignment == Constants.EN_JUSTIFY) {
// the space can stretch and shrink, and must be preserved
// when starting a line
baseList.add(makeAuxiliaryZeroWidthBox());
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(areaInfo.areaIPD, mainPosition, false));
} else {
// the space does not need to stretch or shrink, and must be
// preserved when starting a line
baseList.add(new KnuthInlineBox(areaInfo.areaIPD.getOpt(), null, mainPosition,
true));
}
} else {
if (foText.charAt(areaInfo.startIndex) != CharUtilities.SPACE
|| foText.getWhitespaceTreatment() == Constants.EN_PRESERVE) {
// a breaking space that needs to be preserved
baseList
.addAll(getElementsForBreakingSpace(alignment, areaInfo, auxiliaryPosition, 0,
mainPosition, areaInfo.areaIPD.getOpt(), true));
} else {
// a (possible block) of breaking spaces
baseList
.addAll(getElementsForBreakingSpace(alignment, areaInfo, mainPosition,
areaInfo.areaIPD.getOpt(), auxiliaryPosition, 0, false));
}
}
}
private List getElementsForBreakingSpace(int alignment, AreaInfo areaInfo, Position pos2,
int p2WidthOffset, Position pos3,
int p3WidthOffset, boolean skipZeroCheck) {
List elements = new ArrayList();
switch (alignment) {
case EN_CENTER:
// centered text:
// if the second element is chosen as a line break these elements
// add a constant amount of stretch at the end of a line and at the
// beginning of the next one, otherwise they don't add any stretch
elements.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
elements.add(makeZeroWidthPenalty(0));
elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP), -6
* LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false));
elements.add(makeAuxiliaryZeroWidthBox());
elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset,
3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos3, false));
break;
case EN_START: // fall through
case EN_END:
// left- or right-aligned text:
// if the second element is chosen as a line break these elements
// add a constant amount of stretch at the end of a line, otherwise
// they don't add any stretch
KnuthGlue g;
if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) {
g = new KnuthGlue
(lineEndBAP,
3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false);
elements.add(g);
elements.add(makeZeroWidthPenalty(0));
g = new KnuthGlue
(p2WidthOffset - (lineStartBAP + lineEndBAP),
-3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false);
elements.add(g);
elements.add(makeAuxiliaryZeroWidthBox());
elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
g = new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false);
elements.add(g);
} else {
g = new KnuthGlue
(0,
3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false);
elements.add(g);
elements.add(makeZeroWidthPenalty(0));
g = new KnuthGlue
(areaInfo.areaIPD.getOpt(),
-3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false);
elements.add(g);
}
break;
case EN_JUSTIFY:
// justified text:
// the stretch and shrink depends on the space width
elements.addAll(getElementsForJustifiedText(areaInfo, pos2, p2WidthOffset, pos3,
p3WidthOffset, skipZeroCheck, areaInfo.areaIPD.getShrink()));
break;
default:
// last line justified, the other lines unjustified:
// use only the space stretch
elements.addAll(getElementsForJustifiedText(areaInfo, pos2, p2WidthOffset, pos3,
p3WidthOffset, skipZeroCheck, 0));
}
return elements;
}
private List getElementsForJustifiedText
(AreaInfo areaInfo, Position pos2, int p2WidthOffset,
Position pos3, int p3WidthOffset, boolean skipZeroCheck,
int shrinkability) {
int stretchability = areaInfo.areaIPD.getStretch();
List elements = new ArrayList();
if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) {
elements.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false));
elements.add(makeZeroWidthPenalty(0));
elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP),
stretchability, shrinkability, pos2, false));
elements.add(makeAuxiliaryZeroWidthBox());
elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false));
} else {
elements.add(new KnuthGlue(areaInfo.areaIPD.getOpt(), stretchability, shrinkability,
pos2, false));
}
return elements;
}
private void addElementsForAWordFragment(List baseList, int alignment, AreaInfo areaInfo,
int leafValue) {
LeafPosition mainPosition = new LeafPosition(this, leafValue);
// if the last character of the word fragment is '-' or '/',
// the fragment could end a line; in this case, it loses one
// of its letter spaces;
boolean suppressibleLetterSpace = areaInfo.breakOppAfter && !areaInfo.isHyphenated;
if (letterSpaceIPD.isStiff()) {
// constant letter spacing
baseList.add(new KnuthInlineBox(suppressibleLetterSpace
? areaInfo.areaIPD.getOpt() - letterSpaceIPD.getOpt()
: areaInfo.areaIPD.getOpt(),
alignmentContext, notifyPos(mainPosition), false));
} else {
// adjustable letter spacing
int unsuppressibleLetterSpaces = suppressibleLetterSpace
? areaInfo.letterSpaceCount - 1
: areaInfo.letterSpaceCount;
baseList.add(new KnuthInlineBox(areaInfo.areaIPD.getOpt()
- areaInfo.letterSpaceCount * letterSpaceIPD.getOpt(),
alignmentContext, notifyPos(mainPosition), false));
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(letterSpaceIPD.mult(unsuppressibleLetterSpaces),
auxiliaryPosition, true));
baseList.add(makeAuxiliaryZeroWidthBox());
}
// extra-elements if the word fragment is the end of a syllable,
// or it ends with a character that can be used as a line break
if (areaInfo.isHyphenated) {
MinOptMax widthIfNoBreakOccurs = null;
if (areaInfo.breakIndex < foText.length()) {
//Add in kerning in no-break condition
widthIfNoBreakOccurs = letterSpaceAdjustArray[areaInfo.breakIndex];
}
//if (areaInfo.breakIndex)
// the word fragment ends at the end of a syllable:
// if a break occurs the content width increases,
// otherwise nothing happens
addElementsForAHyphen(baseList, alignment, hyphIPD, widthIfNoBreakOccurs,
areaInfo.breakOppAfter && areaInfo.isHyphenated);
} else if (suppressibleLetterSpace) {
// the word fragment ends with a character that acts as a hyphen
// if a break occurs the width does not increase,
// otherwise there is one more letter space
addElementsForAHyphen(baseList, alignment, 0, letterSpaceIPD, true);
}
}
private void addElementsForAHyphen(List baseList, int alignment, int widthIfBreakOccurs,
MinOptMax widthIfNoBreakOccurs, boolean unflagged) {
if (widthIfNoBreakOccurs == null) {
widthIfNoBreakOccurs = MinOptMax.ZERO;
}
switch (alignment) {
case EN_CENTER:
// centered text:
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, true));
baseList.add(new KnuthPenalty(hyphIPD, unflagged
? TextLayoutManager.SOFT_HYPHEN_PENALTY
: KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false));
baseList.add(new KnuthGlue(-(lineEndBAP + lineStartBAP),
-6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
baseList.add(makeAuxiliaryZeroWidthBox());
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineStartBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH,
0, auxiliaryPosition, true));
break;
case EN_START: // fall through
case EN_END:
// left- or right-aligned text:
if (lineStartBAP != 0 || lineEndBAP != 0) {
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineEndBAP,
3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
baseList.add(new KnuthPenalty(widthIfBreakOccurs,
unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
: KnuthPenalty.FLAGGED_PENALTY, !unflagged,
auxiliaryPosition, false));
baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt()
- (lineStartBAP + lineEndBAP),
-3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
baseList.add(makeAuxiliaryZeroWidthBox());
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineStartBAP, 0, 0, auxiliaryPosition, false));
} else {
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
baseList.add(new KnuthPenalty(widthIfBreakOccurs,
unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
: KnuthPenalty.FLAGGED_PENALTY, !unflagged,
auxiliaryPosition, false));
baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt(),
-3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
auxiliaryPosition, false));
}
break;
default:
// justified text, or last line justified:
// just a flagged penalty
if (lineStartBAP != 0 || lineEndBAP != 0) {
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false));
baseList.add(new KnuthPenalty(widthIfBreakOccurs,
unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
: KnuthPenalty.FLAGGED_PENALTY, !unflagged,
auxiliaryPosition, false));
// extra elements representing a letter space that is suppressed
// if a break occurs
if (widthIfNoBreakOccurs.isNonZero()) {
baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt()
- (lineStartBAP + lineEndBAP),
widthIfNoBreakOccurs.getStretch(),
widthIfNoBreakOccurs.getShrink(),
auxiliaryPosition, false));
} else {
baseList.add(new KnuthGlue(-(lineStartBAP + lineEndBAP), 0, 0,
auxiliaryPosition, false));
}
baseList.add(makeAuxiliaryZeroWidthBox());
baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
baseList.add(new KnuthGlue(lineStartBAP, 0, 0,
auxiliaryPosition, false));
} else {
baseList.add(new KnuthPenalty(widthIfBreakOccurs,
unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
: KnuthPenalty.FLAGGED_PENALTY, !unflagged,
auxiliaryPosition, false));
// extra elements representing a letter space that is suppressed
// if a break occurs
if (widthIfNoBreakOccurs.isNonZero()) {
baseList.add(new KnuthGlue(widthIfNoBreakOccurs, auxiliaryPosition, false));
}
}
}
}
/** {@inheritDoc} */
public String toString() {
return super.toString() + "{"
+ "chars = \'"
+ CharUtilities.toNCRefs ( new String ( foText.getCharArray(), 0, foText.length() ) )
+ "\'"
+ ", len = " + foText.length()
+ "}";
}
}