From d93dfd6e1ef331a1539cdae9f2a37f274aba89c4 Mon Sep 17 00:00:00 2001 From: Simon Pepping Date: Sun, 5 Sep 2004 18:16:32 +0000 Subject: New line breaking algorithm, patch 29124, submitted by Luca Furini. This patch implements the algorithm for most but not yet all inline layout managers. For the algorithm, see D.E. Knuth and M.F. Plass, "Breaking paragraphs into lines", Software, Practice and Experience 11 (1981) 1119-1184; reprinted in: D. E. Knuth, "Digital typography", CSLI Lecture Notes Number 78 (CLSI Publications, Stanford, CA, USA) pp. 67-155. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@197907 13f79535-47bb-0310-9956-ffa450edef68 --- src/java/org/apache/fop/area/inline/Character.java | 59 +- src/java/org/apache/fop/area/inline/TextArea.java | 36 +- src/java/org/apache/fop/fo/PropertyManager.java | 50 +- .../fop/layoutmgr/AbstractLayoutManager.java | 39 + .../fop/layoutmgr/CharacterLayoutManager.java | 216 ++++ .../apache/fop/layoutmgr/ContentLayoutManager.java | 30 + .../fop/layoutmgr/InlineStackingLayoutManager.java | 244 +++- .../org/apache/fop/layoutmgr/LayoutContext.java | 1 + .../org/apache/fop/layoutmgr/LayoutManager.java | 15 + .../apache/fop/layoutmgr/LeaderLayoutManager.java | 102 ++ .../fop/layoutmgr/LeafNodeLayoutManager.java | 120 +- .../apache/fop/layoutmgr/LineLayoutManager.java | 1225 ++++++++++++++++---- .../apache/fop/layoutmgr/TextLayoutManager.java | 598 ++++++++-- .../org/apache/fop/render/pdf/PDFRenderer.java | 70 +- .../org/apache/fop/render/xml/XMLRenderer.java | 3 +- 15 files changed, 2459 insertions(+), 349 deletions(-) (limited to 'src/java/org/apache/fop') diff --git a/src/java/org/apache/fop/area/inline/Character.java b/src/java/org/apache/fop/area/inline/Character.java index 9630614e0..bed1b194a 100644 --- a/src/java/org/apache/fop/area/inline/Character.java +++ b/src/java/org/apache/fop/area/inline/Character.java @@ -23,15 +23,19 @@ package org.apache.fop.area.inline; * This inline area holds a single character. */ public class Character extends InlineArea { - private char character; + // use a String instead of a character because if this character + // ends a syllable the hyphenation character must be added + private String character; + private int iTextWordSpaceAdjust = 0; + private int iTextLetterSpaceAdjust = 0; /** - * Create a new characater inline area with the given character. + * Create a new character inline area with the given character. * * @param ch the character for this inline area */ public Character(char ch) { - character = ch; + character = new String() + ch; } /** @@ -39,8 +43,55 @@ public class Character extends InlineArea { * * @return the character */ - public char getChar() { + public String getChar() { return character; } + + /** + * Add the hyphenation character and its length. + * + * @param hyphChar the hyphenation character + * @param hyphSize the size of the hyphenation character + */ + public void addHyphen(char hyphChar, int hyphSize) { + character += hyphChar; + this.setIPD(this.getIPD() + hyphSize); + } + + /** + * Get text word space adjust. + * + * @return the text word space adjustment + */ + public int getTextWordSpaceAdjust() { + return iTextWordSpaceAdjust; + } + + /** + * Set text word space adjust. + * + * @param iTWSadjust the text word space adjustment + */ + public void setTextWordSpaceAdjust(int iTWSadjust) { + iTextWordSpaceAdjust = iTWSadjust; + } + + /** + * Get text letter space adjust. + * + * @return the text letter space adjustment + */ + public int getTextLetterSpaceAdjust() { + return iTextLetterSpaceAdjust; + } + + /** + * Set text letter space adjust. + * + * @param iTLSadjust the text letter space adjustment + */ + public void setTextLetterSpaceAdjust(int iTLSadjust) { + iTextLetterSpaceAdjust = iTLSadjust; + } } diff --git a/src/java/org/apache/fop/area/inline/TextArea.java b/src/java/org/apache/fop/area/inline/TextArea.java index 110eaa03f..86bf35838 100644 --- a/src/java/org/apache/fop/area/inline/TextArea.java +++ b/src/java/org/apache/fop/area/inline/TextArea.java @@ -27,7 +27,8 @@ public class TextArea extends InlineArea { * The text for this inline area */ protected String text; - private int iTextSpaceAdjust = 0; + private int iTextWordSpaceAdjust = 0; + private int iTextLetterSpaceAdjust = 0; /** * Create a text inline area @@ -54,21 +55,38 @@ public class TextArea extends InlineArea { } /** - * Get text space adjust. + * Get text word space adjust. * - * @return the text space adjustment + * @return the text word space adjustment */ - public int getTextSpaceAdjust() { - return iTextSpaceAdjust; + public int getTextWordSpaceAdjust() { + return iTextWordSpaceAdjust; } /** - * Set text space adjust. + * Set text word space adjust. * - * @param iTSadjust the text space adjustment + * @param iTWSadjust the text word space adjustment */ - public void setTextSpaceAdjust(int iTSadjust) { - iTextSpaceAdjust = iTSadjust; + public void setTextWordSpaceAdjust(int iTWSadjust) { + iTextWordSpaceAdjust = iTWSadjust; + } + /** + * Get text letter space adjust. + * + * @return the text letter space adjustment + */ + public int getTextLetterSpaceAdjust() { + return iTextLetterSpaceAdjust; + } + + /** + * Set text letter space adjust. + * + * @param iTLSadjust the text letter space adjustment + */ + public void setTextLetterSpaceAdjust(int iTLSadjust) { + iTextLetterSpaceAdjust = iTLSadjust; } } diff --git a/src/java/org/apache/fop/fo/PropertyManager.java b/src/java/org/apache/fop/fo/PropertyManager.java index c3ab86847..51fbaf307 100644 --- a/src/java/org/apache/fop/fo/PropertyManager.java +++ b/src/java/org/apache/fop/fo/PropertyManager.java @@ -19,6 +19,7 @@ package org.apache.fop.fo; // FOP +import org.apache.fop.apps.FOPException; import org.apache.fop.fonts.Font; import org.apache.fop.fonts.FontInfo; import org.apache.fop.fo.properties.Property; @@ -472,28 +473,47 @@ public class PropertyManager implements Constants { textInfo.wrapOption = propertyList.get(PR_WRAP_OPTION).getEnum(); textInfo.bWrap = (textInfo.wrapOption == Constants.WRAP); + // if word-spacing or letter-spacing is "normal", convert it + // into a suitable MinOptMax value Property wordSpacing = propertyList.get(PR_WORD_SPACING); + Property letterSpacing = propertyList.get(PR_LETTER_SPACING); if (wordSpacing.getEnum() == NORMAL) { - textInfo.wordSpacing = new SpaceVal(new MinOptMax(0), true, true, 0); + if (letterSpacing.getEnum() == NORMAL) { + // letter spaces are set to zero (or use different values?) + textInfo.letterSpacing + = new SpaceVal(new MinOptMax(0), true, true, 0); + } else { + textInfo.letterSpacing + = new SpaceVal(letterSpacing.getSpace()); + } + // give word spaces the possibility to shrink by a third, + // and stretch by a half; + int spaceCharIPD = textInfo.fs.getCharWidth(' '); + textInfo.wordSpacing = new SpaceVal + (MinOptMax.add + (new MinOptMax(-spaceCharIPD / 3, 0, spaceCharIPD / 2), + MinOptMax.multiply(textInfo.letterSpacing.getSpace(), 2)), + true, true, 0); } else { textInfo.wordSpacing = new SpaceVal(wordSpacing.getSpace()); + if (letterSpacing.getEnum() == NORMAL) { + // letter spaces are set to zero (or use different values?) + textInfo.letterSpacing + = new SpaceVal(new MinOptMax(0), true, true, 0); + } else { + textInfo.letterSpacing + = new SpaceVal(letterSpacing.getSpace()); + } } - /* textInfo.letterSpacing = - new SpaceVal(propertyList.get("letter-spacing").getSpace());*/ - - textInfo.whiteSpaceCollapse = - propertyList.get(PR_WHITE_SPACE_COLLAPSE).getEnum(); - - textInfo.lineHeight = this.propertyList.get( - PR_LINE_HEIGHT).getLength().getValue(); - + textInfo.whiteSpaceCollapse + = propertyList.get(PR_WHITE_SPACE_COLLAPSE).getEnum(); + textInfo.lineHeight + = this.propertyList.get(PR_LINE_HEIGHT).getLength().getValue(); textInfo.textTransform - = this.propertyList.get(PR_TEXT_TRANSFORM).getEnum(); - - textInfo.hyphChar = this.propertyList.get( - PR_HYPHENATION_CHARACTER).getCharacter(); - + = this.propertyList.get(PR_TEXT_TRANSFORM).getEnum(); + textInfo.hyphChar + = this.propertyList.get(PR_HYPHENATION_CHARACTER).getCharacter(); } return textInfo; } diff --git a/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java b/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java index 7ce205e6c..6a4c043e7 100644 --- a/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.LogFactory; import java.util.List; import java.util.ArrayList; +import java.util.LinkedList; import java.util.ListIterator; import java.util.Map; @@ -284,6 +285,44 @@ public abstract class AbstractLayoutManager implements LayoutManager, Constants * PROVIDE NULL IMPLEMENTATIONS OF METHODS from LayoutManager * interface which are declared abstract in AbstractLayoutManager. * ---------------------------------------------------------*/ + + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + log.debug("null implementation of getNextKnuthElements() called!"); + setFinished(true); + return null; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + log.debug("null implementation of addALetterSpaceTo() called!"); + return element; + } + + public void getWordChars(StringBuffer sbChars, Position pos) { + log.debug("null implementation of getWordChars() called!"); + } + + public void hyphenate(Position pos, HyphContext hc) { + log.debug("null implementation of hyphenate called!"); + } + + public boolean applyChanges(List oldList) { + log.debug("null implementation of applyChanges() called!"); + return false; + } + + public LinkedList getChangedKnuthElements(List oldList, + int flaggedPenalty, + int alignment) { + log.debug("null implementation of getChangeKnuthElement() called!"); + return null; + } + + public int getWordSpaceIPD() { + log.debug("null implementation of getWordSpaceIPD() called!"); + return 0; + } + public Area getParentArea(Area childArea) { return null; } diff --git a/src/java/org/apache/fop/layoutmgr/CharacterLayoutManager.java b/src/java/org/apache/fop/layoutmgr/CharacterLayoutManager.java index 68c11a906..d028cd684 100644 --- a/src/java/org/apache/fop/layoutmgr/CharacterLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/CharacterLayoutManager.java @@ -19,13 +19,24 @@ package org.apache.fop.layoutmgr; import org.apache.fop.fo.flow.Character; +import org.apache.fop.fo.TextInfo; import org.apache.fop.area.inline.InlineArea; +import org.apache.fop.area.Trait; +import org.apache.fop.traits.MinOptMax; +import org.apache.fop.traits.SpaceVal; + +import java.util.List; +import java.util.LinkedList; /** * LayoutManager for the fo:character formatting object */ public class CharacterLayoutManager extends LeafNodeLayoutManager { + private MinOptMax letterSpaceIPD; + private int hyphIPD; + private TextInfo textInfo; + /** * Constructor * @@ -36,6 +47,14 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager { super(node); InlineArea inline = getCharacterInlineArea(node); setCurrentArea(inline); + + textInfo = node.getPropertyManager().getTextLayoutProps + (node.getFOInputHandler().getFontInfo()); + SpaceVal ls = textInfo.letterSpacing; + letterSpaceIPD = new MinOptMax(ls.getSpace().min, + ls.getSpace().opt, + ls.getSpace().max); + hyphIPD = textInfo.fs.getCharWidth(textInfo.hyphChar); } private InlineArea getCharacterInlineArea(Character node) { @@ -44,5 +63,202 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager { new org.apache.fop.area.inline.Character(str.charAt(0)); return ch; } + + /** + * Offset this area. + * Offset the inline area in the bpd direction when adding the + * inline area. + * This is used for vertical alignment. + * Subclasses should override this if necessary. + * @param context the layout context used for adding the area + */ + protected void offsetArea(LayoutContext context) { + int bpd = curArea.getHeight(); + switch (alignment) { + case VerticalAlign.MIDDLE: + curArea.setOffset(context.getBaseline() - bpd / 2 /* - fontLead/2 */); + break; + case VerticalAlign.TOP: + //curArea.setOffset(0); + break; + case VerticalAlign.BOTTOM: + curArea.setOffset(context.getLineHeight() - bpd); + break; + case VerticalAlign.BASELINE: + default: + curArea.setOffset(context.getBaseline()); + break; + } + } + + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + MinOptMax ipd; + curArea = get(context); + LinkedList returnList = new LinkedList(); + + if (curArea == null) { + setFinished(true); + return null; + } + + ipd = new MinOptMax(textInfo.fs.getCharWidth(((org.apache.fop.area.inline.Character) curArea).getChar().charAt(0))); + + curArea.setIPD(ipd.opt); + curArea.setWidth(ipd.opt); + curArea.setHeight(textInfo.fs.getAscender() + - textInfo.fs.getDescender()); + + // offset is set in the offsetArea() method + //curArea.setOffset(textInfo.fs.getAscender()); + //curArea.setOffset(context.getBaseline()); + + curArea.addTrait(Trait.FONT_NAME, textInfo.fs.getFontName()); + curArea.addTrait(Trait.FONT_SIZE, + new Integer(textInfo.fs.getFontSize())); + curArea.addTrait(Trait.COLOR, textInfo.color); + + int bpd = curArea.getHeight(); + int lead = 0; + int total = 0; + int middle = 0; + switch (alignment) { + case VerticalAlign.MIDDLE : middle = bpd / 2 ; + lead = bpd / 2 ; + break; + case VerticalAlign.TOP : total = bpd; + break; + case VerticalAlign.BOTTOM : total = bpd; + break; + case VerticalAlign.BASELINE: + default: lead = bpd; + break; + } + + // create the AreaInfo object to store the computed values + areaInfo = new AreaInfo((short) 0, ipd, false, + lead, total, middle); + + // node is a fo:Character + if (letterSpaceIPD.min == letterSpaceIPD.max) { + // constant letter space, only return a box + returnList.add(new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false)); + } else { + // adjustable letter space, return a sequence of elements; + // at the moment the character is supposed to have no letter spaces, + // but returning this sequence allows us to change only one element + // if addALetterSpaceTo() is called + returnList.add(new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false)); + returnList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, + new LeafPosition(this, -1), true)); + returnList.add(new KnuthGlue(0, 0, 0, + new LeafPosition(this, -1), true)); + returnList.add(new KnuthBox(0, 0, 0, 0, + new LeafPosition(this, -1), true)); + } + + setFinished(true); + return returnList; + } + + public void getWordChars(StringBuffer sbChars, Position bp) { + sbChars.append + (((org.apache.fop.area.inline.Character) curArea).getChar()); + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + areaInfo.iLScount ++; + areaInfo.ipdArea.add(letterSpaceIPD); + + if (letterSpaceIPD.min == letterSpaceIPD.max) { + // constant letter space, return a new box + return new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false); + } else { + // adjustable letter space, return a new glue + return new KnuthGlue(letterSpaceIPD.opt, + letterSpaceIPD.max - letterSpaceIPD.opt, + letterSpaceIPD.opt - letterSpaceIPD.min, + new LeafPosition(this, -1), true); + } + } + + public void hyphenate(Position pos, HyphContext hc) { + if (hc.getNextHyphPoint() == 1) { + // the character ends a syllable + areaInfo.bHyphenated = true; + bSomethingChanged = true; + } else { + // hc.getNextHyphPoint() returned -1 (no more hyphenation points) + // or a number > 1; + // the character does not end a syllable + } + hc.updateOffset(1); + } + + public boolean applyChanges(List oldList) { + setFinished(false); + if (bSomethingChanged) { + // there is nothing to do, + // possible changes have already been applied + // in the hyphenate() method + return true; + } else { + return false; + } + } + + public LinkedList getChangedKnuthElements(List oldList, + int flaggedPenalty, + int alignment) { + if (isFinished()) { + return null; + } + + LinkedList returnList = new LinkedList(); + + if (letterSpaceIPD.min == letterSpaceIPD.max + || areaInfo.iLScount == 0) { + // constant letter space, or no letter space + returnList.add(new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false)); + if (areaInfo.bHyphenated) { + returnList.add + (new KnuthPenalty(hyphIPD, flaggedPenalty, true, + new LeafPosition(this, -1), false)); + } + } else { + // adjustable letter space + returnList.add + (new KnuthBox(areaInfo.ipdArea.opt + - areaInfo.iLScount * letterSpaceIPD.opt, + areaInfo.lead, areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false)); + returnList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthGlue(areaInfo.iLScount * letterSpaceIPD.opt, + areaInfo.iLScount * letterSpaceIPD.max - letterSpaceIPD.opt, + areaInfo.iLScount * letterSpaceIPD.opt - letterSpaceIPD.min, + new LeafPosition(this, -1), true)); + returnList.add(new KnuthBox(0, 0, 0, 0, + new LeafPosition(this, -1), true)); + if (areaInfo.bHyphenated) { + returnList.add + (new KnuthPenalty(hyphIPD, flaggedPenalty, true, + new LeafPosition(this, -1), false)); + } + } + + setFinished(true); + return returnList; + } + } diff --git a/src/java/org/apache/fop/layoutmgr/ContentLayoutManager.java b/src/java/org/apache/fop/layoutmgr/ContentLayoutManager.java index ba89110fd..9adb9fd00 100644 --- a/src/java/org/apache/fop/layoutmgr/ContentLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/ContentLayoutManager.java @@ -26,6 +26,7 @@ import org.apache.fop.area.AreaTreeHandler; import org.apache.fop.area.Resolveable; import org.apache.fop.area.PageViewport; +import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -294,5 +295,34 @@ public class ContentLayoutManager implements LayoutManager { } } + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + setFinished(true); + return null; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + return element; + } + + public void getWordChars(StringBuffer sbChars, Position pos) { + } + + public void hyphenate(Position pos, HyphContext hc) { + } + + public boolean applyChanges(List oldList) { + return false; + } + + public LinkedList getChangedKnuthElements(List oldList, + int flaggedPenalty, + int alignment) { + return null; + } + + public int getWordSpaceIPD() { + return 0; + } } diff --git a/src/java/org/apache/fop/layoutmgr/InlineStackingLayoutManager.java b/src/java/org/apache/fop/layoutmgr/InlineStackingLayoutManager.java index f35961eee..a6562bef7 100644 --- a/src/java/org/apache/fop/layoutmgr/InlineStackingLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/InlineStackingLayoutManager.java @@ -18,6 +18,8 @@ package org.apache.fop.layoutmgr; +import java.util.List; +import java.util.LinkedList; import java.util.Iterator; import java.util.ListIterator; import java.util.HashMap; @@ -47,13 +49,12 @@ public class InlineStackingLayoutManager extends AbstractLayoutManager { } protected LayoutManager getLM(Object nextObj) { - return ((Position) nextObj).getPosition().getLM(); + return ((Position) nextObj).getLM(); } protected Position getPos(Object nextObj) { - return ((Position) nextObj).getPosition(); + return ((Position) nextObj); } - } @@ -80,6 +81,8 @@ public class InlineStackingLayoutManager extends AbstractLayoutManager { private LayoutManager lastChildLM = null; // Set when return last breakposs private boolean bAreaCreated = false; + private LayoutManager currentLM = null; + /** Used to store previous content IPD for each child LM. */ private HashMap hmPrevIPD = new HashMap(); @@ -465,21 +468,37 @@ public class InlineStackingLayoutManager extends AbstractLayoutManager { context.getLeadingSpace().addSpace(inlineProps.spaceStart); + // "unwrap" the NonLeafPositions stored in parentIter + // and put them in a new list; + // also set lastLM to be the LayoutManager which created + // the last Position: if the LAST_AREA flag is set in context, + // it must be also set in the LayoutContext given to lastLM, + // but unset in the LayoutContext given to the other LMs + LinkedList positionList = new LinkedList(); + NonLeafPosition pos; + LayoutManager lastLM = null; // last child LM in this iterator + while (parentIter.hasNext()) { + pos = (NonLeafPosition) parentIter.next(); + lastLM = pos.getPosition().getLM(); + positionList.add(pos.getPosition()); + } + + StackingIter childPosIter + = new StackingIter(positionList.listIterator()); - // posIter iterates over positions returned by this LM - StackingIter childPosIter = new StackingIter(parentIter); LayoutManager prevLM = null; LayoutManager childLM ; while ((childLM = childPosIter.getNextChildLM()) != null) { - //getContext().setTrailingSpace(new SpaceSpecifier(false)); + getContext().setFlags(LayoutContext.LAST_AREA, + context.isLastArea() && childLM == lastLM); childLM.addAreas(childPosIter, getContext()); getContext().setLeadingSpace(getContext().getTrailingSpace()); - getContext().setFlags(LayoutContext.RESOLVE_LEADING_SPACE, - true); + getContext().setFlags(LayoutContext.RESOLVE_LEADING_SPACE, true); prevLM = childLM; } - /* If has trailing fence, resolve trailing space specs from descendants. + /* If has trailing fence, + * resolve trailing space specs from descendants. * Otherwise, propagate any trailing space specs to parent LM via * the context object. * If the last child LM called return ISLAST in the context object @@ -554,11 +573,11 @@ public class InlineStackingLayoutManager extends AbstractLayoutManager { int iAdjust = spaceRange.opt; if (dSpaceAdjust > 0.0) { // Stretch by factor - iAdjust += (int)((double)(spaceRange.max + iAdjust += (int) ((double) (spaceRange.max - spaceRange.opt) * dSpaceAdjust); } else if (dSpaceAdjust < 0.0) { // Shrink by factor - iAdjust += (int)((double)(spaceRange.opt + iAdjust += (int) ((double) (spaceRange.opt - spaceRange.min) * dSpaceAdjust); } if (iAdjust != 0) { @@ -570,5 +589,208 @@ public class InlineStackingLayoutManager extends AbstractLayoutManager { } } + public LinkedList getNextKnuthElements(LayoutContext lc, int alignment) { + LayoutManager curLM; + + // the list returned by child LM + LinkedList returnedList; + KnuthElement returnedElement; + + // the list which will be returned to the parent LM + LinkedList returnList = new LinkedList(); + + SpaceSpecifier leadingSpace = lc.getLeadingSpace(); + + if (lc.startsNewArea()) { + // First call to this LM in new parent "area", but this may + // not be the first area created by this inline + childLC = new LayoutContext(lc); + lc.getLeadingSpace().addSpace(inlineProps.spaceStart); + + // Check for "fence" + if (hasLeadingFence(!lc.isFirstArea())) { + // Reset leading space sequence for child areas + leadingSpace = new SpaceSpecifier(false); + } + // Reset state variables + clearPrevIPD(); // Clear stored prev content dimensions + } + + while ((curLM = getChildLM()) != null) { + // get KnuthElements from curLM + returnedList = curLM.getNextKnuthElements(lc, alignment); + if (returnedList != null) { + // "wrap" the Position stored in each element of returnedList + ListIterator listIter = returnedList.listIterator(); + while (listIter.hasNext()) { + returnedElement = (KnuthElement) listIter.next(); + returnedElement.setPosition + (new NonLeafPosition(this, + returnedElement.getPosition())); + returnList.add(returnedElement); + } + return returnList; + } else { + // curLM returned null because it finished; + // just iterate once more to see if there is another child + } + } + setFinished(true); + return null; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + NonLeafPosition savedPos = (NonLeafPosition) element.getPosition(); + element.setPosition(savedPos.getPosition()); + + KnuthElement newElement + = element.getLayoutManager().addALetterSpaceTo(element); + newElement.setPosition + (new NonLeafPosition(this, newElement.getPosition())); + element.setPosition(savedPos); + return newElement; + } + + public void getWordChars(StringBuffer sbChars, Position pos) { + Position newPos = ((NonLeafPosition) pos).getPosition(); + newPos.getLM().getWordChars(sbChars, newPos); + } + + public void hyphenate(Position pos, HyphContext hc) { + Position newPos = ((NonLeafPosition) pos).getPosition(); + newPos.getLM().hyphenate(newPos, hc); + } + + public boolean applyChanges(List oldList) { + // "unwrap" the Positions stored in the elements + ListIterator oldListIterator = oldList.listIterator(); + KnuthElement oldElement; + while (oldListIterator.hasNext()) { + oldElement = (KnuthElement) oldListIterator.next(); + oldElement.setPosition + (((NonLeafPosition) oldElement.getPosition()).getPosition()); + } + // reset the iterator + oldListIterator = oldList.listIterator(); + + LayoutManager prevLM = null; + LayoutManager currLM; + int fromIndex = 0; + + boolean bSomethingChanged = false; + while(oldListIterator.hasNext()) { + oldElement = (KnuthElement) oldListIterator.next(); + currLM = oldElement.getLayoutManager(); + // initialize prevLM + if (prevLM == null) { + prevLM = currLM; + } + + if (currLM != prevLM || !oldListIterator.hasNext()) { + if (oldListIterator.hasNext()) { + bSomethingChanged + = prevLM.applyChanges(oldList.subList(fromIndex, oldListIterator.previousIndex())) + || bSomethingChanged; + prevLM = currLM; + fromIndex = oldListIterator.previousIndex(); + } else if (currLM == prevLM) { + bSomethingChanged + = prevLM.applyChanges(oldList.subList(fromIndex, oldList.size())) + || bSomethingChanged; + } else { + bSomethingChanged + = prevLM.applyChanges(oldList.subList(fromIndex, oldListIterator.previousIndex())) + || bSomethingChanged; + bSomethingChanged + = currLM.applyChanges(oldList.subList(oldListIterator.previousIndex(), oldList.size())) + || bSomethingChanged; + } + } + } + + // "wrap" again the Positions stored in the elements + oldListIterator = oldList.listIterator(); + while (oldListIterator.hasNext()) { + oldElement = (KnuthElement) oldListIterator.next(); + oldElement.setPosition + (new NonLeafPosition(this, oldElement.getPosition())); + } + return bSomethingChanged; + } + + public LinkedList getChangedKnuthElements(List oldList, int flaggedPenalty, int alignment) { + // "unwrap" the Positions stored in the elements + ListIterator oldListIterator = oldList.listIterator(); + KnuthElement oldElement; + while (oldListIterator.hasNext()) { + oldElement = (KnuthElement) oldListIterator.next(); + oldElement.setPosition + (((NonLeafPosition) oldElement.getPosition()).getPosition()); + } + // reset the iterator + oldListIterator = oldList.listIterator(); + + KnuthElement returnedElement; + LinkedList returnedList = new LinkedList(); + LinkedList returnList = new LinkedList(); + LayoutManager prevLM = null; + LayoutManager currLM; + int fromIndex = 0; + + while(oldListIterator.hasNext()) { + oldElement = (KnuthElement) oldListIterator.next(); + currLM = oldElement.getLayoutManager(); + if (prevLM == null) { + prevLM = currLM; + } + + if (currLM != prevLM || !oldListIterator.hasNext()) { + if (oldListIterator.hasNext()) { + returnedList.addAll + (prevLM.getChangedKnuthElements + (oldList.subList(fromIndex, + oldListIterator.previousIndex()), + flaggedPenalty, alignment)); + prevLM = currLM; + fromIndex = oldListIterator.previousIndex(); + } else if (currLM == prevLM) { + returnedList.addAll + (prevLM.getChangedKnuthElements + (oldList.subList(fromIndex, oldList.size()), + flaggedPenalty, alignment)); + } else { + returnedList.addAll + (prevLM.getChangedKnuthElements + (oldList.subList(fromIndex, + oldListIterator.previousIndex()), + flaggedPenalty, alignment)); + returnedList.addAll + (currLM.getChangedKnuthElements + (oldList.subList(oldListIterator.previousIndex(), + oldList.size()), + flaggedPenalty, alignment)); + } + } + } + + // "wrap" the Position stored in each element of returnedList + ListIterator listIter = returnedList.listIterator(); + while (listIter.hasNext()) { + returnedElement = (KnuthElement) listIter.next(); + returnedElement.setPosition + (new NonLeafPosition(this, returnedElement.getPosition())); + returnList.add(returnedElement); + } + return returnList; + } + + public int getWordSpaceIPD() { + LayoutManager firstChild = getChildLM(); + if (firstChild != null) { + return firstChild.getWordSpaceIPD(); + } else { + return 0; + } + } } diff --git a/src/java/org/apache/fop/layoutmgr/LayoutContext.java b/src/java/org/apache/fop/layoutmgr/LayoutContext.java index 59c674476..5cbcb132e 100644 --- a/src/java/org/apache/fop/layoutmgr/LayoutContext.java +++ b/src/java/org/apache/fop/layoutmgr/LayoutContext.java @@ -95,6 +95,7 @@ public class LayoutContext { this.trailingSpace = parentLC.trailingSpace; //??? this.hyphContext = parentLC.hyphContext; this.dSpaceAdjust = parentLC.dSpaceAdjust; + this.ipdAdjust = parentLC.ipdAdjust; this.iLineHeight = parentLC.iLineHeight; this.iBaseline = parentLC.iBaseline; // Copy other fields as necessary. Use clone??? diff --git a/src/java/org/apache/fop/layoutmgr/LayoutManager.java b/src/java/org/apache/fop/layoutmgr/LayoutManager.java index 97a79a67b..10da478e0 100644 --- a/src/java/org/apache/fop/layoutmgr/LayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/LayoutManager.java @@ -18,6 +18,7 @@ package org.apache.fop.layoutmgr; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -245,4 +246,18 @@ public interface LayoutManager { */ void addChildLMs(List newLMs); + LinkedList getNextKnuthElements(LayoutContext context, int alignment); + + KnuthElement addALetterSpaceTo(KnuthElement element); + + void getWordChars(StringBuffer sbChars, Position pos); + + void hyphenate(Position pos, HyphContext hc); + + boolean applyChanges(List oldList); + + LinkedList getChangedKnuthElements(List oldList, int flaggedPenalty, + int alignment); + + int getWordSpaceIPD(); } diff --git a/src/java/org/apache/fop/layoutmgr/LeaderLayoutManager.java b/src/java/org/apache/fop/layoutmgr/LeaderLayoutManager.java index 8bcbb74de..ac3f81e13 100644 --- a/src/java/org/apache/fop/layoutmgr/LeaderLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/LeaderLayoutManager.java @@ -30,6 +30,9 @@ import org.apache.fop.fo.flow.Leader; import org.apache.fop.fonts.Font; import org.apache.fop.traits.MinOptMax; +import java.util.List; +import java.util.LinkedList; + /** * LayoutManager for the fo:leader formatting object */ @@ -139,4 +142,103 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { } return leaderArea; } + + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + MinOptMax ipd; + curArea = get(context); + LinkedList returnList = new LinkedList(); + + if (curArea == null) { + setFinished(true); + return null; + } + + ipd = getAllocationIPD(context.getRefIPD()); + + int bpd = curArea.getHeight(); + int lead = 0; + int total = 0; + int middle = 0; + switch (alignment) { + case VerticalAlign.MIDDLE : middle = bpd / 2 ; + lead = bpd / 2 ; + break; + case VerticalAlign.TOP : total = bpd; + break; + case VerticalAlign.BOTTOM : total = bpd; + break; + case VerticalAlign.BASELINE: + default: lead = bpd; + break; + } + + // create the AreaInfo object to store the computed values + areaInfo = new AreaInfo((short) 0, ipd, false, + lead, total, middle); + + // node is a fo:Leader + returnList.add(new KnuthBox(0, areaInfo.lead, areaInfo.total, + areaInfo.middle, + new LeafPosition(this, -1), true)); + returnList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthGlue(areaInfo.ipdArea.opt, + areaInfo.ipdArea.max - areaInfo.ipdArea.opt, + areaInfo.ipdArea.opt - areaInfo.ipdArea.min, + new LeafPosition(this, 0), false)); + returnList.add(new KnuthBox(0, areaInfo.lead, areaInfo.total, + areaInfo.middle, + new LeafPosition(this, -1), true)); + + setFinished(true); + return returnList; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + // return the unchanged glue object + return new KnuthGlue(areaInfo.ipdArea.opt, + areaInfo.ipdArea.max - areaInfo.ipdArea.opt, + areaInfo.ipdArea.opt - areaInfo.ipdArea.min, + new LeafPosition(this, 0), false); + } + + public void hyphenate(Position pos, HyphContext hc) { + // use the AbstractLayoutManager.hyphenate() null implementation + super.hyphenate(pos, hc); + } + + public boolean applyChanges(List oldList) { + setFinished(false); + return false; + } + + public LinkedList getChangedKnuthElements(List oldList, + int flaggedPenalty, + int alignment) { + if (isFinished()) { + return null; + } + + LinkedList returnList = new LinkedList(); + + returnList.add(new KnuthBox(0, areaInfo.lead, areaInfo.total, + areaInfo.middle, + new LeafPosition(this, -1), true)); + returnList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthGlue(areaInfo.ipdArea.opt, + areaInfo.ipdArea.max - areaInfo.ipdArea.opt, + areaInfo.ipdArea.opt - areaInfo.ipdArea.min, + new LeafPosition(this, 0), false)); + returnList.add(new KnuthBox(0, areaInfo.lead, areaInfo.total, + areaInfo.middle, + new LeafPosition(this, -1), true)); + + setFinished(true); + return returnList; + } + } diff --git a/src/java/org/apache/fop/layoutmgr/LeafNodeLayoutManager.java b/src/java/org/apache/fop/layoutmgr/LeafNodeLayoutManager.java index 61952e9e2..d9c2fd283 100644 --- a/src/java/org/apache/fop/layoutmgr/LeafNodeLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/LeafNodeLayoutManager.java @@ -23,6 +23,9 @@ import org.apache.fop.area.inline.InlineArea; import org.apache.fop.fo.FObj; import org.apache.fop.traits.MinOptMax; +import java.util.List; +import java.util.LinkedList; + /** * Base LayoutManager for leaf-node FObj, ie: ones which have no children. * These are all inline objects. Most of them cannot be split (Text is @@ -35,10 +38,36 @@ public class LeafNodeLayoutManager extends AbstractLayoutManager { * The inline area that this leafnode will add. */ protected InlineArea curArea = null; - private int alignment; + protected int alignment; private int lead; private MinOptMax ipd; + protected boolean bSomethingChanged = false; + protected AreaInfo areaInfo = null; + + /** + * Store information about the inline area + */ + protected class AreaInfo { + protected short iLScount; + protected MinOptMax ipdArea; + protected boolean bHyphenated; + protected int lead; + protected int total; + protected int middle; + + public AreaInfo(short iLS, MinOptMax ipd, boolean bHyph, + int l, int t, int m) { + iLScount = iLS; + ipdArea = ipd; + bHyphenated = bHyph; + lead = l; + total = t; + middle = m; + } + } + + /** * Create a Leaf node layout mananger. * @param node the FObj to attach to this LM. @@ -196,12 +225,11 @@ public class LeafNodeLayoutManager extends AbstractLayoutManager { * @param context the layout context for adding the area */ public void addAreas(PositionIterator posIter, LayoutContext context) { - parentLM.addChild(curArea); - addID(); offsetArea(context); widthAdjustArea(context); + parentLM.addChild(curArea); while (posIter.hasNext()) { posIter.next(); @@ -243,11 +271,13 @@ public class LeafNodeLayoutManager extends AbstractLayoutManager { */ protected void widthAdjustArea(LayoutContext context) { double dAdjust = context.getIPDAdjust(); - int width = ipd.opt; + int width = areaInfo.ipdArea.opt; if (dAdjust < 0) { - width = (int)(width + dAdjust * (ipd.opt - ipd.min)); + width = (int) (width + dAdjust * (areaInfo.ipdArea.opt + - areaInfo.ipdArea.min)); } else if (dAdjust > 0) { - width = (int)(width + dAdjust * (ipd.max - ipd.opt)); + width = (int) (width + dAdjust * (areaInfo.ipdArea.max + - areaInfo.ipdArea.opt)); } curArea.setWidth(width); } @@ -260,5 +290,83 @@ public class LeafNodeLayoutManager extends AbstractLayoutManager { public boolean canBreakBefore(LayoutContext context) { return true; } + + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + MinOptMax ipd; + curArea = get(context); + LinkedList returnList = new LinkedList(); + + if (curArea == null) { + setFinished(true); + return null; + } + ipd = getAllocationIPD(context.getRefIPD()); + + int bpd = curArea.getHeight(); + int lead = 0; + int total = 0; + int middle = 0; + switch (alignment) { + case VerticalAlign.MIDDLE : middle = bpd / 2 ; + lead = bpd / 2 ; + break; + case VerticalAlign.TOP : total = bpd; + break; + case VerticalAlign.BOTTOM : total = bpd; + break; + case VerticalAlign.BASELINE: + default: lead = bpd; + break; + } + + // create the AreaInfo object to store the computed values + areaInfo = new AreaInfo((short) 0, ipd, false, + lead, total, middle); + + // node is a fo:ExternalGraphic, fo:InstreamForeignObject, + // fo:PageNumber or fo:PageNumberCitation + returnList.add(new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false)); + setFinished(true); + return returnList; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + // return the unchanged box object + return new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), false); + } + + public void hyphenate(Position pos, HyphContext hc) { + // use the AbstractLayoutManager.hyphenate() null implementation + super.hyphenate(pos, hc); + } + + public boolean applyChanges(List oldList) { + setFinished(false); + return false; + } + + public LinkedList getChangedKnuthElements(List oldList, + int flaggedPenalty, + int alignment) { + if (isFinished()) { + return null; + } + + LinkedList returnList = new LinkedList(); + + // fobj is a fo:ExternalGraphic, fo:InstreamForeignObject, + // fo:PageNumber or fo:PageNumberCitation + returnList.add(new KnuthBox(areaInfo.ipdArea.opt, areaInfo.lead, + areaInfo.total, areaInfo.middle, + new LeafPosition(this, 0), true)); + + setFinished(true); + return returnList; + } } diff --git a/src/java/org/apache/fop/layoutmgr/LineLayoutManager.java b/src/java/org/apache/fop/layoutmgr/LineLayoutManager.java index 70e923b15..2a097ecec 100644 --- a/src/java/org/apache/fop/layoutmgr/LineLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/LineLayoutManager.java @@ -34,6 +34,8 @@ import java.util.ListIterator; import java.util.Iterator; import java.util.List; import java.util.ArrayList; +import java.util.LinkedList; + import org.apache.fop.traits.MinOptMax; /** @@ -58,6 +60,13 @@ public class LineLayoutManager extends InlineStackingLayoutManager { iIndents = marginProps.startIndent + marginProps.endIndent; BlockProps blockProps = pm.getBlockProps(); bTextAlignment = blockProps.textAlign; + bTextAlignmentLast = blockProps.textAlignLast; + // + if (bTextAlignment != JUSTIFY && bTextAlignmentLast == JUSTIFY) { + effectiveAlignment = 0; + } else { + effectiveAlignment = bTextAlignment; + } textIndent = blockProps.firstIndent; hyphProps = pm.getHyphenationProps(); } @@ -93,6 +102,8 @@ public class LineLayoutManager extends InlineStackingLayoutManager { private BreakPoss prevBP = null; // Last confirmed break position private int bTextAlignment = TextAlign.JUSTIFY; + private int bTextAlignmentLast; + private int effectiveAlignment; private Length textIndent; private int iIndents = 0; private CommonHyphenation hyphProps; @@ -104,6 +115,237 @@ public class LineLayoutManager extends InlineStackingLayoutManager { // inline start pos when adding areas private int iStartPos = 0; + private ArrayList knuthParagraphs = null; + private LinkedList activeList = null; + private LinkedList inactiveList = null; + private ArrayList breakpoints = null; + private int iReturnedLBP = 0; + private int iStartElement = 0; + private int iEndElement = 0; + private int iCurrParIndex = 0; + + private KnuthNode lastDeactivatedNode = null; + + // parameters of Knuth's algorithm: + // penalty value for flagged penalties + private int flaggedPenalty = 50; + // demerit for consecutive lines ending at flagged penalties + private int repeatedFlaggedDemerit = 50; + // demerit for consecutive lines belonging to incompatible fitness classes + private int incompatibleFitnessDemerit = 50; + // suggested modification to the "optimum" number of lines + private int looseness = 0; + + private static final int INFINITE_RATIO = 1000; + + // this class represent a feasible breaking point + private class KnuthNode { + // index of the breakpoint represented by this node + public int position; + + // number of the line ending at this breakpoint + public int line; + + // fitness class of the line ending at his breakpoint + public int fitness; + + // accumulated width of the KnuthElements + public int totalWidth; + + // accumulated stretchability of the KnuthElements + public int totalStretch; + + // accumulated shrinkability of the KnuthElements + public int totalShrink; + + // adjustment ratio if the line ends at this breakpoint + public double adjustRatio; + + // difference between target and actual line width + public int difference; + + // minimum total demerits up to this breakpoint + public double totalDemerits; + + // best node for the preceding breakpoint + public KnuthNode previous; + + public KnuthNode(int position, int line, int fitness, + int totalWidth, int totalStretch, int totalShrink, + double adjustRatio, int difference, + double totalDemerits, KnuthNode previous) { + this.position = position; + this.line = line; + this.fitness = fitness; + this.totalWidth = totalWidth; + this.totalStretch = totalStretch; + this.totalShrink = totalShrink; + this.adjustRatio = adjustRatio; + this.difference = difference; + this.totalDemerits = totalDemerits; + this.previous = previous; + } + } + + // this class stores information about how the nodes + // which could start a line + // ending at the current element + private class BestRecords { + private static final double INFINITE_DEMERITS = 1E11; + + private double bestDemerits[] = { + INFINITE_DEMERITS, INFINITE_DEMERITS, + INFINITE_DEMERITS, INFINITE_DEMERITS + }; + private KnuthNode bestNode[] = {null, null, null, null}; + private double bestAdjust[] = {0.0, 0.0, 0.0, 0.0}; + private int bestDifference[] = {0, 0, 0, 0}; + private int bestIndex = -1; + + public BestRecords() { + } + + public void addRecord(double demerits, KnuthNode node, double adjust, + int difference, int fitness) { + if (demerits > bestDemerits[fitness]) { + log.error("New demerits value greter than the old one"); + } + bestDemerits[fitness] = demerits; + bestNode[fitness] = node; + bestAdjust[fitness] = adjust; + bestDifference[fitness] = difference; + if (bestIndex == -1 || demerits < bestDemerits[bestIndex]) { + bestIndex = fitness; + } + } + + public boolean hasRecords() { + return (bestIndex != -1); + } + + public boolean notInfiniteDemerits(int fitness) { + return (bestDemerits[fitness] != INFINITE_DEMERITS); + } + + public double getDemerits(int fitness) { + return bestDemerits[fitness]; + } + + public KnuthNode getNode(int fitness) { + return bestNode[fitness]; + } + + public double getAdjust(int fitness) { + return bestAdjust[fitness]; + } + + public int getDifference(int fitness) { + return bestDifference[fitness]; + } + + public double getMinDemerits() { + if (bestIndex != -1) { + return getDemerits(bestIndex); + } else { + // anyway, this should never happen + return INFINITE_DEMERITS; + } + } + } + + // this class is used to remember + // which was the first element in the paragraph + // returned by each LM + private class Update { + private LayoutManager inlineLM; + private int iFirstIndex; + + public Update(LayoutManager lm, int index) { + inlineLM = lm; + iFirstIndex = index; + } + } + + // this class represents a paragraph + private class Paragraph extends LinkedList { + // number of KnuthElements added by the LineLayoutManager + public int ignoreAtStart = 0; + public int ignoreAtEnd = 0; + // minimum space at the end of the last line (in millipoints) + public int lineFillerWidth; + // word space dimension (in millipoints) + private int wordSpaceIPD; + + public void startParagraph(int lineWidth) { + // get the word space dimension, which needs to be known + // in order to center text + LayoutManager lm; + if ((lm = getChildLM()) != null) { + wordSpaceIPD = lm.getWordSpaceIPD(); + } + + // set the minimum amount of empty space at the end of the + // last line + if (bTextAlignment == CENTER) { + lineFillerWidth = 0; + } else { + lineFillerWidth = (int)(lineWidth / 6); + } + + // add auxiliary elements at the beginning of the paragraph + if (bTextAlignment == CENTER && bTextAlignmentLast != JUSTIFY) { + this.add(new KnuthGlue(0, 3 * wordSpaceIPD, 0, + null, false)); + ignoreAtStart ++; + } + + // add the element representing text indentation + // at the beginning of the first paragraph + if (knuthParagraphs.size() == 0 + && textIndent.getValue() != 0) { + this.add(new KnuthBox(textIndent.getValue(), 0, 0, 0, + null, false)); + ignoreAtStart ++; + } + } + + public void endParagraph() { + // remove glue and penalty item at the end of the paragraph + while (this.size() > ignoreAtStart + && !((KnuthElement) this.get(this.size() - 1)).isBox()) { + this.remove(this.size() - 1); + } + if (this.size() > ignoreAtStart) { + if (bTextAlignment == CENTER + && bTextAlignmentLast != JUSTIFY) { + this.add(new KnuthGlue(0, 3 * wordSpaceIPD, 0, + null, false)); + this.add(new KnuthPenalty(0, -KnuthElement.INFINITE, + false, null, false)); + ignoreAtEnd = 2; + } else if (bTextAlignmentLast != JUSTIFY) { + // add the elements representing the space + // at the end of the last line + // and the forced break + this.add(new KnuthPenalty(0, KnuthElement.INFINITE, + false, null, false)); + this.add(new KnuthGlue(lineFillerWidth, 10000000, 0, + null, false)); + this.add(new KnuthPenalty(0, -KnuthElement.INFINITE, + false, null, false)); + ignoreAtEnd = 3; + } else { + // add only the element representing the forced break + this.add(new KnuthPenalty(0, -KnuthElement.INFINITE, + false, null, false)); + ignoreAtEnd = 1; + } + knuthParagraphs.add(this); + } + } + } + + /** * Create a new Line Layout Manager. * This is used by the block layout manager to create @@ -143,9 +385,6 @@ public class LineLayoutManager extends InlineStackingLayoutManager { // IPD remaining in line MinOptMax availIPD = context.getStackLimit(); - // QUESTION: maybe LayoutContext holds the Properties which - // come from block-level? - LayoutContext inlineLC = new LayoutContext(context); clearPrevIPD(); @@ -156,185 +395,701 @@ public class LineLayoutManager extends InlineStackingLayoutManager { } prevBP = null; - while ((curLM = getChildLM()) != null) { - // INITIALIZE LAYOUT CONTEXT FOR CALL TO CHILD LM - // First break for the child LM in each of its areas - boolean bFirstBPforLM = (vecInlineBreaks.isEmpty() - || (((BreakPoss) vecInlineBreaks.get(vecInlineBreaks.size() - 1)). - getLayoutManager() != curLM)); - - // Need previous breakpoint! ATTENTION when backing up for hyphenation! - prev = (vecInlineBreaks.isEmpty()) - ? null - : (BreakPoss) vecInlineBreaks.get(vecInlineBreaks.size() - 1); - initChildLC(inlineLC, prev, - (vecInlineBreaks.size() == iPrevLineEnd), - bFirstBPforLM, new SpaceSpecifier(true)); - - - /* If first BP in this line but line is not first in this - * LM and previous line end decision was not forced (LINEFEED), - * then set the SUPPRESS_LEADING_SPACE flag. - */ - inlineLC.setFlags(LayoutContext.SUPPRESS_LEADING_SPACE, - (vecInlineBreaks.size() == iPrevLineEnd - && !vecInlineBreaks.isEmpty() - && ((BreakPoss) vecInlineBreaks.get(vecInlineBreaks.size() - 1)). - isForcedBreak() == false)); - - // GET NEXT POSSIBLE BREAK FROM CHILD LM - // prevBP = bp; - if ((bp = curLM.getNextBreakPoss(inlineLC)) != null) { - // Add any space before and previous content dimension - MinOptMax prevIPD = updatePrevIPD(bp, prev, - (vecInlineBreaks.size() == iPrevLineEnd), - inlineLC.isFirstArea()); - MinOptMax bpDim = - MinOptMax.add(bp.getStackingSize(), prevIPD); - - // check if this bp fits in line - boolean bBreakOK = couldEndLine(bp); - if (bBreakOK) { - /* Add any non-conditional trailing space, assuming we - * end the line here. If we can't break here, we just - * check if the content fits. - */ - bpDim.add(bp.resolveTrailingSpace(true)); + // here starts Knuth's algorithm + KnuthElement thisElement = null; + LinkedList returnedList = null; + LineBreakPosition lbp = null; + + if (knuthParagraphs == null) { + // it's the first time this method is called + knuthParagraphs = new ArrayList(); + breakpoints = new ArrayList(); + + // convert all the text in a sequence of paragraphs made + // of KnuthBox, KnuthGlue and KnuthPenalty objects + boolean bPrevWasKnuthBox = false; + KnuthBox prevBox = null; + + Paragraph knuthPar = new Paragraph(); + knuthPar.startParagraph(availIPD.opt); + while ((curLM = getChildLM()) != null) { + if ((returnedList + = curLM.getNextKnuthElements(inlineLC, + effectiveAlignment)) + != null) { + // if there are two consecutive KnuthBox, the first one + // does not represent a whole word, so it must be given + // one more letter space + thisElement = (KnuthElement) returnedList.getFirst(); + if (returnedList.size() > 1 + || !(thisElement.isPenalty() + && ((KnuthPenalty) thisElement).getP() + == -KnuthElement.INFINITE)) { + if (thisElement.isBox() && !thisElement.isAuxiliary() + && bPrevWasKnuthBox) { + prevBox = (KnuthBox) knuthPar.removeLast(); + if (!prevBox.isAuxiliary()) { + // if letter spacing is constant, + // only prevBox needs to be replaced; + knuthPar.addLast(prevBox.getLayoutManager() + .addALetterSpaceTo(prevBox)); + } else { + // prevBox is the last element + // in the sub-sequence + // + // the letter space is added to , + // while the other elements are not changed + KnuthBox auxBox = prevBox; + KnuthGlue auxGlue + = (KnuthGlue) knuthPar.removeLast(); + KnuthPenalty auxPenalty + = (KnuthPenalty) knuthPar.removeLast(); + prevBox = (KnuthBox) knuthPar.getLast(); + knuthPar.addLast(auxPenalty); + knuthPar.addLast(prevBox.getLayoutManager() + .addALetterSpaceTo(prevBox)); + knuthPar.addLast(auxBox); } - // TODO: stop if linebreak is forced (NEWLINE) - // PROBLEM: interaction with wrap which can be set - // at lower levels! - // System.err.println("BPdim=" + bpDim.opt); - - // Check if proposed area would fit in line - if (bpDim.min > availIPD.max) { - // See if we have already found a potential break - //if (vecPossEnd.size() > 0) break; - - // This break position doesn't fit - // TODO: If we are in nowrap, we use it as is! - if (hyphProps.hyphenate == Constants.TRUE) { - // If we are already in a hyphenation loop, then stop. - - if (inlineLC.tryHyphenate()) { - if (prevBP == null) { - vecInlineBreaks.add(bp); - prevBP = bp; - } - break; } - // Otherwise, prepare to try hyphenation - if (!bBreakOK) { - // Make sure we collect the entire word! - vecInlineBreaks.add(bp); - continue; + if (((KnuthElement) returnedList.getLast()).isBox()) { + bPrevWasKnuthBox = true; + } else { + bPrevWasKnuthBox = false; } - - inlineLC.setHyphContext( - getHyphenContext((prevBP == null) ? prev : prevBP, bp)); - if (inlineLC.getHyphContext() == null) { - if (prevBP == null) { - vecInlineBreaks.add(bp); - prevBP = bp; - } - break; - } - inlineLC.setFlags(LayoutContext.TRY_HYPHENATE, - true); - // Reset to previous acceptable break - resetBP((prevBP == null) ? prev : prevBP); + // add the new elements to the paragraph + knuthPar.addAll(returnedList); } else { - /* If we are not in justified text, we can end the line at - * prevBP. - */ - if (prevBP == null) { - vecInlineBreaks.add(bp); - prevBP = bp; - } - break; + // a list with a single penalty item + // whose value is -inf + // represents a preserved linefeed, + // wich forces a line break + knuthPar.endParagraph(); + knuthPar = new Paragraph(); + knuthPar.startParagraph(availIPD.opt); + bPrevWasKnuthBox = false; } } else { - // Add the BP to the list whether or not we can break - vecInlineBreaks.add(bp); - // Handle end of this LM's areas - if (bBreakOK) { - prevBP = bp; // Save reference to this BP - if (bp.isForcedBreak()) { - break; - } - if (bpDim.max >= availIPD.min) { - /* This is a possible line BP (line could be filled) - * bpDim.max >= availIPD.min - * Keep this as a possible break, depending on - * "cost". We will choose lowest cost. - * Cost depends on stretch - * (ie, bpDim.opt closes to availIPD.opt), keeps - * and hyphenation. - */ - vecPossEnd.add(new BreakCost(bp, - Math.abs(availIPD.opt - bpDim.opt))); - } - // Otherwise it's short + // curLM returned null; this can happen + // if it has nothing more to layout, + // so just iterate once more to see + // if there are other chilren + } + } + knuthPar.endParagraph(); + + // find the optimal line breaking points for each paragraph + ListIterator paragraphsIterator + = knuthParagraphs.listIterator(knuthParagraphs.size()); + Paragraph currPar = null; + while (paragraphsIterator.hasPrevious()) { + currPar = (Paragraph) paragraphsIterator.previous(); + double maxAdjustment = 1; + int iBPcount = 0; + + // first try + if ((iBPcount + = findBreakingPoints(currPar, + context.getStackLimit().opt, + maxAdjustment, false)) == 0) { + // the first try failed, now try something different + log.debug("No set of breaking points found with maxAdjustment = " + maxAdjustment); + if (hyphProps.hyphenate == Constants.TRUE) { + // consider every hyphenation point as a legal break + findHyphenationPoints(currPar); } else { - /* Can't end line here. */ + // try with a higher threshold + maxAdjustment = 5; } - } // end of bpDim.min <= availIPD.max - // end of getNextBreakPoss!=null on current child LM - } else { - /* The child LM can return a null BreakPoss if it has - * nothing (more) to layout. This can happen when backing - * up. Just try the next child LM. - */ - } - if (inlineLC.tryHyphenate() - && !inlineLC.getHyphContext().hasMoreHyphPoints()) { - break; + + if ((iBPcount + = findBreakingPoints(currPar, + context.getStackLimit().opt, + maxAdjustment, false)) == 0) { + // the second try failed too, try with a huge threshold + // and force the algorithm to find + // a set of breaking points + log.debug("No set of breaking points found with maxAdjustment = " + maxAdjustment + + (hyphProps.hyphenate == Constants.TRUE ? " and hyphenation" : "")); + maxAdjustment = 20; + iBPcount + = findBreakingPoints(currPar, + context.getStackLimit().opt, + maxAdjustment, true); + } + } } - } // end of while on child LM - if ((curLM = getChildLM()) == null) { - // No more content to layout! + } else { + // this method has been called before + // all line breaks are already calculated + } + + // get a break point from the list + lbp = (LineBreakPosition) breakpoints.get(iReturnedLBP ++); + if (iReturnedLBP == breakpoints.size()) { setFinished(true); } - if (bp == null) { - return null; + BreakPoss curLineBP = new BreakPoss(lbp); + curLineBP.setFlag(BreakPoss.ISLAST, isFinished()); + curLineBP.setStackingSize(new MinOptMax(lbp.lineHeight)); + return curLineBP; + } + + private int findBreakingPoints(Paragraph par, int lineWidth, + double threshold, boolean force) { + int totalWidth = 0; + int totalStretch = 0; + int totalShrink = 0; + boolean bForced = false; + + // current element in the paragraph + KnuthElement thisElement = null; + // previous element in the paragraph is a KnuthBox + boolean previousIsBox = false; + + // create an active node representing the starting point + activeList = new LinkedList(); + activeList.add(new KnuthNode(0, 0, 1, 0, 0, 0, 0, 0, 0, null)); + inactiveList = new LinkedList(); + + // main loop + ListIterator paragraphIterator = par.listIterator(); + while (paragraphIterator.hasNext()) { + thisElement = (KnuthElement) paragraphIterator.next(); + if (thisElement.isBox()) { + // a KnuthBox object is not a legal line break + totalWidth += thisElement.getW(); + previousIsBox = true; + } else if (thisElement.isGlue()) { + // a KnuthGlue object is a legal line break + // only if the previous object is a KnuthBox + if (previousIsBox) { + considerLegalBreak(par, lineWidth, thisElement, + totalWidth, totalStretch, totalShrink, + threshold); + } + totalWidth += thisElement.getW(); + totalStretch += ((KnuthGlue) thisElement).getY(); + totalShrink += ((KnuthGlue) thisElement).getZ(); + previousIsBox = false; + } else { + // a KnuthPenalty is a legal line break + // only if its penalty is not infinite + if (((KnuthPenalty) thisElement).getP() + < KnuthElement.INFINITE) { + considerLegalBreak(par, lineWidth, thisElement, + totalWidth, totalStretch, totalShrink, + threshold); + } + previousIsBox = false; + } } - if (prevBP == null) { - BreakPoss prevLineEnd = (iPrevLineEnd == 0) - ? null - : (BreakPoss) vecInlineBreaks.get(iPrevLineEnd); - if (allAreSuppressible(prevLineEnd)) { - removeAllBP(prevLineEnd); - return null; + if (activeList.size() == 0) { + if (force) { + activeList.add(lastDeactivatedNode); + bForced = true; + log.error("Could not find a set of breaking points"); } else { - prevBP = bp; + inactiveList.clear(); + return 0; + } + } + + // there is at least one set of breaking points + // choose the active node with fewest total demerits + ListIterator activeListIterator = activeList.listIterator(); + KnuthNode tempNode = null; + KnuthNode bestActiveNode = null; + double bestDemerits = BestRecords.INFINITE_DEMERITS; + int line = 0; + while (activeListIterator.hasNext()) { + tempNode = (KnuthNode) activeListIterator.next(); + if (tempNode.totalDemerits < bestDemerits) { + bestActiveNode = tempNode; + bestDemerits = bestActiveNode.totalDemerits; } } + line = bestActiveNode.line; + + if (looseness != 0) { + // choose the appropriate active node + activeListIterator = activeList.listIterator(); + int s = 0; + while (activeListIterator.hasNext()) { + tempNode = (KnuthNode) activeListIterator.next(); + int delta = tempNode.line - line; + if (looseness <= delta && delta < s + || s < delta && delta <= looseness) { + s = delta; + bestActiveNode = tempNode; + bestDemerits = tempNode.totalDemerits; + } else if (delta == s + && tempNode.totalDemerits < bestDemerits) { + bestActiveNode = tempNode; + bestDemerits = tempNode.totalDemerits; + } + } + line = bestActiveNode.line; + } + + // use the chosen node to determine the optimum breakpoints + for (int i = line; i > 0; i--) { + // compute indent and adjustment ratio, according to + // the value of text-align and text-align-last + int indent = 0; + int difference = (bestActiveNode.line < line || bForced) + ? bestActiveNode.difference + : bestActiveNode.difference + par.lineFillerWidth; + int textAlign = (bestActiveNode.line < line || bForced) + ? bTextAlignment : bTextAlignmentLast; + indent += (textAlign == CENTER) + ? difference / 2 + : (textAlign == END) ? difference : 0; + indent += (bestActiveNode.line == 1 + && knuthParagraphs.indexOf(par) == 0) + ? textIndent.getValue() : 0; + double ratio = (textAlign == JUSTIFY) + ? bestActiveNode.adjustRatio : 0; + + // lead to baseline is + // max of: baseline fixed alignment and middle/2 + // after baseline is + // max: top height-lead, middle/2 and bottom height-lead + int halfLeading = (lineHeight - lead - follow) / 2; + // height before baseline + int lineLead = lead + halfLeading; + // maximum size of top and bottom alignment + int maxtb = follow + halfLeading; + // max size of middle alignment below baseline + int middlefollow = maxtb; + + // index of the first KnuthElement in this line + int firstElementIndex = 0; + if (line > 1) { + firstElementIndex = bestActiveNode.previous.position + 1; + } + ListIterator inlineIterator = par.listIterator(firstElementIndex); + for (int j = 0; + j < (bestActiveNode.position - firstElementIndex + 1); + j++) { + KnuthElement element = (KnuthElement) inlineIterator.next(); + if (element.isBox()) { + if (((KnuthBox) element).getLead() > lineLead) { + lineLead = ((KnuthBox) element).getLead(); + } + if (((KnuthBox) element).getTotal() > maxtb) { + maxtb = ((KnuthBox) element).getTotal(); + } + if (((KnuthBox) element).getMiddle() > middlefollow) { + middlefollow = ((KnuthBox) element).getMiddle(); + } + } + } - // Choose the best break - if (!bp.isForcedBreak() && vecPossEnd.size() > 0) { - prevBP = getBestBP(vecPossEnd); + if (maxtb - lineLead > middlefollow) { + middlefollow = maxtb - lineLead; + } + + // add nodes at the beginning of the list, as they are found + // backwards, from the last one to the first one + breakpoints.add(0, + new LineBreakPosition(this, + bestActiveNode.position, + ratio, 0, indent, + lineLead + middlefollow, + lineLead)); + bestActiveNode = bestActiveNode.previous; } - if (bp != prevBP) { - /** Remove BPs after prevBP - if condAllAreSuppressible returns true, - else backup child LM */ - if (condAllAreSuppressible(prevBP)) { - removeAllBP(prevBP); - } else { - reset(); + if (bForced) { + fallback(par, line); + } + activeList.clear(); + inactiveList.clear(); + return line; + } + + private void fallback(Paragraph par, int line) { + // lead to baseline is + // max of: baseline fixed alignment and middle/2 + // after baseline is + // max: top height-lead, middle/2 and bottom height-lead + int halfLeading = (lineHeight - lead - follow) / 2; + // height before baseline + int lineLead = lead + halfLeading; + // maximum size of top and bottom alignment + int maxtb = follow + halfLeading; + // max size of middle alignment below baseline + int middlefollow = maxtb; + + ListIterator inlineIterator + = par.listIterator(lastDeactivatedNode.position); + for (int j = lastDeactivatedNode.position; + j < (par.size()); + j++) { + KnuthElement element = (KnuthElement) inlineIterator.next(); + if (element.isBox()) { + if (((KnuthBox) element).getLead() > lineLead) { + lineLead = ((KnuthBox) element).getLead(); + } + if (((KnuthBox) element).getTotal() > maxtb) { + maxtb = ((KnuthBox) element).getTotal(); + } + if (((KnuthBox) element).getMiddle() > middlefollow) { + middlefollow = ((KnuthBox) element).getMiddle(); + } } } - // Don't justify last line in the sequence or if forced line-end - int talign = bTextAlignment; - if ((bTextAlignment == TextAlign.JUSTIFY - && (prevBP.isForcedBreak() - || isFinished()))) { - talign = TextAlign.START; + if (maxtb - lineLead > middlefollow) { + middlefollow = maxtb - lineLead; } - return makeLineBreak(iPrevLineEnd, availIPD, talign); + + breakpoints.add(line, + new LineBreakPosition(this, par.size() - 1, + 0, 0, 0, + lineLead + middlefollow, + lineLead)); + } + + + private void considerLegalBreak(LinkedList par, int lineWidth, + KnuthElement element, + int totalWidth, int totalStretch, + int totalShrink, double threshold) { + KnuthNode activeNode = null; + + ListIterator activeListIterator = activeList.listIterator(); + if (activeListIterator.hasNext()) { + activeNode = (KnuthNode) activeListIterator.next(); + } else { + activeNode = null; + } + + while (activeNode != null) { + BestRecords best = new BestRecords(); + + // these are the new values that must be computed + // in order to define a new active node + int newLine = 0; + int newFitnessClass = 0; + int newWidth = 0; + int newStretch = 0; + int newShrink = 0; + double newIPDAdjust = 0; + double newDemerits = 0; + + while (activeNode != null) { + // compute the line number + newLine = activeNode.line + 1; + + // compute the adjustment ratio + int actualWidth = totalWidth - activeNode.totalWidth; + if (element.isPenalty()) { + actualWidth += element.getW(); + } + int neededAdjustment = lineWidth - actualWidth; + int maxAdjustment = 0; + if (neededAdjustment > 0) { + maxAdjustment = totalStretch - activeNode.totalStretch; + if (maxAdjustment > 0) { + newIPDAdjust + = (double) neededAdjustment / maxAdjustment; + } else { + newIPDAdjust = INFINITE_RATIO; + } + } else if (neededAdjustment < 0) { + maxAdjustment = totalShrink - activeNode.totalShrink; + if (maxAdjustment > 0) { + newIPDAdjust + = (double) neededAdjustment / maxAdjustment; + } else { + newIPDAdjust = INFINITE_RATIO; + } + } else { + // neededAdjustment == 0 + newIPDAdjust = 0; + } + if (newIPDAdjust < -1 + || (element.isPenalty() + && ((KnuthPenalty) element).getP() + == -KnuthElement.INFINITE) + && !(activeNode.position == par.indexOf(element))) { + // deactivate activeNode + KnuthNode tempNode + = (KnuthNode) activeListIterator.previous(); + int iCallNext = 0; + while (tempNode != activeNode) { + // this is not the node we meant to remove! + tempNode = (KnuthNode) activeListIterator.previous(); + iCallNext ++; + } + lastDeactivatedNode = tempNode; + inactiveList.add(tempNode); + activeListIterator.remove(); + for (int i = 0; i < iCallNext; i++) { + activeListIterator.next(); + } + } + + if ((-1 <= newIPDAdjust) && (newIPDAdjust <= threshold)) { + // compute demerits and fitness class + if (element.isPenalty() + && ((KnuthPenalty) element).getP() >= 0) { + newDemerits + = Math.pow((1 + + 100 * Math.pow(Math.abs(newIPDAdjust), 3) + + ((KnuthPenalty) element).getP()), 2); + } else if (element.isPenalty() + && ((KnuthPenalty) element).getP() + > -INFINITE_RATIO) { + newDemerits + = Math.pow((1 + + 100 * Math.pow(Math.abs(newIPDAdjust), 3)), 2) + - Math.pow(((KnuthPenalty) element).getP(), 2); + } else { + newDemerits + = Math.pow((1 + + 100 * Math.pow(Math.abs(newIPDAdjust), 3)), 2); + } + if (element.isPenalty() + && ((KnuthPenalty) element).isFlagged() + && ((KnuthElement) par.get(activeNode.position)).isPenalty() + && ((KnuthPenalty) par.get(activeNode.position)).isFlagged()) { + // add demerit for consecutive breaks at flagged penalties + newDemerits += repeatedFlaggedDemerit; + } + if (newIPDAdjust < -0.5) { + newFitnessClass = 0; + } else if (newIPDAdjust <= 0.5) { + newFitnessClass = 1; + } else if (newIPDAdjust <= 1) { + newFitnessClass = 2; + } else { + newFitnessClass = 3; + } + if (Math.abs(newFitnessClass - activeNode.fitness) > 1) { + // add demerit for consecutive breaks + // with very different fitness classes + newDemerits += incompatibleFitnessDemerit; + } + newDemerits += activeNode.totalDemerits; + if (newDemerits < best.getDemerits(newFitnessClass)) { + // updates best demerits data + best.addRecord(newDemerits, activeNode, newIPDAdjust, + neededAdjustment, newFitnessClass); + } + } + + + if (activeListIterator.hasNext()) { + activeNode = (KnuthNode) activeListIterator.next(); + } else { + activeNode = null; + break; + } + if (activeNode.line >= newLine) { + break; + } + } // end of the inner while + + if (best.hasRecords()) { + // compute width, stratchability and shrinkability + newWidth = totalWidth; + newStretch = totalStretch; + newShrink = totalShrink; + ListIterator tempIterator + = par.listIterator(par.indexOf(element)); + while (tempIterator.hasNext()) { + KnuthElement tempElement + = (KnuthElement) tempIterator.next(); + if (tempElement.isBox()) { + break; + } else if (tempElement.isGlue()) { + newWidth += ((KnuthGlue) tempElement).getW(); + newStretch += ((KnuthGlue) tempElement).getY(); + newShrink += ((KnuthGlue) tempElement).getZ(); + } else if (((KnuthPenalty) tempElement).getP() + == -KnuthElement.INFINITE + && tempElement != element) { + break; + } + } + + // add nodes to the active nodes list + for (int i = 0; i <= 3; i++) { + if (best.notInfiniteDemerits(i) + && best.getDemerits(i) + <= (best.getMinDemerits() + + incompatibleFitnessDemerit)) { + // the nodes in activeList must be ordered + // by line number and position; + // so: + // 1) advance in the list until the end, + // or a node with a higher line number, is reached + int iStepsForward = 0; + KnuthNode tempNode; + while (activeListIterator.hasNext()) { + iStepsForward ++; + tempNode = (KnuthNode) activeListIterator.next(); + if (tempNode.line > (best.getNode(i).line + 1)) { + activeListIterator.previous(); + iStepsForward --; + break; + } + } + // 2) add the new node + activeListIterator.add + (new KnuthNode(par.indexOf(element), + best.getNode(i).line + 1, i, + newWidth, newStretch, newShrink, + best.getAdjust(i), + best.getDifference(i), + best.getDemerits(i), + best.getNode(i))); + // 3) go back + for (int j = 0; + j <= iStepsForward; + j ++) { + activeListIterator.previous(); + } + } + } + } + if (activeNode == null) { + break; + } + } // end of the outer while + } + + /** + * find hyphenation points for every word int the current paragraph + * @ param currPar the paragraph whose words will be hyphenated + */ + private void findHyphenationPoints(Paragraph currPar){ + // hyphenate every word + ListIterator currParIterator + = currPar.listIterator(currPar.ignoreAtStart); + // list of TLM involved in hyphenation + LinkedList updateList = new LinkedList(); + KnuthElement firstElement = null; + KnuthElement nextElement = null; + // current TextLayoutManager + LayoutManager currLM = null; + // number of KnuthBox elements containing word fragments + int boxCount; + // number of auxiliary KnuthElements between KnuthBoxes + int auxCount; + StringBuffer sbChars = null; + + // find all hyphenation points + while (currParIterator.hasNext()) { + firstElement = (KnuthElement) currParIterator.next(); + // + if (firstElement.getLayoutManager() != currLM) { + currLM = firstElement.getLayoutManager(); + if (currLM != null) { + updateList.add(new Update(currLM, currParIterator.previousIndex())); + } else { + break; + } + } + + // collect word fragments, ignoring auxiliary elements; + // each word fragment was created by a different TextLM + if (firstElement.isBox() && !firstElement.isAuxiliary()) { + boxCount = 1; + auxCount = 0; + sbChars = new StringBuffer(); + currLM.getWordChars(sbChars, firstElement.getPosition()); + // look if next elements are boxes too + while (currParIterator.hasNext()) { + nextElement = (KnuthElement) currParIterator.next(); + if (nextElement.isBox() && !nextElement.isAuxiliary()) { + // a non-auxiliary KnuthBox: append word chars + if (currLM != nextElement.getLayoutManager()) { + currLM = nextElement.getLayoutManager(); + updateList.add(new Update(currLM, currParIterator.previousIndex())); + } + // append text to recreate the whole word + boxCount ++; + currLM.getWordChars(sbChars, nextElement.getPosition()); + } else if (!nextElement.isAuxiliary()) { + // a non-auxiliary non-box KnuthElement: stop + // go back to the last box or auxiliary element + currParIterator.previous(); + break; + } else { + // an auxiliary KnuthElement: simply ignore it + auxCount ++; + } + } + log.trace(" Word to hyphenate: " + sbChars.toString()); + // find hyphenation points + HyphContext hc = getHyphenContext(sbChars); + // ask each LM to hyphenate its word fragment + if (hc != null) { + KnuthElement element = null; + for (int i = 0; i < (boxCount + auxCount); i++) { + currParIterator.previous(); + } + for (int i = 0; i < (boxCount + auxCount); i++) { + element = (KnuthElement) currParIterator.next(); + if (element.isBox() && !element.isAuxiliary()) { + element.getLayoutManager().hyphenate(element.getPosition(), hc); + } else { + // nothing to do, element is an auxiliary KnuthElement + } + } + } + } + } + + // create iterator for the updateList + ListIterator updateListIterator = updateList.listIterator(); + Update currUpdate = null; + int iPreservedElements = 0; + int iAddedElements = 0; + int iRemovedElements = 0; + + while (updateListIterator.hasNext()) { + // ask the LMs to apply the changes and return + // the new KnuthElements to replace the old ones + currUpdate = (Update) updateListIterator.next(); + int fromIndex = currUpdate.iFirstIndex; + int toIndex; + if (updateListIterator.hasNext()) { + Update nextUpdate = (Update) updateListIterator.next(); + toIndex = nextUpdate.iFirstIndex; + updateListIterator.previous(); + } else { + // maybe this is not always correct! + toIndex = currPar.size() - currPar.ignoreAtEnd + - iAddedElements; + } + + // applyChanges() returns true if the LM modifies its data, + // so it must return new KnuthElements to replace the old ones + if (currUpdate.inlineLM + .applyChanges(currPar.subList(fromIndex + iAddedElements, + toIndex + iAddedElements))) { + // insert the new KnuthElements + LinkedList newElements = null; + newElements + = currUpdate.inlineLM.getChangedKnuthElements + (currPar.subList(fromIndex + iAddedElements, + toIndex + iAddedElements), + flaggedPenalty, effectiveAlignment); + // remove the old elements + currPar.subList(fromIndex + iAddedElements, + toIndex + iAddedElements).clear(); + // insert the new elements + currPar.addAll(fromIndex + iAddedElements, newElements); + iAddedElements += newElements.size() - (toIndex - fromIndex); + } + } + updateListIterator = null; + updateList.clear(); } private void resetBP(BreakPoss resetBP) { @@ -438,57 +1193,33 @@ public class LineLayoutManager extends InlineStackingLayoutManager { } } - private HyphContext getHyphenContext(BreakPoss prev, - BreakPoss newBP) { - // Get a "word" to hyphenate by getting characters from all - // pending break poss which are in vecInlineBreaks, starting - // with the position just AFTER prev.getPosition() - - vecInlineBreaks.add(newBP); - ListIterator bpIter = - vecInlineBreaks.listIterator(vecInlineBreaks.size()); - while (bpIter.hasPrevious() && bpIter.previous() != prev) { - } - if (prev != null && bpIter.next() != prev) { - log.error("findHyphenPoss: problem!"); - return null; - } - StringBuffer sbChars = new StringBuffer(30); - while (bpIter.hasNext()) { - BreakPoss bp = (BreakPoss) bpIter.next(); - if (prev != null && - bp.getLayoutManager() == prev.getLayoutManager()) { - bp.getLayoutManager().getWordChars(sbChars, - prev.getPosition(), bp.getPosition()); - } else { - bp.getLayoutManager().getWordChars(sbChars, null, - bp.getPosition()); - } - prev = bp; - } - vecInlineBreaks.remove(vecInlineBreaks.size() - 1); // remove last - log.debug("Word to hyphenate: " + sbChars.toString()); - - // Now find all hyphenation points in this word (get in an array of offsets) - // hyphProps are from the block level?. Note that according to the spec, - // they also "apply to" fo:character. I don't know what that means, since - // if we change language in the middle of a "word", the effect would seem - // quite strange! Or perhaps in that case, we say that it's several words. + private HyphContext getHyphenContext(StringBuffer sbChars) { + // Find all hyphenation points in this word + // (get in an array of offsets) + // hyphProps are from the block level?. + // Note that according to the spec, + // they also "apply to" fo:character. + // I don't know what that means, since + // if we change language in the middle of a "word", + // the effect would seem quite strange! + // Or perhaps in that case, we say that it's several words. // We probably should bring the hyphenation props up from the actual - // TextLM which generate the hyphenation buffer, since these properties - // inherit and could be specified on an inline or wrapper below the block - // level. - Hyphenation hyph = Hyphenator.hyphenate(hyphProps.language, - hyphProps.country, sbChars.toString(), - hyphProps.hyphenationRemainCharacterCount, - hyphProps.hyphenationPushCharacterCount); + // TextLM which generate the hyphenation buffer, + // since these properties inherit and could be specified + // on an inline or wrapper below the block level. + Hyphenation hyph + = Hyphenator.hyphenate(hyphProps.language, + hyphProps.country, sbChars.toString(), + hyphProps.hyphenationRemainCharacterCount, + hyphProps.hyphenationPushCharacterCount); // They hyph structure contains the information we need // Now start from prev: reset to that position, ask that LM to get // a Position for the first hyphenation offset. If the offset isn't in - // its characters, it returns null, but must tell how many chars it had. + // its characters, it returns null, + // but must tell how many chars it had. // Keep looking at currentBP using next hyphenation point until the - // returned size is greater than the available size or no more hyphenation - // points remain. Choose the best break. + // returned size is greater than the available size + // or no more hyphenation points remain. Choose the best break. if (hyph != null) { return new HyphContext(hyph.getHyphenationPoints()); } else { @@ -529,7 +1260,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager { LayoutManager lastLM = null; for (Iterator iter = vecInlineBreaks.listIterator(prevLineEnd); iter.hasNext();) { - BreakPoss bp = (BreakPoss)iter.next(); + BreakPoss bp = (BreakPoss) iter.next(); if (bp.getLead() > lineLead) { lineLead = bp.getLead(); } @@ -573,7 +1304,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager { if (actual.opt > targetWith) { if (actual.opt - targetWith < (actual.opt - actual.min)) { ipdAdjust = -(actual.opt - targetWith) - / (float)(actual.opt - actual.min); + / (float) (actual.opt - actual.min); realWidth = targetWith; } else { ipdAdjust = -1; @@ -582,7 +1313,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager { } else { if (targetWith - actual.opt < actual.max - actual.opt) { ipdAdjust = (targetWith - actual.opt) - / (float)(actual.max - actual.opt); + / (float) (actual.max - actual.opt); realWidth = targetWith; } else { ipdAdjust = 1; @@ -597,7 +1328,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager { switch (textalign) { case TextAlign.JUSTIFY: if (realWidth != 0) { - dAdjust = (double)(targetWith - realWidth) / realWidth; + dAdjust = (double) (targetWith - realWidth) / realWidth; } break; case TextAlign.START: @@ -612,9 +1343,6 @@ public class LineLayoutManager extends InlineStackingLayoutManager { indent = targetWith - realWidth; break; } - //System.err.println(" "); - //System.err.println("LineLayoutManager> difference to fill= " + (targetWith - realWidth)); - //System.err.println("LineLayoutManager> ipdAdjust= " + ipdAdjust + " dAdjust= " + dAdjust); LineBreakPosition lbp; lbp = new LineBreakPosition(this, @@ -635,16 +1363,21 @@ public class LineLayoutManager extends InlineStackingLayoutManager { */ public void resetPosition(Position resetPos) { if (resetPos == null) { - iStartPos = 0; - reset(null); - vecInlineBreaks.clear(); - prevBP = null; + setFinished(false); + iReturnedLBP = 0; } else { - prevBP = (BreakPoss)vecInlineBreaks.get(((LineBreakPosition)resetPos).getLeafPos()); - while (vecInlineBreaks.get(vecInlineBreaks.size() - 1) != prevBP) { - vecInlineBreaks.remove(vecInlineBreaks.size() - 1); + if (isFinished()) { + // if isFinished is true, iReturned LBP == breakpoints.size() + // and breakpoints.get(iReturnedLBP) would generate + // an IndexOutOfBoundException + setFinished(false); + iReturnedLBP--; } - reset(prevBP.getPosition()); + while ((LineBreakPosition) breakpoints.get(iReturnedLBP) + != (LineBreakPosition) resetPos) { + iReturnedLBP --; + } + iReturnedLBP ++; } } @@ -675,6 +1408,11 @@ public class LineLayoutManager extends InlineStackingLayoutManager { LayoutManager childLM; LayoutContext lc = new LayoutContext(0); while (parentIter.hasNext()) { + ListIterator paragraphIterator = null; + KnuthElement tempElement = null; + // the TLM which created the last KnuthElement in this line + LayoutManager lastLM = null; + LineBreakPosition lbp = (LineBreakPosition) parentIter.next(); LineArea lineArea = new LineArea(); lineArea.setStartIndent(lbp.startIndent); @@ -682,11 +1420,49 @@ public class LineLayoutManager extends InlineStackingLayoutManager { lc.setBaseline(lbp.baseline); lc.setLineHeight(lbp.lineHeight); setCurrentArea(lineArea); + + Paragraph currPar = (Paragraph) knuthParagraphs.get(iCurrParIndex); + iEndElement = lbp.getLeafPos(); + + // ignore the first elements added by the LineLayoutManager + iStartElement += (iStartElement == 0) ? currPar.ignoreAtStart : 0; + + // ignore the last elements added by the LineLayoutManager + iEndElement -= (iEndElement == (currPar.size() - 1)) + ? currPar.ignoreAtEnd : 0; + + // ignore the last element in the line if it is a KnuthGlue object + paragraphIterator = currPar.listIterator(iEndElement); + tempElement = (KnuthElement) paragraphIterator.next(); + if (tempElement.isGlue()) { + iEndElement --; + // this returns the same KnuthElement + paragraphIterator.previous(); + tempElement = (KnuthElement) paragraphIterator.previous(); + } + lastLM = tempElement.getLayoutManager(); + + // ignore KnuthGlue and KnuthPenalty objects + // at the beginning of the line + paragraphIterator = currPar.listIterator(iStartElement); + tempElement = (KnuthElement) paragraphIterator.next(); + while (!tempElement.isBox() && paragraphIterator.hasNext()) { + tempElement = (KnuthElement) paragraphIterator.next(); + iStartElement ++; + } + // Add the inline areas to lineArea - PositionIterator inlinePosIter = - new BreakPossPosIter(vecInlineBreaks, iStartPos, - lbp.getLeafPos() + 1); - iStartPos = lbp.getLeafPos() + 1; + PositionIterator inlinePosIter + = new KnuthPossPosIter(currPar, iStartElement, + iEndElement + 1); + + iStartElement = lbp.getLeafPos() + 1; + if (iStartElement == currPar.size()) { + // advance to next paragraph + iCurrParIndex ++; + iStartElement = 0; + } + lc.setSpaceAdjust(lbp.dAdjust); lc.setIPDAdjust(lbp.ipdAdjust); lc.setLeadingSpace(new SpaceSpecifier(true)); @@ -694,6 +1470,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager { lc.setFlags(LayoutContext.RESOLVE_LEADING_SPACE, true); setChildContext(lc); while ((childLM = inlinePosIter.getNextChildLM()) != null) { + lc.setFlags(LayoutContext.LAST_AREA, (childLM == lastLM)); childLM.addAreas(inlinePosIter, lc); lc.setLeadingSpace(lc.getTrailingSpace()); lc.setTrailingSpace(new SpaceSpecifier(false)); diff --git a/src/java/org/apache/fop/layoutmgr/TextLayoutManager.java b/src/java/org/apache/fop/layoutmgr/TextLayoutManager.java index 59ddcc73a..e3abe293c 100644 --- a/src/java/org/apache/fop/layoutmgr/TextLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/TextLayoutManager.java @@ -19,8 +19,12 @@ 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.Constants; import org.apache.fop.traits.SpaceVal; import org.apache.fop.area.Trait; import org.apache.fop.area.inline.InlineArea; @@ -45,18 +49,31 @@ public class TextLayoutManager extends AbstractLayoutManager { 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, - MinOptMax ipd, boolean bHyph) { + 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; @@ -85,6 +102,8 @@ public class TextLayoutManager extends AbstractLayoutManager { // 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 */ @@ -92,6 +111,12 @@ public class TextLayoutManager extends AbstractLayoutManager { /** Number of space characters after previous possible break position. */ private int iNbSpacesPending; + private boolean bChanged = false; + private int iReturnedIndex = 0; + private short iThisStart = 0; + private short iTempStart = 0; + private LinkedList changeList = null; + /** * Create a Text layout manager. * @@ -114,6 +139,23 @@ public class TextLayoutManager extends AbstractLayoutManager { SpaceVal ws = foText.textInfo.wordSpacing; halfWS = new SpaceVal(MinOptMax.multiply(ws.getSpace(), 0.5), ws.isConditional(), ws.isForcing(), ws.getPrecedence()); + SpaceVal ls = foText.textInfo.letterSpacing; + + // 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<>SIMPLE<>TEST + // 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 PropertyManager.getTextLayoutProps() method + letterSpaceIPD = new MinOptMax(ls.getSpace().min, + ls.getSpace().opt, ls.getSpace().max); + wordSpaceIPD = new MinOptMax(spaceCharIPD + ws.getSpace().min, + spaceCharIPD + ws.getSpace().opt, + spaceCharIPD + ws.getSpace().max); } /** @@ -141,8 +183,9 @@ public class TextLayoutManager extends AbstractLayoutManager { // Skip all leading spaces for hyphenation int i; for (i = ai.iStartIndex; - i < ai.iBreakIndex && CharUtilities.isAnySpace(textArray[i]) == true; - i++) { + i < ai.iBreakIndex + && CharUtilities.isAnySpace(textArray[i]) == true; + i++) { //nop } sbChars.append(new String(textArray, i, ai.iBreakIndex - i)); @@ -159,10 +202,12 @@ public class TextLayoutManager extends AbstractLayoutManager { */ public boolean canBreakBefore(LayoutContext context) { char c = textArray[iNextStart]; - return ((c == NEWLINE) || (foText.textInfo.bWrap + return ((c == NEWLINE) + || (foText.textInfo.bWrap && (CharUtilities.isBreakableSpace(c) - || (BREAK_CHARS.indexOf(c) >= 0 && (iNextStart == 0 - || Character.isLetterOrDigit(textArray[iNextStart-1])))))); + || (BREAK_CHARS.indexOf(c) >= 0 + && (iNextStart == 0 + || Character.isLetterOrDigit(textArray[iNextStart-1])))))); } /** @@ -174,8 +219,8 @@ public class TextLayoutManager extends AbstractLayoutManager { if (prevPos != null) { // ASSERT (prevPos.getLM() == this) if (prevPos.getLM() != this) { - log.error( - "TextLayoutManager.resetPosition: " + "LM mismatch!!!"); + log.error("TextLayoutManager.resetPosition: " + + "LM mismatch!!!"); } LeafPosition tbp = (LeafPosition) prevPos; AreaInfo ai = @@ -260,7 +305,7 @@ public class TextLayoutManager extends AbstractLayoutManager { */ if (context.suppressLeadingSpace()) { for (; iNextStart < textArray.length - && textArray[iNextStart] == SPACE; iNextStart++) { + && textArray[iNextStart] == SPACE; iNextStart++) { } // If now at end, nothing to compose here! if (iNextStart >= textArray.length) { @@ -293,7 +338,7 @@ public class TextLayoutManager extends AbstractLayoutManager { ++iWScount; // Counted as word-space if (iNextStart == iThisStart - && (iFlags & BreakPoss.ISFIRST) != 0) { + && (iFlags & BreakPoss.ISFIRST) != 0) { // If possible, treat as normal inter-word space if (context.getLeadingSpace().hasSpaces()) { context.getLeadingSpace().addSpace(halfWS); @@ -330,8 +375,8 @@ public class TextLayoutManager extends AbstractLayoutManager { iFlags |= BreakPoss.ALL_ARE_SUPPRESS_AT_LB; } return makeBreakPoss(iThisStart, spaceIPD, wordIPD, - context.getLeadingSpace(), pendingSpace, iFlags, - iWScount); + context.getLeadingSpace(), pendingSpace, + iFlags, iWScount); } if (context.tryHyphenate()) { @@ -356,7 +401,8 @@ public class TextLayoutManager extends AbstractLayoutManager { if (c != SPACE) { iNextStart++; if (c != NEWLINE) { - wordIPD += foText.textInfo.fs.getCharWidth(c); + wordIPD + += foText.textInfo.fs.getCharWidth(c); } else { iFlags |= BreakPoss.FORCE; } @@ -373,8 +419,8 @@ public class TextLayoutManager extends AbstractLayoutManager { iFlags |= BreakPoss.REST_ARE_SUPPRESS_AT_LB; } return makeBreakPoss(iThisStart, spaceIPD, wordIPD, - context.getLeadingSpace(), null, iFlags, - iWScount); + context.getLeadingSpace(), null, + iFlags, iWScount); } wordIPD += foText.textInfo.fs.getCharWidth(c); // Note, if a normal non-breaking space, is it stretchable??? @@ -382,12 +428,14 @@ public class TextLayoutManager extends AbstractLayoutManager { } } return makeBreakPoss(iThisStart, spaceIPD, wordIPD, - context.getLeadingSpace(), null, iFlags, iWScount); + context.getLeadingSpace(), null, + iFlags, iWScount); } private BreakPoss makeBreakPoss(short iWordStart, MinOptMax spaceIPD, int wordDim, - SpaceSpecifier leadingSpace, SpaceSpecifier trailingSpace, + SpaceSpecifier leadingSpace, + SpaceSpecifier trailingSpace, int flags, short iWScount) { MinOptMax ipd = new MinOptMax(wordDim); ipd.add(spaceIPD); @@ -397,16 +445,12 @@ public class TextLayoutManager extends AbstractLayoutManager { // Note: break position now stores total size to here // Position is the index of the info for this word in the vector - vecAreaInfo.add( - new AreaInfo(iWordStart, iNextStart, iWScount, ipd, - ((flags & BreakPoss.HYPHENATED) != 0))); - BreakPoss bp = new BreakPoss( - new LeafPosition(this, vecAreaInfo.size() - 1)); + 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))); + bp.setStackingSize(MinOptMax.add(ipd, new MinOptMax(hyphIPD))); } else { bp.setStackingSize(ipd); } @@ -449,91 +493,108 @@ public class TextLayoutManager extends AbstractLayoutManager { * @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; } - // ignore newline character + // 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); + String str = new String(textArray, iStart, + ai.iBreakIndex - iStart - adjust); // add hyphenation character if the last word is hyphenated - if (ai.bHyphenated) { + if (context.isLastArea() && ai.bHyphenated) { str += foText.textInfo.hyphChar; - ai.ipdArea = MinOptMax.add(ai.ipdArea, new MinOptMax(hyphIPD)); + realWidth.add(new MinOptMax(hyphIPD)); } - // Calculate total adjustment - int iRealWidth = ai.ipdArea.opt; - int iAdjust = 0; + // Calculate adjustments + int iDifference = 0; + int iTotalAdjust = 0; + int iWordSpaceDim = wordSpaceIPD.opt; + int iLetterSpaceDim = letterSpaceIPD.opt; double dIPDAdjust = context.getIPDAdjust(); - double dSpaceAdjust = context.getSpaceAdjust(); + double dSpaceAdjust = context.getSpaceAdjust(); // not used + + // calculate total difference between real and available width if (dIPDAdjust > 0.0) { - iRealWidth += (int)((double)(ai.ipdArea.max - ai.ipdArea.opt) * dIPDAdjust); - } - else { - iRealWidth += (int)((double)(ai.ipdArea.opt - ai.ipdArea.min) * dIPDAdjust); + iDifference = (int) ((double) (realWidth.max - realWidth.opt) + * dIPDAdjust); + } else { + iDifference = (int) ((double) (realWidth.opt - realWidth.min) + * dIPDAdjust); } - iAdjust = (int)((double)(iRealWidth * dSpaceAdjust)); - //System.err.println(" "); - //System.err.println("TextLayoutManager> recreated difference to fill= " + iAdjust); - - // Make an area containing all characters between start and end. - InlineArea word = null; - - if (" ".equals(str)) { - word = new Space(); - word.setWidth(ai.ipdArea.opt + iAdjust); + + // set letter space adjustment + if (dIPDAdjust > 0.0) { + iLetterSpaceDim + += (int) ((double) (letterSpaceIPD.max - letterSpaceIPD.opt) + * dIPDAdjust); } else { - TextArea t = createTextArea(str, iRealWidth + iAdjust, - context.getBaseline()); - if (iWScount > 0) { - //getLogger().error("Adjustment per word-space= " + - // iAdjust / iWScount); - t.setTextSpaceAdjust(iAdjust / iWScount); - //System.err.println("TextLayoutManager> word spaces= " + iWScount + " adjustment per word space= " + (iAdjust/iWScount)); - } - word = t; - } - if ((textArray[iStart] == SPACE || textArray[iStart] == NBSPACE) - && context.getLeadingSpace().hasSpaces()) { - context.getLeadingSpace().addSpace(halfWS); + iLetterSpaceDim + += (int) ((double) (letterSpaceIPD.opt - letterSpaceIPD.min) + * dIPDAdjust); } - // Set LAST flag if done making characters - int iLastChar; - for (iLastChar = ai.iBreakIndex; - iLastChar < textArray.length && textArray[iLastChar] == SPACE; - iLastChar++) { - //nop - } - context.setFlags(LayoutContext.LAST_AREA, - iLastChar == textArray.length); - - // Can we have any trailing space? Yes, if last char was a space! - context.setTrailingSpace(new SpaceSpecifier(false)); - if (textArray[ai.iBreakIndex - 1] == SPACE - || textArray[ai.iBreakIndex - 1] == NBSPACE) { - context.getTrailingSpace().addSpace(halfWS); + 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.getBaseline()); + + // 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.addChild(word); } @@ -552,17 +613,400 @@ public class TextLayoutManager extends AbstractLayoutManager { TextArea textArea = new TextArea(); textArea.setWidth(width); textArea.setHeight(foText.textInfo.fs.getAscender() - - foText.textInfo.fs.getDescender()); + - foText.textInfo.fs.getDescender()); textArea.setOffset(foText.textInfo.fs.getAscender()); textArea.setOffset(base); textArea.setTextArea(str); textArea.addTrait(Trait.FONT_NAME, foText.textInfo.fs.getFontName()); textArea.addTrait(Trait.FONT_SIZE, - new Integer(foText.textInfo.fs.getFontSize())); + new Integer(foText.textInfo.fs.getFontSize())); textArea.addTrait(Trait.COLOR, foText.textInfo.color); return textArea; } + public LinkedList getNextKnuthElements(LayoutContext context, + int alignment) { + LinkedList returnList = new LinkedList(); + + while (iNextStart < textArray.length) { + if (textArray[iNextStart] == SPACE) { + // normal, breaking space + switch (alignment) { + case CENTER : + 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, + - 6 * wordSpaceIPD.opt, 0, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthBox(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 * wordSpaceIPD.opt, 0, + new LeafPosition(this, -1), true)); + iNextStart ++; + break; + + case START : // fall through + case 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 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" + // the linefeed character is the first one in textArray, + // so we can just return a list with a penalty item + 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; + iTempStart ++) { + // ignore newline characters + if (textArray[iTempStart] != NEWLINE) { + wordIPD.add + (new MinOptMax(foText.textInfo.fs.getCharWidth(textArray[iTempStart]))); + } + } + wordIPD.add(MinOptMax.multiply(letterSpaceIPD, (iTempStart - iThisStart - 1))); + vecAreaInfo.add + (new AreaInfo(iThisStart, iTempStart, (short) 0, + (short) (iTempStart - iThisStart - 1), + wordIPD, false)); + if (letterSpaceIPD.min == letterSpaceIPD.max) { + // constant letter space; simply return a box + // whose width includes letter spaces + returnList.add + (new KnuthBox(wordIPD.opt, 0, 0, 0, + new LeafPosition(this, vecAreaInfo.size() - 1), false)); + iNextStart = iTempStart; + } else { + // adjustable letter space; + // some other KnuthElements are needed + returnList.add + (new KnuthBox(wordIPD.opt - (iTempStart - iThisStart - 1) * letterSpaceIPD.opt, 0, 0, 0, + new LeafPosition(this, vecAreaInfo.size() - 1), false)); + returnList.add + (new KnuthPenalty(0, KnuthElement.INFINITE, false, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthGlue((iTempStart - iThisStart - 1) * letterSpaceIPD.opt, + (iTempStart - iThisStart - 1) * (letterSpaceIPD.max - letterSpaceIPD.opt), + (iTempStart - iThisStart - 1) * (letterSpaceIPD.opt - letterSpaceIPD.min), + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthBox(0, 0, 0, 0, + new LeafPosition(this, -1), true)); + iNextStart = iTempStart; + } + } + } // end of while + setFinished(true); + if (returnList.size() > 0) { + return returnList; + } else { + return null; + } + } + + public int getWordSpaceIPD() { + return wordSpaceIPD.opt; + } + + public KnuthElement addALetterSpaceTo(KnuthElement element) { + LeafPosition pos = (LeafPosition) element.getPosition(); + AreaInfo ai = (AreaInfo) vecAreaInfo.get(pos.getLeafPos()); + ai.iLScount ++; + ai.ipdArea.add(letterSpaceIPD); + if (letterSpaceIPD.min == letterSpaceIPD.max) { + return new KnuthBox(ai.ipdArea.opt, 0, 0, 0, pos, false); + } else { + return new KnuthGlue(ai.iLScount * letterSpaceIPD.opt, + ai.iLScount * (letterSpaceIPD.max - letterSpaceIPD.opt), + ai.iLScount * (letterSpaceIPD.opt - letterSpaceIPD.min), + new LeafPosition(this, -1), true); + } + } + + 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(foText.textInfo.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 (letterSpaceIPD.min == letterSpaceIPD.max) { + returnList.add + (new KnuthBox(ai.ipdArea.opt, 0, 0, 0, + new LeafPosition(this, iReturnedIndex), false)); + } else { + returnList.add + (new KnuthBox(ai.ipdArea.opt + - ai.iLScount * letterSpaceIPD.opt, + 0, 0, 0, + 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 KnuthBox(0, 0, 0, 0, + new LeafPosition(this, -1), true)); + } + if (ai.bHyphenated) { + returnList.add + (new KnuthPenalty(hyphIPD, flaggedPenalty, true, + new LeafPosition(this, -1), false)); + } + iReturnedIndex ++; + } else { + // ai refers to a space + switch (alignment) { + case CENTER : + returnList.add + (new KnuthGlue(0, 3 * wordSpaceIPD.opt, 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 * wordSpaceIPD.opt, 0, + new LeafPosition(this, -1), true)); + returnList.add + (new KnuthBox(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 * wordSpaceIPD.opt, 0, + new LeafPosition(this, -1), true)); + iReturnedIndex ++; + break; + case START : // fall through + case END : + returnList.add + (new KnuthGlue(0, 3 * wordSpaceIPD.opt, 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 * wordSpaceIPD.opt, 0, + new LeafPosition(this, -1), true)); + iReturnedIndex ++; + break; + case 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)); + } + } } diff --git a/src/java/org/apache/fop/render/pdf/PDFRenderer.java b/src/java/org/apache/fop/render/pdf/PDFRenderer.java index b2a92158a..38ebf4fd6 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRenderer.java +++ b/src/java/org/apache/fop/render/pdf/PDFRenderer.java @@ -905,6 +905,70 @@ public class PDFRenderer extends PrintRenderer { * @see org.apache.fop.render.Renderer#renderCharacter(Character) */ public void renderCharacter(Character ch) { + StringBuffer pdf = new StringBuffer(); + + String name = (String) ch.getTrait(Trait.FONT_NAME); + int size = ((Integer) ch.getTrait(Trait.FONT_SIZE)).intValue(); + + // This assumes that *all* CIDFonts use a /ToUnicode mapping + Typeface f = (Typeface) fontInfo.getFonts().get(name); + boolean useMultiByte = f.isMultiByte(); + + // String startText = useMultiByte ? " " : ") "; + + updateFont(name, size, pdf); + ColorType ct = (ColorType) ch.getTrait(Trait.COLOR); + if (ct != null) { + updateColor(ct, true, pdf); + } + + // word.getOffset() = only height of text itself + // currentBlockIPPosition: 0 for beginning of line; nonzero + // where previous line area failed to take up entire allocated space + int rx = currentBlockIPPosition + ipMarginOffset; + int bl = currentBPPosition + bpMarginOffset + ch.getOffset(); + +/* System.out.println("Text = " + ch.getTextArea() + + "; text width: " + ch.getWidth() + + "; BlockIP Position: " + currentBlockIPPosition + + "; currentBPPosition: " + currentBPPosition + + "; offset: " + ch.getOffset()); +*/ + // Set letterSpacing + //float ls = fs.getLetterSpacing() / this.currentFontSize; + //pdf.append(ls).append(" Tc\n"); + + if (!textOpen || bl != prevWordY) { + closeText(); + + pdf.append("1 0 0 -1 " + (rx / 1000f) + " " + (bl / 1000f) + " Tm " + + (ch.getTextLetterSpaceAdjust() / 1000f) + " Tc " + + (ch.getTextWordSpaceAdjust() / 1000f) + " Tw [" + startText); + prevWordY = bl; + textOpen = true; + } else { + closeText(); + + pdf.append("1 0 0 -1 " + (rx / 1000f) + " " + (bl / 1000f) + " Tm " + + (ch.getTextLetterSpaceAdjust() / 1000f) + " Tc " + + (ch.getTextWordSpaceAdjust() / 1000f) + " Tw [" + startText); + textOpen = true; + } + prevWordWidth = ch.getWidth(); + prevWordX = rx; + + String s = ch.getChar(); + + + FontMetrics metrics = fontInfo.getMetricsFor(name); + Font fs = new Font(name, metrics, size); + escapeText(s, fs, useMultiByte, pdf); + pdf.append(endText); + + currentStream.add(pdf.toString()); + super.renderCharacter(ch); } @@ -951,14 +1015,16 @@ public class PDFRenderer extends PrintRenderer { closeText(); pdf.append("1 0 0 -1 " + (rx / 1000f) + " " + (bl / 1000f) + " Tm " - + (text.getTextSpaceAdjust() / 1000f) + " Tw [" + startText); + + (text.getTextLetterSpaceAdjust() / 1000f) + " Tc " + + (text.getTextWordSpaceAdjust() / 1000f) + " Tw [" + startText); prevWordY = bl; textOpen = true; } else { closeText(); pdf.append("1 0 0 -1 " + (rx / 1000f) + " " + (bl / 1000f) + " Tm " - + (text.getTextSpaceAdjust() / 1000f) + " Tw [" + startText); + + (text.getTextLetterSpaceAdjust() / 1000f) + " Tc " + + (text.getTextWordSpaceAdjust() / 1000f) + " Tw [" + startText); textOpen = true; } prevWordWidth = text.getWidth(); diff --git a/src/java/org/apache/fop/render/xml/XMLRenderer.java b/src/java/org/apache/fop/render/xml/XMLRenderer.java index 6a3be0847..91c7d9322 100644 --- a/src/java/org/apache/fop/render/xml/XMLRenderer.java +++ b/src/java/org/apache/fop/render/xml/XMLRenderer.java @@ -427,7 +427,8 @@ public class XMLRenderer extends AbstractRenderer { if (map != null) { prop = " props=\"" + getPropString(map) + "\""; } - writeElement("" + text.getTextArea() + ""); super.renderText(text); } -- cgit v1.2.3