blob: 13d6d89adb5843bd521f95c768e44c6a81f56afd [file] [log] [blame]
/*
* Copyright 1999-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.layoutmgr;
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedList;
import java.util.ListIterator;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.flow.Inline;
import org.apache.fop.fonts.Font;
import org.apache.fop.traits.SpaceVal;
import org.apache.fop.area.Trait;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.util.CharUtilities;
import org.apache.fop.traits.MinOptMax;
/**
* LayoutManager for text (a sequence of characters) which generates one
* or more inline areas.
*/
public class TextLayoutManager extends LeafNodeLayoutManager {
/**
* 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 short iStartIndex;
private short iBreakIndex;
private short iWScount;
private short iLScount;
private MinOptMax ipdArea;
private boolean bHyphenated;
public AreaInfo(short iSIndex, short iBIndex, short iWS, short iLS,
MinOptMax ipd, boolean bHyph) {
iStartIndex = iSIndex;
iBreakIndex = iBIndex;
iWScount = iWS;
iLScount = iLS;
ipdArea = ipd;
bHyphenated = bHyph;
}
}
// this class stores information about changes in vecAreaInfo
// which are not yet applied
private class PendingChange {
public AreaInfo ai;
public int index;
public PendingChange(AreaInfo ai, int index) {
this.ai = ai;
this.index = index;
}
}
// Hold all possible breaks for the text in this LM's FO.
private ArrayList vecAreaInfo;
/** Non-space characters on which we can end a line. */
private static final String BREAK_CHARS = "-/" ;
private FOText foText;
private char[] textArray;
private static final char NEWLINE = '\n';
private static final char SPACE = '\u0020'; // Normal space
private static final char NBSPACE = '\u00A0'; // Non-breaking space
private static final char LINEBREAK = '\u2028';
private static final char ZERO_WIDTH_SPACE = '\u200B';
// byte order mark
private static final char ZERO_WIDTH_NOBREAK_SPACE = '\uFEFF';
/** Start index of first character in this parent Area */
private short iAreaStart = 0;
/** Start index of next TextArea */
private short iNextStart = 0;
/** Size since last makeArea call, except for last break */
private MinOptMax ipdTotal;
/** Size including last break possibility returned */
// private MinOptMax nextIPD = new MinOptMax(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;
/** 1/2 of word-spacing value */
private SpaceVal halfWS;
/** Number of space characters after previous possible break position. */
private int iNbSpacesPending;
private Font fs;
private boolean bChanged = false;
private int iReturnedIndex = 0;
private short iThisStart = 0;
private short iTempStart = 0;
private LinkedList changeList = null;
private int textHeight;
private int lead = 0;
private int total = 0;
private int middle = 0;
private int verticalAlignment = EN_BASELINE;
/**
* Create a Text layout manager.
*
* @param node The FOText object to be rendered
*/
public TextLayoutManager(FOText node) {
super();
foText = node;
textArray = new char[node.endIndex - node.startIndex];
System.arraycopy(node.ca, node.startIndex, textArray, 0,
node.endIndex - node.startIndex);
vecAreaInfo = new java.util.ArrayList();
fs = foText.getCommonFont().getFontState(foText.getFOEventHandler().getFontInfo());
// With CID fonts, space isn't neccesary currentFontState.width(32)
spaceCharIPD = fs.getCharWidth(' ');
// Use hyphenationChar property
hyphIPD = fs.getCharWidth(foText.getCommonHyphenation().hyphenationCharacter);
// Make half-space: <space> on either side of a word-space)
SpaceVal ls = SpaceVal.makeLetterSpacing(foText.getLetterSpacing());
SpaceVal ws = SpaceVal.makeWordSpacing(foText.getWordSpacing(), ls, fs);
halfWS = new SpaceVal(MinOptMax.multiply(ws.getSpace(), 0.5),
ws.isConditional(), ws.isForcing(), ws.getPrecedence());
// 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
// set letter space and word space dimension;
// the default value "normal" was converted into a MinOptMax value
// in the SpaceVal.makeWordSpacing() method
letterSpaceIPD = ls.getSpace();
wordSpaceIPD = MinOptMax.add(new MinOptMax(spaceCharIPD), ws.getSpace());
// set text height
textHeight = fs.getAscender()
- fs.getDescender();
// if the text node is son of an inline, set vertical align
if (foText.getParent() instanceof Inline) {
setAlignment(((Inline) foText.getParent()).getVerticalAlign());
}
switch (verticalAlignment) {
case EN_MIDDLE : middle = textHeight / 2 ;
break;
case EN_TOP : // fall through
case EN_BOTTOM : total = textHeight;
break;
case EN_BASELINE: // fall through
default : lead = fs.getAscender();
total = textHeight;
break;
}
}
/**
* Text always generates inline areas.
*
* @return true
*/
public boolean generatesInlineAreas() {
return true;
}
/**
* Get the word characters between two positions.
* This is used when doing hyphenation or other word manipulations.
*
* @param sbChars the string buffer to put the chars into
* @param bp1 the start position
* @param bp2 the end position
*/
public void getWordChars(StringBuffer sbChars, Position bp1,
Position bp2) {
LeafPosition endPos = (LeafPosition) bp2;
AreaInfo ai =
(AreaInfo) vecAreaInfo.get(endPos.getLeafPos());
// Skip all leading spaces for hyphenation
int i;
for (i = ai.iStartIndex;
i < ai.iBreakIndex
&& CharUtilities.isAnySpace(textArray[i]) == true;
i++) {
//nop
}
sbChars.append(new String(textArray, i, ai.iBreakIndex - i));
}
/**
* Return value indicating whether the next area to be generated could
* start a new line. This should only be called in the "START" condition
* if a previous inline BP couldn't end the line.
* Return true if the first character is a potential linebreak character.
*
* @param context the layout context for determining a break
* @return true if can break before this text
*/
public boolean canBreakBefore(LayoutContext context) {
char c = textArray[iNextStart];
return ((c == NEWLINE) || (foText.getWrapOption() == EN_WRAP
&& (CharUtilities.isBreakableSpace(c)
|| (BREAK_CHARS.indexOf(c) >= 0
&& (iNextStart == 0
|| Character.isLetterOrDigit(textArray[iNextStart-1]))))));
}
/**
* Reset position for returning next BreakPossibility.
*
* @param prevPos the position to reset to
*/
public void resetPosition(Position prevPos) {
if (prevPos != null) {
// ASSERT (prevPos.getLM() == this)
if (prevPos.getLM() != this) {
log.error("TextLayoutManager.resetPosition: "
+ "LM mismatch!!!");
}
LeafPosition tbp = (LeafPosition) prevPos;
AreaInfo ai =
(AreaInfo) vecAreaInfo.get(tbp.getLeafPos());
if (ai.iBreakIndex != iNextStart) {
iNextStart = ai.iBreakIndex;
vecAreaInfo.ensureCapacity(tbp.getLeafPos() + 1);
// TODO: reset or recalculate total IPD = sum of all word IPD
// up to the break position
ipdTotal = ai.ipdArea;
setFinished(false);
}
} else {
// Reset to beginning!
vecAreaInfo.clear();
iNextStart = 0;
setFinished(false);
}
}
// TODO: see if we can use normal getNextBreakPoss for this with
// extra hyphenation information in LayoutContext
private boolean getHyphenIPD(HyphContext hc, MinOptMax hyphIPD) {
// Skip leading word-space before calculating count?
boolean bCanHyphenate = true;
int iStopIndex = iNextStart + hc.getNextHyphPoint();
if (textArray.length < iStopIndex) {
iStopIndex = textArray.length;
bCanHyphenate = false;
}
hc.updateOffset(iStopIndex - iNextStart);
for (; iNextStart < iStopIndex; iNextStart++) {
char c = textArray[iNextStart];
hyphIPD.opt += fs.getCharWidth(c);
// letter-space?
}
// Need to include hyphen size too, but don't count it in the
// stored running total, since it would be double counted
// with later hyphenation points
return bCanHyphenate;
}
/**
* Return the next break possibility that fits the constraints.
* @param context An object specifying the flags and input information
* concerning the context of the BreakPoss.
* @return BreakPoss An object containing information about the next
* legal break position or the end of the text run if no break
* was found.
* <p>Assumptions: white-space-treatment and
* linefeed-treatment processing
* are already done, so there are no TAB or RETURN characters remaining.
* white-space-collapse handling is also done
* (but perhaps this shouldn't be true!)
* There may be LINEFEED characters if they weren't converted
* into spaces. A LINEFEED always forces a break.
*/
public BreakPoss getNextBreakPoss(LayoutContext context) {
/* On first call in a new Line, the START_AREA
* flag in LC is set.
*/
int iFlags = 0;
if (context.startsNewArea()) {
/* This could be first call on this LM, or the first call
* in a new (possible) LineArea.
*/
ipdTotal = new MinOptMax(0);
iFlags |= BreakPoss.ISFIRST;
}
/* HANDLE SUPPRESSED LEADING SPACES
* See W3C XSL Rec. 7.16.3.
* Suppress characters whose "suppress-at-line-break" property = "suppress"
* This can only be set on an explicit fo:character object. The default
* behavior is that U+0020 is suppressed; all other character codes are
* retained.
*/
if (context.suppressLeadingSpace()) {
for (; iNextStart < textArray.length
&& textArray[iNextStart] == SPACE; iNextStart++) {
}
// If now at end, nothing to compose here!
if (iNextStart >= textArray.length) {
setFinished(true);
return null; // Or an "empty" BreakPoss?
}
}
/* Start of this TextArea, plus any non-suppressed leading space.
* Collapse any remaining word-space with leading space from
* ancestor FOs.
* Add up other leading space which is counted in the TextArea IPD.
*/
SpaceSpecifier pendingSpace = new SpaceSpecifier(false);
short iThisStart = iNextStart; // Index of first character counted
MinOptMax spaceIPD = new MinOptMax(0); // Extra IPD from word-spacing
// Sum of glyph IPD of all characters in a word, inc. leading space
int wordIPD = 0;
short iWScount = 0; // Count of word spaces
boolean bSawNonSuppressible = false;
for (; iNextStart < textArray.length; iNextStart++) {
char c = textArray[iNextStart];
if (CharUtilities.isAnySpace(c) == false) {
break;
}
if (c == SPACE || c == NBSPACE) {
++iWScount;
// Counted as word-space
if (iNextStart == iThisStart
&& (iFlags & BreakPoss.ISFIRST) != 0) {
// If possible, treat as normal inter-word space
if (context.getLeadingSpace().hasSpaces()) {
context.getLeadingSpace().addSpace(halfWS);
} else {
// Doesn't combine with any other leading spaces
// from ancestors
spaceIPD.add(halfWS.getSpace());
}
} else {
pendingSpace.addSpace(halfWS);
spaceIPD.add(pendingSpace.resolve(false));
}
wordIPD += spaceCharIPD; // Space glyph IPD
pendingSpace.clear();
pendingSpace.addSpace(halfWS);
if (c == NBSPACE) {
bSawNonSuppressible = true;
}
} else {
// If we have letter-space, so we apply this to fixed-
// width spaces (which are not word-space) also?
bSawNonSuppressible = true;
spaceIPD.add(pendingSpace.resolve(false));
pendingSpace.clear();
wordIPD += fs.getCharWidth(c);
}
}
if (iNextStart < textArray.length) {
spaceIPD.add(pendingSpace.resolve(false));
} else {
// This FO ended with spaces. Return the BP
if (!bSawNonSuppressible) {
iFlags |= BreakPoss.ALL_ARE_SUPPRESS_AT_LB;
}
return makeBreakPoss(iThisStart, spaceIPD, wordIPD,
context.getLeadingSpace(), pendingSpace,
iFlags, iWScount);
}
if (context.tryHyphenate()) {
// Get the size of the next syallable
MinOptMax hyphIPD = new MinOptMax(0);
if (getHyphenIPD(context.getHyphContext(), hyphIPD)) {
iFlags |= (BreakPoss.CAN_BREAK_AFTER | BreakPoss.HYPHENATED);
}
wordIPD += hyphIPD.opt;
} else {
// Look for a legal line-break: breakable white-space and certain
// characters such as '-' which can serve as word breaks.
// Don't look for hyphenation points here though
for (; iNextStart < textArray.length; iNextStart++) {
char c = textArray[iNextStart];
// Include any breakable white-space as break char
if ((c == NEWLINE) || (foText.getWrapOption() == EN_WRAP
&& (CharUtilities.isBreakableSpace(c)
|| (BREAK_CHARS.indexOf(c) >= 0 && (iNextStart == 0
|| Character.isLetterOrDigit(textArray[iNextStart-1])))))) {
iFlags |= BreakPoss.CAN_BREAK_AFTER;
if (c != SPACE) {
iNextStart++;
if (c != NEWLINE) {
wordIPD += fs.getCharWidth(c);
} else {
iFlags |= BreakPoss.FORCE;
}
}
// If all remaining characters would be suppressed at
// line-end, set a flag for parent LM.
int iLastChar;
for (iLastChar = iNextStart;
iLastChar < textArray.length
&& textArray[iLastChar] == SPACE; iLastChar++) {
//nop
}
if (iLastChar == textArray.length) {
iFlags |= BreakPoss.REST_ARE_SUPPRESS_AT_LB;
}
return makeBreakPoss(iThisStart, spaceIPD, wordIPD,
context.getLeadingSpace(), null,
iFlags, iWScount);
}
wordIPD += fs.getCharWidth(c);
// Note, if a normal non-breaking space, is it stretchable???
// If so, keep a count of these embedded spaces.
}
}
return makeBreakPoss(iThisStart, spaceIPD, wordIPD,
context.getLeadingSpace(), null,
iFlags, iWScount);
}
private BreakPoss makeBreakPoss(short iWordStart,
MinOptMax spaceIPD, int wordDim,
SpaceSpecifier leadingSpace,
SpaceSpecifier trailingSpace,
int flags, short iWScount) {
MinOptMax ipd = new MinOptMax(wordDim);
ipd.add(spaceIPD);
if (ipdTotal != null) {
ipd.add(ipdTotal); // sum of all words so far in line
}
// Note: break position now stores total size to here
// Position is the index of the info for this word in the vector
BreakPoss bp = new BreakPoss(new LeafPosition(this,
vecAreaInfo.size() - 1));
ipdTotal = ipd;
if ((flags & BreakPoss.HYPHENATED) != 0) {
// Add the hyphen size, but don't change total IPD!
bp.setStackingSize(MinOptMax.add(ipd, new MinOptMax(hyphIPD)));
} else {
bp.setStackingSize(ipd);
}
// TODO: make this correct (see Keiron's vertical alignment code)
bp.setNonStackingSize(new SpaceVal(foText.getLineHeight()).getSpace());
/* Set max ascender and descender (offset from baseline),
* used for calculating the bpd of the line area containing
* this text.
*/
//bp.setDescender(fs.getDescender());
//bp.setAscender(fs.getAscender());
if (iNextStart == textArray.length) {
flags |= BreakPoss.ISLAST;
setFinished(true);
}
bp.setFlag(flags);
if (trailingSpace != null) {
bp.setTrailingSpace(trailingSpace);
} else {
bp.setTrailingSpace(new SpaceSpecifier(false));
}
if (leadingSpace != null) {
bp.setLeadingSpace(leadingSpace);
} else {
bp.setLeadingSpace(new SpaceSpecifier(false));
}
return bp;
}
/**
* 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(PositionIterator posIter, LayoutContext context) {
// Add word areas
AreaInfo ai = null;
int iStart = -1;
int iWScount = 0;
int iLScount = 0;
MinOptMax realWidth = new MinOptMax(0);
/* On first area created, add any leading space.
* Calculate word-space stretch value.
*/
while (posIter.hasNext()) {
LeafPosition tbpNext = (LeafPosition) posIter.next();
//
if (tbpNext.getLeafPos() != -1) {
ai = (AreaInfo) vecAreaInfo.get(tbpNext.getLeafPos());
if (iStart == -1) {
iStart = ai.iStartIndex;
}
iWScount += ai.iWScount;
iLScount += ai.iLScount;
realWidth.add(ai.ipdArea);
}
}
if (ai == null) {
return;
}
// Make an area containing all characters between start and end.
InlineArea word = null;
int adjust = 0;
// ignore newline character
if (textArray[ai.iBreakIndex - 1] == NEWLINE) {
adjust = 1;
}
String str = new String(textArray, iStart,
ai.iBreakIndex - iStart - adjust);
// add hyphenation character if the last word is hyphenated
if (context.isLastArea() && ai.bHyphenated) {
str += foText.getCommonHyphenation().hyphenationCharacter;
realWidth.add(new MinOptMax(hyphIPD));
}
// Calculate adjustments
int iDifference = 0;
int iTotalAdjust = 0;
int iWordSpaceDim = wordSpaceIPD.opt;
int iLetterSpaceDim = letterSpaceIPD.opt;
double dIPDAdjust = context.getIPDAdjust();
double dSpaceAdjust = context.getSpaceAdjust(); // not used
// calculate total difference between real and available width
if (dIPDAdjust > 0.0) {
iDifference = (int) ((double) (realWidth.max - realWidth.opt)
* dIPDAdjust);
} else {
iDifference = (int) ((double) (realWidth.opt - realWidth.min)
* dIPDAdjust);
}
// set letter space adjustment
if (dIPDAdjust > 0.0) {
iLetterSpaceDim
+= (int) ((double) (letterSpaceIPD.max - letterSpaceIPD.opt)
* dIPDAdjust);
} else {
iLetterSpaceDim
+= (int) ((double) (letterSpaceIPD.opt - letterSpaceIPD.min)
* dIPDAdjust);
}
iTotalAdjust += (iLetterSpaceDim - letterSpaceIPD.opt) * iLScount;
// set word space adjustment
//
if (iWScount > 0) {
iWordSpaceDim += (int) ((iDifference - iTotalAdjust) / iWScount);
} else {
// there are no word spaces in this area
}
iTotalAdjust += (iWordSpaceDim - wordSpaceIPD.opt) * iWScount;
TextArea t = createTextArea(str, realWidth.opt + iTotalAdjust,
context);
// iWordSpaceDim 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 +
// + (iWordSpaceDim - spaceCharIPD - 2 * letterSpaceAdjust)
// = iWordSpaceDim - letterSpaceAdjust
t.setTextLetterSpaceAdjust(iLetterSpaceDim);
t.setTextWordSpaceAdjust(iWordSpaceDim - spaceCharIPD
- 2 * t.getTextLetterSpaceAdjust());
word = t;
if (word != null) {
parentLM.addChildArea(word);
}
}
/**
* Create an inline word area.
* This creates a TextArea and sets up the various attributes.
*
* @param str the string for the TextArea
* @param width the width that the TextArea uses
* @param base the baseline position
* @return the new word area
*/
protected TextArea createTextArea(String str, int width, LayoutContext context) {
TextArea textArea = new TextArea();
textArea.setIPD(width);
textArea.setBPD(fs.getAscender() - fs.getDescender());
int bpd = textArea.getBPD();
switch (verticalAlignment) {
case EN_MIDDLE:
textArea.setOffset(context.getMiddleBaseline() + fs.getXHeight() / 2);
break;
case EN_TOP:
textArea.setOffset(context.getTopBaseline() + fs.getAscender());
break;
case EN_BOTTOM:
textArea.setOffset(context.getBottomBaseline() - bpd + fs.getAscender());
break;
case EN_BASELINE:
default:
textArea.setOffset(context.getBaseline());
break;
}
textArea.setTextArea(str);
textArea.addTrait(Trait.FONT_NAME, fs.getFontName());
textArea.addTrait(Trait.FONT_SIZE, new Integer(fs.getFontSize()));
textArea.addTrait(Trait.COLOR, foText.getColor());
TraitSetter.addTextDecoration(textArea, foText.getTextDecoration());
return textArea;
}
/**
* Set the alignment of the inline area.
* @param al the vertical alignment positioning
*/
public void setAlignment(int al) {
verticalAlignment = al;
}
public LinkedList getNextKnuthElements(LayoutContext context,
int alignment) {
LinkedList returnList = new LinkedList();
while (iNextStart < textArray.length) {
if (textArray[iNextStart] == SPACE
|| textArray[iNextStart] == NBSPACE) {
// normal, breaking space
// or non-breaking space
if (textArray[iNextStart] == NBSPACE) {
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, vecAreaInfo.size() - 1),
false));
}
switch (alignment) {
case EN_CENTER :
vecAreaInfo.add
(new AreaInfo(iNextStart, (short) (iNextStart + 1),
(short) 1, (short) 0,
wordSpaceIPD, false));
returnList.add
(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
returnList.add
(new KnuthPenalty(0, 0, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
- 6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthInlineBox(0, 0, 0, 0,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, -1), true));
iNextStart ++;
break;
case EN_START : // fall through
case EN_END :
vecAreaInfo.add
(new AreaInfo(iNextStart, (short) (iNextStart + 1),
(short) 1, (short) 0,
wordSpaceIPD, false));
returnList.add
(new KnuthGlue(0, 3 * wordSpaceIPD.opt, 0,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
returnList.add
(new KnuthPenalty(0, 0, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
- 3 * wordSpaceIPD.opt, 0,
new LeafPosition(this, -1), true));
iNextStart ++;
break;
case EN_JUSTIFY:
vecAreaInfo.add
(new AreaInfo(iNextStart, (short) (iNextStart + 1),
(short) 1, (short) 0,
wordSpaceIPD, false));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
wordSpaceIPD.max - wordSpaceIPD.opt,
wordSpaceIPD.opt - wordSpaceIPD.min,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
iNextStart ++;
break;
default:
vecAreaInfo.add
(new AreaInfo(iNextStart, (short) (iNextStart + 1),
(short) 1, (short) 0,
wordSpaceIPD, false));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
wordSpaceIPD.max - wordSpaceIPD.opt, 0,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
iNextStart ++;
}
} else if (textArray[iNextStart] == NBSPACE) {
// non breaking space
vecAreaInfo.add
(new AreaInfo(iNextStart, (short) (iNextStart + 1),
(short) 1, (short) 0,
wordSpaceIPD, false));
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
wordSpaceIPD.max - wordSpaceIPD.opt,
wordSpaceIPD.opt - wordSpaceIPD.min,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
iNextStart ++;
} else if (textArray[iNextStart] == NEWLINE) {
// linefeed; this can happen when linefeed-treatment="preserve"
// add a penalty item to the list and return
returnList.add
(new KnuthPenalty(0, -KnuthElement.INFINITE,
false, null, false));
iNextStart ++;
return returnList;
} else {
// the beginning of a word
iThisStart = iNextStart;
iTempStart = iNextStart;
MinOptMax wordIPD = new MinOptMax(0);
for (; iTempStart < textArray.length
&& textArray[iTempStart] != SPACE
&& textArray[iTempStart] != NBSPACE
&& textArray[iTempStart] != NEWLINE
&& !(iTempStart > iNextStart
&& alignment == EN_JUSTIFY
&& BREAK_CHARS.indexOf(textArray[iTempStart - 1]) >= 0);
iTempStart++) {
wordIPD.add(fs.getCharWidth(textArray[iTempStart]));
}
int iLetterSpaces = iTempStart - iThisStart - 1;
wordIPD.add(MinOptMax.multiply(letterSpaceIPD, iLetterSpaces));
vecAreaInfo.add
(new AreaInfo(iThisStart, iTempStart, (short) 0,
(short) iLetterSpaces,
wordIPD, false));
if (letterSpaceIPD.min == letterSpaceIPD.max) {
// constant letter space; simply return a box
// whose width includes letter spaces
returnList.add
(new KnuthInlineBox(wordIPD.opt, lead, total, middle,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
} else {
// adjustable letter space;
// some other KnuthElements are needed
returnList.add
(new KnuthInlineBox(wordIPD.opt - iLetterSpaces * letterSpaceIPD.opt,
lead, total, middle,
new LeafPosition(this, vecAreaInfo.size() - 1), false));
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(iLetterSpaces * letterSpaceIPD.opt,
iLetterSpaces * (letterSpaceIPD.max - letterSpaceIPD.opt),
iLetterSpaces * (letterSpaceIPD.opt - letterSpaceIPD.min),
new LeafPosition(this, -1), true));
returnList.add
(new KnuthInlineBox(0, lead, total, middle,
new LeafPosition(this, -1), true));
}
// if the last character is '-' or '/', it could be used as a line end;
// add a flagged penalty element and glue element representing a suppressible
// letter space if the next character is not a space
if (BREAK_CHARS.indexOf(textArray[iTempStart - 1]) >= 0
&& iTempStart < textArray.length
&& textArray[iTempStart] != SPACE
&& textArray[iTempStart] != NBSPACE) {
returnList.add
(new KnuthPenalty(0, KnuthPenalty.FLAGGED_PENALTY, true,
new LeafPosition(this, -1), false));
returnList.add
(new KnuthGlue(letterSpaceIPD.opt,
letterSpaceIPD.max - letterSpaceIPD.opt,
letterSpaceIPD.opt - letterSpaceIPD.min,
new LeafPosition(this, -1), false));
// update the information in the AreaInfo, adding one more letter space
AreaInfo ai = (AreaInfo) vecAreaInfo.get(vecAreaInfo.size() - 1);
ai.iLScount ++;
}
iNextStart = iTempStart;
}
} // end of while
setFinished(true);
if (returnList.size() > 0) {
return returnList;
} else {
return null;
}
}
public List addALetterSpaceTo(List oldList) {
// 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();
LeafPosition pos = (LeafPosition) ((KnuthBox) oldListIterator.next()).getPosition();
AreaInfo ai = (AreaInfo) vecAreaInfo.get(pos.getLeafPos());
ai.iLScount ++;
ai.ipdArea.add(letterSpaceIPD);
if (BREAK_CHARS.indexOf(textArray[iTempStart - 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,
new LeafPosition(this, -1), false));
oldListIterator.add(new KnuthGlue(letterSpaceIPD.opt,
letterSpaceIPD.max - letterSpaceIPD.opt,
letterSpaceIPD.opt - letterSpaceIPD.min,
new LeafPosition(this, -1), false));
} else if (letterSpaceIPD.min == letterSpaceIPD.max) {
// constant letter space: replace the box
oldListIterator.set(new KnuthInlineBox(ai.ipdArea.opt, lead, total, middle, 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(ai.iLScount * letterSpaceIPD.opt,
ai.iLScount * (letterSpaceIPD.max - letterSpaceIPD.opt),
ai.iLScount * (letterSpaceIPD.opt - letterSpaceIPD.min),
new LeafPosition(this, -1), true));
}
return oldList;
}
public void hyphenate(Position pos, HyphContext hc) {
AreaInfo ai
= (AreaInfo) vecAreaInfo.get(((LeafPosition) pos).getLeafPos());
int iStartIndex = ai.iStartIndex;
int iStopIndex;
boolean bNothingChanged = true;
while (iStartIndex < ai.iBreakIndex) {
MinOptMax newIPD = new MinOptMax(0);
boolean bHyphenFollows;
if (hc.hasMoreHyphPoints()
&& (iStopIndex = iStartIndex + hc.getNextHyphPoint())
<= ai.iBreakIndex) {
// iStopIndex is the index of the first character
// after a hyphenation point
bHyphenFollows = true;
} else {
// there are no more hyphenation points,
// or the next one is after ai.iBreakIndex
bHyphenFollows = false;
iStopIndex = ai.iBreakIndex;
}
hc.updateOffset(iStopIndex - iStartIndex);
for (int i = iStartIndex; i < iStopIndex; i++) {
char c = textArray[i];
newIPD.add(new MinOptMax(fs.getCharWidth(c)));
}
// add letter spaces
boolean bIsWordEnd
= iStopIndex == ai.iBreakIndex
&& ai.iLScount < (ai.iBreakIndex - ai.iStartIndex);
newIPD.add(MinOptMax.multiply(letterSpaceIPD,
(bIsWordEnd
? (iStopIndex - iStartIndex - 1)
: (iStopIndex - iStartIndex))));
if (!(bNothingChanged
&& iStopIndex == ai.iBreakIndex
&& bHyphenFollows == false)) {
// the new AreaInfo object is not equal to the old one
if (changeList == null) {
changeList = new LinkedList();
}
changeList.add
(new PendingChange
(new AreaInfo((short) iStartIndex, (short) iStopIndex,
(short) 0,
(short) (bIsWordEnd
? (iStopIndex - iStartIndex - 1)
: (iStopIndex - iStartIndex)),
newIPD, bHyphenFollows),
((LeafPosition) pos).getLeafPos()));
bNothingChanged = false;
}
iStartIndex = iStopIndex;
}
if (!bChanged && !bNothingChanged) {
bChanged = true;
}
}
public boolean applyChanges(List oldList) {
setFinished(false);
if (changeList != null) {
int iAddedAI = 0;
int iRemovedAI = 0;
int iOldIndex = -1;
PendingChange currChange = null;
ListIterator changeListIterator = changeList.listIterator();
while (changeListIterator.hasNext()) {
currChange = (PendingChange) changeListIterator.next();
if (currChange.index != iOldIndex) {
iRemovedAI ++;
iAddedAI ++;
iOldIndex = currChange.index;
vecAreaInfo.remove(currChange.index + iAddedAI - iRemovedAI);
vecAreaInfo.add(currChange.index + iAddedAI - iRemovedAI,
currChange.ai);
} else {
iAddedAI ++;
vecAreaInfo.add(currChange.index + iAddedAI - iRemovedAI,
currChange.ai);
}
}
changeList.clear();
}
iReturnedIndex = 0;
return bChanged;
}
public LinkedList getChangedKnuthElements(List oldList,
/*int flaggedPenalty,*/
int alignment) {
if (isFinished()) {
return null;
}
LinkedList returnList = new LinkedList();
while (iReturnedIndex < vecAreaInfo.size()) {
AreaInfo ai = (AreaInfo) vecAreaInfo.get(iReturnedIndex);
if (ai.iWScount == 0) {
// ai refers either to a word or a word fragment
// if the last character is '-' or '/' and the next character is not a space
// one of the letter spaces must be represented using a penalty and a glue,
// and its width must be subtracted
if (BREAK_CHARS.indexOf(textArray[ai.iBreakIndex - 1]) >= 0
&& ai.iLScount == (ai.iBreakIndex - ai.iStartIndex)) {
ai.ipdArea.add(new MinOptMax(-letterSpaceIPD.min, -letterSpaceIPD.opt, -letterSpaceIPD.max));
}
if (letterSpaceIPD.min == letterSpaceIPD.max) {
returnList.add
(new KnuthInlineBox(ai.ipdArea.opt, lead, total, middle,
new LeafPosition(this, iReturnedIndex), false));
} else {
returnList.add
(new KnuthInlineBox(ai.ipdArea.opt
- ai.iLScount * letterSpaceIPD.opt,
lead, total, middle,
new LeafPosition(this, iReturnedIndex), false));
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(ai.iLScount * letterSpaceIPD.opt,
ai.iLScount * (letterSpaceIPD.max - letterSpaceIPD.opt),
ai.iLScount * (letterSpaceIPD.opt - letterSpaceIPD.min),
new LeafPosition(this, -1), true));
returnList.add
(new KnuthInlineBox(0, 0, 0, 0,
new LeafPosition(this, -1), true));
}
if (ai.bHyphenated) {
returnList.add
(new KnuthPenalty(hyphIPD, KnuthPenalty.FLAGGED_PENALTY, true,
new LeafPosition(this, -1), false));
}
// if the last character is '-' or '/', it could be used as a line end;
// add a flagged penalty element and a glue element representing a suppressible
// letter space if the next character is not a space
if (BREAK_CHARS.indexOf(textArray[ai.iBreakIndex - 1]) >= 0
&& ai.iLScount == (ai.iBreakIndex - ai.iStartIndex)) {
returnList.add
(new KnuthPenalty(0, KnuthPenalty.FLAGGED_PENALTY, true,
new LeafPosition(this, -1), false));
returnList.add
(new KnuthGlue(letterSpaceIPD.opt,
letterSpaceIPD.max - letterSpaceIPD.opt,
letterSpaceIPD.opt - letterSpaceIPD.min,
new LeafPosition(this, -1), false));
}
iReturnedIndex ++;
} else {
// ai refers to a space
if (textArray[ai.iStartIndex] == NBSPACE) {
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, -1),
false));
}
switch (alignment) {
case EN_CENTER :
returnList.add
(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, iReturnedIndex), false));
returnList.add
(new KnuthPenalty(0, 0, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
- 6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthInlineBox(0, 0, 0, 0,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthPenalty(0, KnuthElement.INFINITE, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, -1), true));
iReturnedIndex ++;
break;
case EN_START : // fall through
case EN_END :
returnList.add
(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, iReturnedIndex), false));
returnList.add
(new KnuthPenalty(0, 0, false,
new LeafPosition(this, -1), true));
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
- 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
new LeafPosition(this, -1), true));
iReturnedIndex ++;
break;
case EN_JUSTIFY:
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
wordSpaceIPD.max - wordSpaceIPD.opt,
wordSpaceIPD.opt - wordSpaceIPD.min,
new LeafPosition(this, iReturnedIndex), false));
iReturnedIndex ++;
break;
default:
returnList.add
(new KnuthGlue(wordSpaceIPD.opt,
wordSpaceIPD.max - wordSpaceIPD.opt, 0,
new LeafPosition(this, iReturnedIndex), false));
iReturnedIndex ++;
}
}
} // end of while
setFinished(true);
return returnList;
}
public void getWordChars(StringBuffer sbChars, Position pos) {
int iLeafValue = ((LeafPosition) pos).getLeafPos();
if (iLeafValue != -1) {
AreaInfo ai = (AreaInfo) vecAreaInfo.get(iLeafValue);
sbChars.append(new String(textArray, ai.iStartIndex,
ai.iBreakIndex - ai.iStartIndex));
}
}
}