/*
* $Id: LineArea.java,v 1.53.2.5 2002/02/11 00:43:45 chrisg Exp $
* Copyright (C) 2001 The Apache Software Foundation. All rights reserved.
* For details on use and redistribution please refer to the
* LICENSE file included with these sources.
*/
package org.apache.fop.layout;
// fop
import org.apache.fop.render.Renderer;
import org.apache.fop.messaging.MessageHandler;
import org.apache.fop.layout.inline.*;
import org.apache.fop.datatypes.IDNode;
import org.apache.fop.fo.properties.WrapOption;
import org.apache.fop.fo.properties.WhiteSpaceCollapse;
import org.apache.fop.fo.properties.TextAlign;
import org.apache.fop.fo.properties.TextAlignLast;
import org.apache.fop.fo.properties.LeaderPattern;
import org.apache.fop.fo.properties.Hyphenate;
import org.apache.fop.fo.properties.CountryMaker;
import org.apache.fop.fo.properties.LanguageMaker;
import org.apache.fop.fo.properties.LeaderAlignment;
import org.apache.fop.fo.properties.VerticalAlign;
import org.apache.fop.layout.hyphenation.Hyphenation;
import org.apache.fop.layout.hyphenation.Hyphenator;
import org.apache.fop.configuration.Configuration;
// java
import java.util.Vector;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.awt.Rectangle;
public class LineArea extends Area {
protected int lineHeight;
protected int halfLeading;
protected int nominalFontSize;
protected int nominalGlyphHeight;
protected int allocationHeight;
protected int startIndent;
protected int endIndent;
private int placementOffset;
private FontState currentFontState; // not the nominal, which is
// in this.fontState
private float red, green, blue;
private int wrapOption;
private int whiteSpaceCollapse;
int vAlign;
/* hyphenation */
HyphenationProps hyphProps;
/*
* the width of text that has definitely made it into the line
* area
*/
protected int finalWidth = 0;
/* the position to shift a link rectangle in order to compensate for links embedded within a word */
protected int embeddedLinkStart = 0;
/* the width of the current word so far */
// protected int wordWidth = 0;
/* values that prev (below) may take */
protected static final int NOTHING = 0;
protected static final int WHITESPACE = 1;
protected static final int TEXT = 2;
protected static final int MULTIBYTECHAR = 3;
/* the character type of the previous character */
protected int prev = NOTHING;
/* the position in data[] of the start of the current word */
// protected int wordStart;
/* the length (in characters) of the current word */
// protected int wordLength = 0;
/* width of spaces before current word */
protected int spaceWidth = 0;
/*
* the inline areas that have not yet been added to the line
* because subsequent characters to come (in a different addText)
* may be part of the same word
*/
protected Vector pendingAreas = new Vector();
/* the width of the pendingAreas */
protected int pendingWidth = 0;
/* text-decoration of the previous text */
protected boolean prevUlState = false;
protected boolean prevOlState = false;
protected boolean prevLTState = false;
public LineArea(FontState fontState, int lineHeight, int halfLeading,
int allocationWidth, int startIndent, int endIndent,
LineArea prevLineArea) {
super(fontState);
this.currentFontState = fontState;
this.lineHeight = lineHeight;
this.nominalFontSize = fontState.getFontSize();
this.nominalGlyphHeight = fontState.getAscender()
- fontState.getDescender();
this.placementOffset = fontState.getAscender();
this.contentRectangleWidth = allocationWidth - startIndent
- endIndent;
this.fontState = fontState;
this.allocationHeight = this.nominalGlyphHeight;
this.halfLeading = this.lineHeight - this.allocationHeight;
this.startIndent = startIndent;
this.endIndent = endIndent;
if (prevLineArea != null) {
Enumeration e = prevLineArea.pendingAreas.elements();
Box b = null;
// There might be InlineSpaces at the beginning
// that should not be there - eat them
boolean eatMoreSpace = true;
int eatenWidth = 0;
while (eatMoreSpace) {
if (e.hasMoreElements()) {
b = (Box)e.nextElement();
if (b instanceof InlineSpace) {
InlineSpace is = (InlineSpace)b;
if (is.isEatable())
eatenWidth += is.getSize();
else
eatMoreSpace = false;
} else {
eatMoreSpace = false;
}
} else {
eatMoreSpace = false;
b = null;
}
}
while (b != null) {
pendingAreas.addElement(b);
if (e.hasMoreElements())
b = (Box)e.nextElement();
else
b = null;
}
pendingWidth = prevLineArea.getPendingWidth() - eatenWidth;
}
}
public void render(Renderer renderer) {
renderer.renderLineArea(this);
}
public int addPageNumberCitation(String refid, LinkSet ls) {
/*
* We should add code here to handle the case where the page number doesn't fit on the current line
*/
// Space must be alloted to the page number, so currently we give it 3 spaces
int width = currentFontState.width(currentFontState.mapChar(' '));
PageNumberInlineArea pia = new PageNumberInlineArea(currentFontState,
this.red, this.green, this.blue, refid, width);
pia.setYOffset(placementOffset);
pendingAreas.addElement(pia);
pendingWidth += width;
prev = TEXT;
return -1;
}
/**
* adds text to line area
*
* @return int character position
*/
public int addText(char odata[], int start, int end, LinkSet ls,
TextState textState) {
// this prevents an array index out of bounds
// which occurs when some text is laid out again.
if (start == -1)
return -1;
boolean overrun = false;
int wordStart = start;
int wordLength = 0;
int wordWidth = 0;
// With CID fonts, space isn't neccesary currentFontState.width(32)
int whitespaceWidth = getCharWidth(' ');
char[] data = new char[odata.length];
char[] dataCopy = new char[odata.length];
System.arraycopy(odata, 0, data, 0, odata.length);
System.arraycopy(odata, 0, dataCopy, 0, odata.length);
boolean isText = false;
boolean isMultiByteChar = false;
/* iterate over each character */
for (int i = start; i < end; i++) {
int charWidth;
/* get the character */
char c = data[i];
if (!(isSpace(c) || (c == '\n') || (c == '\r') || (c == '\t')
|| (c == '\u2028'))) {
charWidth = getCharWidth(c);
isText = true;
isMultiByteChar = (c > 127);
// Add support for zero-width spaces
if (charWidth <= 0 && c != '\u200B' && c != '\uFEFF')
charWidth = whitespaceWidth;
} else {
if ((c == '\n') || (c == '\r') || (c == '\t'))
charWidth = whitespaceWidth;
else
charWidth = getCharWidth(c);
isText = false;
isMultiByteChar = false;
if (prev == WHITESPACE) {
// if current & previous are WHITESPACE
if (this.whiteSpaceCollapse == WhiteSpaceCollapse.FALSE) {
if (isSpace(c)) {
spaceWidth += getCharWidth(c);
} else if (c == '\n' || c == '\u2028') {
// force line break
if (spaceWidth > 0) {
InlineSpace is = new InlineSpace(spaceWidth);
is.setUnderlined(textState.getUnderlined());
is.setOverlined(textState.getOverlined());
is.setLineThrough(textState.getLineThrough());
addChild(is);
finalWidth += spaceWidth;
spaceWidth = 0;
}
return i + 1;
} else if (c == '\t') {
spaceWidth += 8 * whitespaceWidth;
}
} else if (c == '\u2028') {
// Line separator
// Breaks line even if WhiteSpaceCollapse = True
if (spaceWidth > 0) {
InlineSpace is = new InlineSpace(spaceWidth);
is.setUnderlined(textState.getUnderlined());
is.setOverlined(textState.getOverlined());
is.setLineThrough(textState.getLineThrough());
addChild(is);
finalWidth += spaceWidth;
spaceWidth = 0;
}
return i + 1;
}
} else if (prev == TEXT || prev == MULTIBYTECHAR ) {
// if current is WHITESPACE and previous TEXT
// the current word made it, so
// add the space before the current word (if there
// was some)
if (spaceWidth > 0) {
InlineSpace is = new InlineSpace(spaceWidth);
if (prevUlState) {
is.setUnderlined(textState.getUnderlined());
}
if (prevOlState) {
is.setOverlined(textState.getOverlined());
}
if (prevLTState) {
is.setLineThrough(textState.getLineThrough());
}
addChild(is);
finalWidth += spaceWidth;
spaceWidth = 0;
}
// add any pending areas
Enumeration e = pendingAreas.elements();
while (e.hasMoreElements()) {
Box box = (Box)e.nextElement();
if (box instanceof InlineArea) {
if (ls != null) {
Rectangle lr =
new Rectangle(finalWidth, 0,
((InlineArea)box).getContentWidth(),
fontState.getFontSize());
ls.addRect(lr, this, (InlineArea)box);
}
}
addChild(box);
}
finalWidth += pendingWidth;
// reset pending areas array
pendingWidth = 0;
pendingAreas = new Vector();
// add the current word
if (wordLength > 0) {
// The word might contain nonbreaking
// spaces. Split the word and add InlineSpace
// as necessary. All spaces inside the word
// Have a fixed width.
addSpacedWord(new String(data, wordStart, wordLength),
ls, finalWidth, 0, textState, false);
finalWidth += wordWidth;
// reset word width
wordWidth = 0;
}
// deal with this new whitespace following the
// word we just added
prev = WHITESPACE;
embeddedLinkStart =
0; // reset embeddedLinkStart since a space was encountered
spaceWidth = getCharWidth(c);
/*
* here is the place for white-space-treatment value 'ignore':
* if (this.spaceTreatment ==
* SpaceTreatment.IGNORE) {
* // do nothing
* } else {
* spaceWidth = currentFontState.width(32);
* }
*/
if (this.whiteSpaceCollapse == WhiteSpaceCollapse.FALSE) {
if (c == '\n' || c == '\u2028') {
// force a line break
return i + 1;
} else if (c == '\t') {
spaceWidth = whitespaceWidth;
}
} else if (c == '\u2028') {
return i + 1;
}
} else {
// if current is WHITESPACE and no previous
if (this.whiteSpaceCollapse == WhiteSpaceCollapse.FALSE) {
if (isSpace(c)) {
prev = WHITESPACE;
spaceWidth = getCharWidth(c);
} else if (c == '\n') {
// force line break
// textdecoration not used because spaceWidth is 0
InlineSpace is = new InlineSpace(spaceWidth);
addChild(is);
return i + 1;
} else if (c == '\t') {
prev = WHITESPACE;
spaceWidth = 8 * whitespaceWidth;
}
} else {
// skip over it
wordStart++;
}
}
}
if (isText) { // current is TEXT
int curr = isMultiByteChar ? MULTIBYTECHAR : TEXT;
if (prev == WHITESPACE) {
// if current is TEXT and previous WHITESPACE
wordWidth = charWidth;
if ((finalWidth + spaceWidth + wordWidth)
> this.getContentWidth()) {
if (overrun)
MessageHandler.log("area contents overflows area");
if (this.wrapOption == WrapOption.WRAP) {
return i;
}
}
prev = curr;
wordStart = i;
wordLength = 1;
} else if (prev == TEXT || prev == MULTIBYTECHAR ) {
if ( prev == TEXT && curr == TEXT || ! canBreakMidWord()) {
wordLength++;
wordWidth += charWidth;
} else {
// if (spaceWidth > 0) { // for text-align="justify"
InlineSpace is = new InlineSpace(spaceWidth);
if (prevUlState) {
is.setUnderlined(textState.getUnderlined());
}
if (prevOlState) {
is.setOverlined(textState.getOverlined());
}
if (prevLTState) {
is.setLineThrough(textState.getLineThrough());
}
addChild(is);
finalWidth += spaceWidth;
spaceWidth = 0;
// }
// add any pending areas
Enumeration e = pendingAreas.elements();
while (e.hasMoreElements()) {
Box box = (Box)e.nextElement();
if (box instanceof InlineArea) {
if (ls != null) {
Rectangle lr =
new Rectangle(finalWidth, 0,
((InlineArea)box).getContentWidth(),
fontState.getFontSize());
ls.addRect(lr, this, (InlineArea)box);
}
}
addChild(box);
}
finalWidth += pendingWidth;
// reset pending areas array
pendingWidth = 0;
pendingAreas = new Vector();
// add the current word
if (wordLength > 0) {
// The word might contain nonbreaking
// spaces. Split the word and add InlineSpace
// as necessary. All spaces inside the word
// Have a fixed width.
addSpacedWord(new String(data, wordStart, wordLength),
ls, finalWidth, 0, textState, false);
finalWidth += wordWidth;
}
spaceWidth = 0;
wordStart = i;
wordLength = 1;
wordWidth = charWidth;
}
prev = curr;
} else { // nothing previous
prev = curr;
wordStart = i;
wordLength = 1;
wordWidth = charWidth;
}
if ((finalWidth + spaceWidth + pendingWidth + wordWidth)
> this.getContentWidth()) {
// BREAK MID WORD
/* if (canBreakMidWord()) {
addSpacedWord(new String(data, wordStart, wordLength - 1),
ls,
finalWidth + spaceWidth
+ embeddedLinkStart, spaceWidth,
textState, false);
finalWidth += wordWidth;
wordWidth = 0;
return i;
}
*/
if (this.wrapOption == WrapOption.WRAP) {
if (hyphProps.hyphenate == Hyphenate.TRUE) {
int ret = wordStart;
ret = this.doHyphenation(dataCopy, i, wordStart,
this.getContentWidth()
- (finalWidth
+ spaceWidth
+ pendingWidth));
// current word couldn't be hypenated
// couldn't fit first word
// I am at the beginning of my line
if ((ret == wordStart) &&
(wordStart == start) &&
(finalWidth == 0)) {
MessageHandler.log("area contents overflows area");
addSpacedWord(new String(data, wordStart, wordLength - 1),
ls,
finalWidth + spaceWidth
+ embeddedLinkStart,
spaceWidth, textState, false);
finalWidth += wordWidth;
wordWidth = 0;
ret = i;
}
return ret;
} else if (wordStart == start) {
// first word
overrun = true;
// if not at start of line, return word start
// to try again on a new line
if (finalWidth > 0) {
return wordStart;
}
} else {
return wordStart;
}
}
}
}
} // end of iteration over text
if (prev == TEXT || prev == MULTIBYTECHAR) {
if (spaceWidth > 0) {
InlineSpace pis = new InlineSpace(spaceWidth);
// Make sure that this space doesn't occur as
// first thing in the next line
pis.setEatable(true);
if (prevUlState) {
pis.setUnderlined(textState.getUnderlined());
}
if (prevOlState) {
pis.setOverlined(textState.getOverlined());
}
if (prevLTState) {
pis.setLineThrough(textState.getLineThrough());
}
pendingAreas.addElement(pis);
pendingWidth += spaceWidth;
spaceWidth = 0;
}
addSpacedWord(new String(data, wordStart, wordLength), ls,
finalWidth + spaceWidth + embeddedLinkStart,
spaceWidth, textState, true);
embeddedLinkStart += wordWidth;
wordWidth = 0;
}
if (overrun)
MessageHandler.log("area contents overflows area");
return -1;
}
/**
* adds a Leader; actually the method receives the leader properties
* and creates a leader area or an inline area which is appended to
* the children of the containing line area. <br>
* leader pattern use-content is not implemented.
*/
public void addLeader(int leaderPattern, int leaderLengthMinimum,
int leaderLengthOptimum, int leaderLengthMaximum,
int ruleStyle, int ruleThickness,
int leaderPatternWidth, int leaderAlignment) {
WordArea leaderPatternArea;
int leaderLength = 0;
char dotIndex = '.'; // currentFontState.mapChar('.');
int dotWidth =
currentFontState.width(currentFontState.mapChar(dotIndex));
char whitespaceIndex = ' '; // currentFontState.mapChar(' ');
int whitespaceWidth =
currentFontState.width(currentFontState.mapChar(whitespaceIndex));
int remainingWidth = this.getRemainingWidth();
/**
* checks whether leaderLenghtOptimum fits into rest of line;
* should never overflow, as it has been checked already in BlockArea
* first check: use remaining width if it smaller than optimum oder maximum
*/
if ((remainingWidth <= leaderLengthOptimum)
|| (remainingWidth <= leaderLengthMaximum)) {
leaderLength = remainingWidth;
} else if ((remainingWidth > leaderLengthOptimum)
&& (remainingWidth > leaderLengthMaximum)) {
leaderLength = leaderLengthMaximum;
} else if ((leaderLengthOptimum > leaderLengthMaximum)
&& (leaderLengthOptimum < remainingWidth)) {
leaderLength = leaderLengthOptimum;
}
// stop if leader-length is too small
if (leaderLength <= 0) {
return;
}
switch (leaderPattern) {
case LeaderPattern.SPACE:
InlineSpace spaceArea = new InlineSpace(leaderLength);
pendingAreas.addElement(spaceArea);
break;
case LeaderPattern.RULE:
LeaderArea leaderArea = new LeaderArea(fontState, red, green,
blue, "", leaderLength,
leaderPattern,
ruleThickness, ruleStyle);
leaderArea.setYOffset(placementOffset);
pendingAreas.addElement(leaderArea);
break;
case LeaderPattern.DOTS:
// if the width of a dot is larger than leader-pattern-width
// ignore this property
if (leaderPatternWidth < dotWidth) {
leaderPatternWidth = 0;
}
// if value of leader-pattern-width is 'use-font-metrics' (0)
if (leaderPatternWidth == 0) {
pendingAreas.addElement(this.buildSimpleLeader(dotIndex,
leaderLength));
} else {
// if leader-alignment is used, calculate space to insert before leader
// so that all dots will be parallel.
if (leaderAlignment == LeaderAlignment.REFERENCE_AREA) {
int spaceBeforeLeader =
this.getLeaderAlignIndent(leaderLength,
leaderPatternWidth);
// appending indent space leader-alignment
// setting InlineSpace to false, so it is not used in line justification
if (spaceBeforeLeader != 0) {
pendingAreas.addElement(new InlineSpace(spaceBeforeLeader,
false));
pendingWidth += spaceBeforeLeader;
// shorten leaderLength, otherwise - in case of
// leaderLength=remaining length - it will cut off the end of
// leaderlength
leaderLength -= spaceBeforeLeader;
}
}
// calculate the space to insert between the dots and create a
// inline area with this width
InlineSpace spaceBetweenDots =
new InlineSpace(leaderPatternWidth - dotWidth, false);
leaderPatternArea = new WordArea(currentFontState, this.red,
this.green, this.blue,
new String("."), dotWidth);
leaderPatternArea.setYOffset(placementOffset);
int dotsFactor =
(int)Math.floor(((double)leaderLength)
/ ((double)leaderPatternWidth));
// add combination of dot + space to fill leader
// is there a way to do this in a more effective way?
for (int i = 0; i < dotsFactor; i++) {
pendingAreas.addElement(leaderPatternArea);
pendingAreas.addElement(spaceBetweenDots);
}
// append at the end some space to fill up to leader length
pendingAreas.addElement(new InlineSpace(leaderLength
- dotsFactor
* leaderPatternWidth));
}
break;
// leader pattern use-content not implemented.
case LeaderPattern.USECONTENT:
MessageHandler.errorln("leader-pattern=\"use-content\" not "
+ "supported by this version of Fop");
return;
}
// adds leader length to length of pending inline areas
pendingWidth += leaderLength;
// sets prev to TEXT and makes so sure, that also blocks only
// containing leaders are processed
prev = TEXT;
}
/**
* adds pending inline areas to the line area
* normally done, when the line area is filled and
* added as child to the parent block area
*/
public void addPending() {
if (spaceWidth > 0) {
addChild(new InlineSpace(spaceWidth));
finalWidth += spaceWidth;
spaceWidth = 0;
}
Enumeration e = pendingAreas.elements();
while (e.hasMoreElements()) {
Box box = (Box)e.nextElement();
addChild(box);
}
finalWidth += pendingWidth;
// reset pending areas array
pendingWidth = 0;
pendingAreas = new Vector();
}
/**
* aligns line area
*
*/
public void align(int type) {
int padding = 0;
switch (type) {
case TextAlign.START: // left
padding = this.getContentWidth() - finalWidth;
endIndent += padding;
break;
case TextAlign.END: // right
padding = this.getContentWidth() - finalWidth;
startIndent += padding;
break;
case TextAlign.CENTER: // center
padding = (this.getContentWidth() - finalWidth) / 2;
startIndent += padding;
endIndent += padding;
break;
case TextAlign.JUSTIFY: // justify
// first pass - count the spaces
int spaceCount = 0;
Enumeration e = children.elements();
while (e.hasMoreElements()) {
Box b = (Box)e.nextElement();
if (b instanceof InlineSpace) {
InlineSpace space = (InlineSpace)b;
if (space.getResizeable()) {
spaceCount++;
}
}
}
if (spaceCount > 0) {
padding = (this.getContentWidth() - finalWidth) / spaceCount;
} else { // no spaces
padding = 0;
}
// second pass - add additional space
spaceCount = 0;
e = children.elements();
while (e.hasMoreElements()) {
Box b = (Box)e.nextElement();
if (b instanceof InlineSpace) {
InlineSpace space = (InlineSpace)b;
if (space.getResizeable()) {
space.setSize(space.getSize() + padding);
spaceCount++;
}
} else if (b instanceof InlineArea) {
((InlineArea)b).setXOffset(spaceCount * padding);
}
}
}
}
/**
* Balance (vertically) the inline areas within this line.
*/
public void verticalAlign() {
int superHeight = -this.placementOffset;
int maxHeight = this.allocationHeight;
Enumeration e = children.elements();
while (e.hasMoreElements()) {
Box b = (Box)e.nextElement();
if (b instanceof InlineArea) {
InlineArea ia = (InlineArea)b;
if (ia instanceof WordArea) {
ia.setYOffset(placementOffset);
}
if (ia.getHeight() > maxHeight) {
maxHeight = ia.getHeight();
}
int vert = ia.getVerticalAlign();
if (vert == VerticalAlign.SUPER) {
int fh = fontState.getAscender();
ia.setYOffset((int)(placementOffset - (2 * fh / 3.0)));
} else if (vert == VerticalAlign.SUB) {
int fh = fontState.getAscender();
ia.setYOffset((int)(placementOffset + (2 * fh / 3.0)));
}
} else {}
}
// adjust the height of this line to the
// resulting alignment height.
this.allocationHeight = maxHeight;
}
public void changeColor(float red, float green, float blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
public void changeFont(FontState fontState) {
this.currentFontState = fontState;
}
public void changeWhiteSpaceCollapse(int whiteSpaceCollapse) {
this.whiteSpaceCollapse = whiteSpaceCollapse;
}
public void changeWrapOption(int wrapOption) {
this.wrapOption = wrapOption;
}
public void changeVerticalAlign(int vAlign) {
this.vAlign = vAlign;
}
public int getEndIndent() {
return endIndent;
}
public int getHeight() {
return this.allocationHeight;
}
public int getPlacementOffset() {
return this.placementOffset;
}
public int getStartIndent() {
return startIndent;
}
public boolean isEmpty() {
return !(pendingAreas.size() > 0 || children.size() > 0);
// return (prev == NOTHING);
}
public Vector getPendingAreas() {
return pendingAreas;
}
public int getPendingWidth() {
return pendingWidth;
}
public void setPendingAreas(Vector areas) {
pendingAreas = areas;
}
public void setPendingWidth(int width) {
pendingWidth = width;
}
/**
* sets hyphenation related traits: language, country, hyphenate, hyphenation-character
* and minimum number of character to remain one the previous line and to be on the
* next line.
*/
public void changeHyphenation(HyphenationProps hyphProps) {
this.hyphProps = hyphProps;
}
/**
* creates a leader as String out of the given char and the leader length
* and wraps it in an InlineArea which is returned
*/
private InlineArea buildSimpleLeader(char c, int leaderLength) {
int width = this.currentFontState.width(currentFontState.mapChar(c));
if (width == 0) {
MessageHandler.errorln("char " + c
+ " has width 0. Using width 100 instead.");
width = 100;
}
int factor = (int)Math.floor(leaderLength / width);
char[] leaderChars = new char[factor];
for (int i = 0; i < factor; i++) {
leaderChars[i] = c; // currentFontState.mapChar(c);
}
WordArea leaderPatternArea = new WordArea(currentFontState, this.red,
this.green, this.blue,
new String(leaderChars),
leaderLength);
leaderPatternArea.setYOffset(placementOffset);
return leaderPatternArea;
}
/**
* calculates the width of space which has to be inserted before the
* start of the leader, so that all leader characters are aligned.
* is used if property leader-align is set. At the moment only the value
* for leader-align="reference-area" is supported.
*
*/
private int getLeaderAlignIndent(int leaderLength,
int leaderPatternWidth) {
// calculate position of used space in line area
double position = getCurrentXPosition();
// calculate factor of next leader pattern cycle
double nextRepeatedLeaderPatternCycle = Math.ceil(position
/ leaderPatternWidth);
// calculate difference between start of next leader
// pattern cycle and already used space
double difference =
(leaderPatternWidth * nextRepeatedLeaderPatternCycle) - position;
return (int)difference;
}
/**
* calculates the used space in this line area
*/
private int getCurrentXPosition() {
return finalWidth + spaceWidth + startIndent + pendingWidth;
}
/**
* extracts a complete word from the character data
*/
private String getHyphenationWord(char[] characters, int wordStart) {
boolean wordendFound = false;
int counter = 0;
char[] newWord = new char[characters.length]; // create a buffer
while ((!wordendFound)
&& ((wordStart + counter) < characters.length)) {
char tk = characters[wordStart + counter];
if (Character.isLetter(tk)) {
newWord[counter] = tk;
counter++;
} else {
wordendFound = true;
}
}
return new String(newWord, 0, counter);
}
/**
* extracts word for hyphenation and calls hyphenation package,
* handles cases of inword punctuation and quotation marks at the beginning
* of words, but not in a internationalized way
*/
public int doHyphenation(char[] characters, int position, int wordStart,
int remainingWidth) {
// check whether the language property has been set
if (this.hyphProps.language.equalsIgnoreCase("none")) {
MessageHandler.errorln("if property 'hyphenate' is used, a language must be specified");
return wordStart;
}
/**
* remaining part string of hyphenation
*/
StringBuffer remainingString = new StringBuffer();
/**
* for words with some inword punctuation like / or -
*/
StringBuffer preString = null;
/**
* char before the word, probably whitespace
*/
char startChar = ' '; // characters[wordStart-1];
/**
* in word punctuation character
*/
char inwordPunctuation;
/**
* the complete word handed to the hyphenator
*/
String wordToHyphenate;
// width of hyphenation character
int hyphCharWidth =
this.currentFontState.width(currentFontState.mapChar(this.hyphProps.hyphenationChar));
remainingWidth -= hyphCharWidth;
// handles ' or " at the beginning of the word
if (characters[wordStart] == '"' || characters[wordStart] == '\'') {
remainingString.append(characters[wordStart]);
// extracts whole word from string
wordToHyphenate = getHyphenationWord(characters, wordStart + 1);
} else {
wordToHyphenate = getHyphenationWord(characters, wordStart);
}
// if the extracted word is smaller than the remaining width
// we have a non letter character inside the word. at the moment
// we will only handle hard hyphens and slashes
if (getWordWidth(wordToHyphenate) < remainingWidth) {
inwordPunctuation =
characters[wordStart + remainingString.length()
+ wordToHyphenate.length()];
if (inwordPunctuation == '-' || inwordPunctuation == '/') {
preString = new StringBuffer(wordToHyphenate);
preString = preString.append(inwordPunctuation);
wordToHyphenate =
getHyphenationWord(characters,
wordStart + remainingString.length()
+ wordToHyphenate.length() + 1);
remainingWidth -=
(getWordWidth(wordToHyphenate)
+ this.currentFontState.width(currentFontState.mapChar(inwordPunctuation)));
}
}
// are there any hyphenation points
Hyphenation hyph =
Hyphenator.hyphenate(hyphProps.language, hyphProps.country,
wordToHyphenate,
hyphProps.hyphenationRemainCharacterCount,
hyphProps.hyphenationPushCharacterCount);
// no hyphenation points and no inword non letter character
if (hyph == null && preString == null) {
if (remainingString.length() > 0) {
return wordStart - 1;
} else {
return wordStart;
}
// no hyphenation points, but a inword non-letter character
} else if (hyph == null && preString != null) {
remainingString.append(preString);
// is.addMapWord(startChar,remainingString);
this.addWord(startChar, remainingString);
return wordStart + remainingString.length();
// hyphenation points and no inword non-letter character
} else if (hyph != null && preString == null) {
int index = getFinalHyphenationPoint(hyph, remainingWidth);
if (index != -1) {
remainingString.append(hyph.getPreHyphenText(index));
remainingString.append(this.hyphProps.hyphenationChar);
// is.addMapWord(startChar,remainingString);
this.addWord(startChar, remainingString);
return wordStart + remainingString.length() - 1;
}
// hyphenation points and a inword non letter character
} else if (hyph != null && preString != null) {
int index = getFinalHyphenationPoint(hyph, remainingWidth);
if (index != -1) {
remainingString.append(preString.append(hyph.getPreHyphenText(index)));
remainingString.append(this.hyphProps.hyphenationChar);
// is.addMapWord(startChar,remainingString);
this.addWord(startChar, remainingString);
return wordStart + remainingString.length() - 1;
} else {
remainingString.append(preString);
// is.addMapWord(startChar,remainingString);
this.addWord(startChar, remainingString);
return wordStart + remainingString.length();
}
}
return wordStart;
}
/**
* Calculates the wordWidth using the actual fontstate
*/
private int getWordWidth(String word) {
if (word == null)
return 0;
int wordLength = word.length();
int width = 0;
char[] characters = new char[wordLength];
word.getChars(0, wordLength, characters, 0);
for (int i = 0; i < wordLength; i++) {
width += getCharWidth(characters[i]);
}
return width;
}
public int getRemainingWidth() {
return this.getContentWidth() + startIndent - this.getCurrentXPosition();
}
public void setLinkSet(LinkSet ls) {}
public void addInlineArea(Area box) {
addPending();
addChild(box);
prev = TEXT;
finalWidth += box.getContentWidth();
}
public void addInlineSpace(InlineSpace is, int spaceWidth) {
addChild(is);
finalWidth += spaceWidth;
// spaceWidth = 0;
}
/**
* adds a single character to the line area tree
*/
public int addCharacter(char data, LinkSet ls, boolean ul) {
WordArea ia = null;
int remainingWidth = this.getRemainingWidth();
int width =
this.currentFontState.width(currentFontState.mapChar(data));
// if it doesn't fit, return
if (width > remainingWidth) {
return org.apache.fop.fo.flow.Character.DOESNOT_FIT;
} else {
// if whitespace-collapse == true, discard character
if (Character.isSpaceChar(data)
&& whiteSpaceCollapse == WhiteSpaceCollapse.TRUE) {
return org.apache.fop.fo.flow.Character.OK;
}
// create new InlineArea
ia = new WordArea(currentFontState, this.red, this.green,
this.blue, new Character(data).toString(),
width);
ia.setYOffset(placementOffset);
ia.setUnderlined(ul);
pendingAreas.addElement(ia);
if (Character.isSpaceChar(data)) {
this.spaceWidth = +width;
prev = LineArea.WHITESPACE;
} else {
pendingWidth += width;
prev = LineArea.TEXT;
}
return org.apache.fop.fo.flow.Character.OK;
}
}
/**
* Same as addWord except that characters in wordBuf is mapped
* to the current fontstate's encoding
*/
private void addMapWord(char startChar, StringBuffer wordBuf) {
StringBuffer mapBuf = new StringBuffer(wordBuf.length());
for (int i = 0; i < wordBuf.length(); i++) {
mapBuf.append(currentFontState.mapChar(wordBuf.charAt(i)));
}
addWord(startChar, mapBuf);
}
/**
* adds a InlineArea containing the String startChar+wordBuf to the line area children.
*/
private void addWord(char startChar, StringBuffer wordBuf) {
String word = (wordBuf != null) ? wordBuf.toString() : "";
WordArea hia;
int startCharWidth = getCharWidth(startChar);
if (isAnySpace(startChar)) {
this.addChild(new InlineSpace(startCharWidth));
} else {
hia = new WordArea(currentFontState, this.red, this.green,
this.blue,
new Character(startChar).toString(), 1);
hia.setYOffset(placementOffset);
this.addChild(hia);
}
int wordWidth = this.getWordWidth(word);
hia = new WordArea(currentFontState, this.red, this.green, this.blue,
word, word.length());
hia.setYOffset(placementOffset);
this.addChild(hia);
// calculate the space needed
finalWidth += startCharWidth + wordWidth;
}
/**
* extracts from a hyphenated word the best (most greedy) fit
*/
private int getFinalHyphenationPoint(Hyphenation hyph,
int remainingWidth) {
int[] hyphenationPoints = hyph.getHyphenationPoints();
int numberOfHyphenationPoints = hyphenationPoints.length;
int index = -1;
String wordBegin = "";
int wordBeginWidth = 0;
for (int i = 0; i < numberOfHyphenationPoints; i++) {
wordBegin = hyph.getPreHyphenText(i);
if (this.getWordWidth(wordBegin) > remainingWidth) {
break;
}
index = i;
}
return index;
}
/**
* Checks if it's legal to break a word in the middle
* based on the current language property.
* @return true if legal to break word in the middle
*/
private boolean canBreakMidWord() {
boolean ret = false;
if (hyphProps != null && hyphProps.language != null
&&!hyphProps.language.equals("NONE")) {
String lang = hyphProps.language.toLowerCase();
if ("zh".equals(lang) || "ja".equals(lang) || "ko".equals(lang)
|| "vi".equals(lang))
ret = true;
}
return ret;
}
/**
* Helper method for getting the width of a unicode char
* from the current fontstate.
* This also performs some guessing on widths on various
* versions of space that might not exists in the font.
*/
private int getCharWidth(char c) {
int width;
if ((c == '\n') || (c == '\r') || (c == '\t') || (c == '\u00A0')) {
width = getCharWidth(' ');
} else {
width = currentFontState.width(currentFontState.mapChar(c));
if (width <= 0) {
// Estimate the width of spaces not represented in
// the font
int em = currentFontState.width(currentFontState.mapChar('m'));
int en = currentFontState.width(currentFontState.mapChar('n'));
if (em <= 0)
em = 500 * currentFontState.getFontSize();
if (en <= 0)
en = em - 10;
if (c == ' ')
width = em;
if (c == '\u2000')
width = en;
if (c == '\u2001')
width = em;
if (c == '\u2002')
width = em / 2;
if (c == '\u2003')
width = currentFontState.getFontSize();
if (c == '\u2004')
width = em / 3;
if (c == '\u2005')
width = em / 4;
if (c == '\u2006')
width = em / 6;
if (c == '\u2007')
width = getCharWidth(' ');
if (c == '\u2008')
width = getCharWidth('.');
if (c == '\u2009')
width = em / 5;
if (c == '\u200A')
width = 5;
if (c == '\u200B')
width = 100;
if (c == '\u202F')
width = getCharWidth(' ') / 2;
if (c == '\u3000')
width = getCharWidth(' ') * 2;
}
}
return width;
}
/**
* Helper method to determine if the character is a
* space with normal behaviour. Normal behaviour means that
* it's not non-breaking
*/
private boolean isSpace(char c) {
if (c == ' ' || c == '\u2000' || // en quad
c == '\u2001' || // em quad
c == '\u2002' || // en space
c == '\u2003' || // em space
c == '\u2004' || // three-per-em space
c == '\u2005' || // four--per-em space
c == '\u2006' || // six-per-em space
c == '\u2007' || // figure space
c == '\u2008' || // punctuation space
c == '\u2009' || // thin space
c == '\u200A' || // hair space
c == '\u200B') // zero width space
return true;
else
return false;
}
/**
* Method to determine if the character is a nonbreaking
* space.
*/
private boolean isNBSP(char c) {
if (c == '\u00A0' || c == '\u202F' || // narrow no-break space
c == '\u3000' || // ideographic space
c == '\uFEFF') { // zero width no-break space
return true;
} else
return false;
}
/**
* @return true if the character represents any kind of space
*/
private boolean isAnySpace(char c) {
boolean ret = (isSpace(c) || isNBSP(c));
return ret;
}
/**
* Add a word that might contain non-breaking spaces.
* Split the word into WordArea and InlineSpace and add it.
* If addToPending is true, add to pending areas.
*/
private void addSpacedWord(String word, LinkSet ls, int startw,
int spacew, TextState textState,
boolean addToPending) {
StringTokenizer st = new StringTokenizer(word, "\u00A0\u202F\u3000\uFEFF", true);
int extraw = 0;
while (st.hasMoreTokens()) {
String currentWord = st.nextToken();
if (currentWord.length() == 1
&& (isNBSP(currentWord.charAt(0)))) {
// Add an InlineSpace
int spaceWidth = getCharWidth(currentWord.charAt(0));
if (spaceWidth > 0) {
InlineSpace is = new InlineSpace(spaceWidth);
extraw += spaceWidth;
if (prevUlState) {
is.setUnderlined(textState.getUnderlined());
}
if (prevOlState) {
is.setOverlined(textState.getOverlined());
}
if (prevLTState) {
is.setLineThrough(textState.getLineThrough());
}
if (addToPending) {
pendingAreas.addElement(is);
pendingWidth += spaceWidth;
} else {
addChild(is);
}
}
} else {
WordArea ia = new WordArea(currentFontState, this.red,
this.green, this.blue,
currentWord,
getWordWidth(currentWord));
ia.setYOffset(placementOffset);
ia.setUnderlined(textState.getUnderlined());
prevUlState = textState.getUnderlined();
ia.setOverlined(textState.getOverlined());
prevOlState = textState.getOverlined();
ia.setLineThrough(textState.getLineThrough());
prevLTState = textState.getLineThrough();
ia.setVerticalAlign(vAlign);
if (addToPending) {
pendingAreas.addElement(ia);
pendingWidth += getWordWidth(currentWord);
} else {
addChild(ia);
}
if (ls != null) {
Rectangle lr = new Rectangle(startw + extraw, spacew,
ia.getContentWidth(),
fontState.getFontSize());
ls.addRect(lr, this, ia);
}
}
}
}
}