| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.layoutmgr.inline; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.area.Area; |
| import org.apache.fop.area.LineArea; |
| import org.apache.fop.area.Trait; |
| import org.apache.fop.area.inline.InlineArea; |
| import org.apache.fop.complexscripts.bidi.BidiResolver; |
| import org.apache.fop.datatypes.Length; |
| import org.apache.fop.datatypes.Numeric; |
| import org.apache.fop.fo.Constants; |
| import org.apache.fop.fo.flow.Block; |
| import org.apache.fop.fo.properties.CommonHyphenation; |
| import org.apache.fop.fo.properties.KeepProperty; |
| import org.apache.fop.fonts.Font; |
| import org.apache.fop.fonts.FontInfo; |
| import org.apache.fop.fonts.FontTriplet; |
| import org.apache.fop.hyphenation.Hyphenation; |
| import org.apache.fop.hyphenation.Hyphenator; |
| import org.apache.fop.layoutmgr.Adjustment; |
| import org.apache.fop.layoutmgr.BlockLayoutManager; |
| import org.apache.fop.layoutmgr.BlockLevelLayoutManager; |
| import org.apache.fop.layoutmgr.BreakElement; |
| import org.apache.fop.layoutmgr.BreakingAlgorithm; |
| import org.apache.fop.layoutmgr.ElementListObserver; |
| import org.apache.fop.layoutmgr.FloatContentLayoutManager; |
| import org.apache.fop.layoutmgr.FootenoteUtil; |
| import org.apache.fop.layoutmgr.FootnoteBodyLayoutManager; |
| import org.apache.fop.layoutmgr.InlineKnuthSequence; |
| import org.apache.fop.layoutmgr.Keep; |
| import org.apache.fop.layoutmgr.KnuthBlockBox; |
| import org.apache.fop.layoutmgr.KnuthBox; |
| import org.apache.fop.layoutmgr.KnuthElement; |
| import org.apache.fop.layoutmgr.KnuthGlue; |
| import org.apache.fop.layoutmgr.KnuthPenalty; |
| import org.apache.fop.layoutmgr.KnuthPossPosIter; |
| import org.apache.fop.layoutmgr.KnuthSequence; |
| import org.apache.fop.layoutmgr.LayoutContext; |
| import org.apache.fop.layoutmgr.LayoutManager; |
| import org.apache.fop.layoutmgr.LeafPosition; |
| import org.apache.fop.layoutmgr.ListElement; |
| import org.apache.fop.layoutmgr.NonLeafPosition; |
| import org.apache.fop.layoutmgr.Position; |
| import org.apache.fop.layoutmgr.PositionIterator; |
| import org.apache.fop.layoutmgr.SpaceSpecifier; |
| import org.apache.fop.traits.MinOptMax; |
| |
| /** |
| * LayoutManager for lines. It builds one or more lines containing |
| * inline areas generated by its sub layout managers. |
| * A break is found for each line which may contain one of more |
| * breaks from the child layout managers. |
| * Once a break is found then it is return for the parent layout |
| * manager to handle. |
| * When the areas are being added to the page this manager |
| * creates a line area to contain the inline areas added by the |
| * child layout managers. |
| */ |
| public class LineLayoutManager extends InlineStackingLayoutManager |
| implements BlockLevelLayoutManager { |
| |
| /** |
| * this constant is used to create elements when text-align is center: |
| * every TextLM descendant of LineLM must use the same value, |
| * otherwise the line breaking algorithm does not find the right |
| * break point |
| */ |
| public static final int DEFAULT_SPACE_WIDTH = 3336; |
| |
| /** |
| * logging instance |
| */ |
| private static Log log = LogFactory.getLog(LineLayoutManager.class); |
| |
| private final Block fobj; |
| private boolean isFirstInBlock; |
| |
| /** |
| * Private class to store information about inline breaks. |
| * Each value holds the start and end indexes into a List of |
| * inline break positions. |
| */ |
| static class LineBreakPosition extends LeafPosition { |
| private final int parIndex; // index of the Paragraph this Position refers to |
| private final int startIndex; //index of the first element this Position refers to |
| private final int availableShrink; |
| private final int availableStretch; |
| private final int difference; |
| private final double dAdjust; // Percentage to adjust (stretch or shrink) |
| private final double ipdAdjust; // Percentage to adjust (stretch or shrink) |
| private final int startIndent; |
| private final int endIndent; |
| private final int lineHeight; |
| private final int lineWidth; |
| private final int spaceBefore; |
| private final int spaceAfter; |
| private final int baseline; |
| |
| LineBreakPosition(LayoutManager lm, int index, int startIndex, int breakIndex, |
| int shrink, int stretch, int diff, double ipdA, double adjust, int si, |
| int ei, int lh, int lw, int sb, int sa, int bl) { |
| super(lm, breakIndex); |
| availableShrink = shrink; |
| availableStretch = stretch; |
| difference = diff; |
| parIndex = index; |
| this.startIndex = startIndex; |
| ipdAdjust = ipdA; |
| dAdjust = adjust; |
| startIndent = si; |
| endIndent = ei; |
| lineHeight = lh; |
| lineWidth = lw; |
| spaceBefore = sb; |
| spaceAfter = sa; |
| baseline = bl; |
| } |
| |
| } |
| |
| |
| private int bidiLevel = -1; |
| private int textAlignment = EN_JUSTIFY; |
| private int textAlignmentLast; |
| private int effectiveAlignment; |
| private Length textIndent; |
| private Length lastLineEndIndent; |
| private CommonHyphenation hyphenationProperties; |
| private Numeric hyphenationLadderCount; |
| private int wrapOption = EN_WRAP; |
| private int whiteSpaceTreament; |
| //private LayoutProps layoutProps; |
| |
| private final Length lineHeight; |
| private final int lead; |
| private final int follow; |
| private AlignmentContext alignmentContext; |
| |
| private int baselineOffset = -1; |
| |
| private List<KnuthSequence> knuthParagraphs; |
| |
| private LineLayoutPossibilities lineLayouts; |
| private LineLayoutPossibilities[] lineLayoutsList; |
| private int ipd; |
| /** |
| * When layout must be re-started due to a change of IPD, there is no need |
| * to perform hyphenation on the remaining Knuth sequence once again. |
| */ |
| private boolean hyphenationPerformed; |
| |
| /** |
| * This class is used to remember |
| * which was the first element in the paragraph |
| * returned by each LM. |
| */ |
| private final class Update { |
| private final InlineLevelLayoutManager inlineLM; |
| private final int firstIndex; |
| |
| private Update(InlineLevelLayoutManager lm, int index) { |
| inlineLM = lm; |
| firstIndex = index; |
| } |
| } |
| |
| // this class represents a paragraph |
| private static class Paragraph extends InlineKnuthSequence { |
| |
| private static final long serialVersionUID = 5862072380375189105L; |
| |
| /** Number of elements to ignore at the beginning of the list. */ |
| private int ignoreAtStart; |
| /** Number of elements to ignore at the end of the list. */ |
| private int ignoreAtEnd; |
| |
| // space at the end of the last line (in millipoints) |
| private MinOptMax lineFiller; |
| private final int textAlignment; |
| private final int textAlignmentLast; |
| private final int textIndent; |
| private final int lastLineEndIndent; |
| // the LM which created the paragraph |
| private final LineLayoutManager layoutManager; |
| |
| Paragraph(LineLayoutManager llm, int alignment, int alignmentLast, |
| int indent, int endIndent) { |
| super(); |
| layoutManager = llm; |
| textAlignment = alignment; |
| textAlignmentLast = alignmentLast; |
| textIndent = indent; |
| lastLineEndIndent = endIndent; |
| } |
| |
| @Override |
| public void startSequence() { |
| // set the minimum amount of empty space at the end of the |
| // last line |
| if (textAlignment == EN_CENTER) { |
| lineFiller = MinOptMax.getInstance(lastLineEndIndent); |
| } else { |
| lineFiller = MinOptMax.getInstance(lastLineEndIndent, lastLineEndIndent, |
| layoutManager.ipd); |
| } |
| |
| // add auxiliary elements at the beginning of the paragraph |
| if (textAlignment == EN_CENTER && textAlignmentLast != EN_JUSTIFY) { |
| this.add(new KnuthGlue(0, 3 * DEFAULT_SPACE_WIDTH, 0, |
| null, false)); |
| ignoreAtStart++; |
| } |
| |
| // add the element representing text indentation |
| // at the beginning of the first paragraph |
| if (layoutManager.isFirstInBlock && layoutManager.knuthParagraphs.size() == 0 |
| && textIndent != 0) { |
| this.add(new KnuthInlineBox(textIndent, null, |
| null, false)); |
| ignoreAtStart++; |
| } |
| } |
| |
| public void endParagraph() { |
| KnuthSequence finishedPar = this.endSequence(); |
| if (finishedPar != null) { |
| layoutManager.knuthParagraphs.add(finishedPar); |
| } |
| } |
| |
| @Override |
| public KnuthSequence endSequence() { |
| if (this.size() > ignoreAtStart) { |
| if (textAlignment == EN_CENTER |
| && textAlignmentLast != EN_JUSTIFY) { |
| this.add(new KnuthGlue(0, 3 * DEFAULT_SPACE_WIDTH, 0, |
| null, false)); |
| this.add(new KnuthPenalty(lineFiller.getOpt(), -KnuthElement.INFINITE, |
| false, null, false)); |
| ignoreAtEnd = 2; |
| } else if (textAlignmentLast != EN_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(0, |
| lineFiller.getStretch(), |
| lineFiller.getShrink(), null, false)); |
| this.add(new KnuthPenalty(lineFiller.getOpt(), -KnuthElement.INFINITE, |
| false, null, false)); |
| ignoreAtEnd = 3; |
| } else { |
| // add only the element representing the forced break |
| this.add(new KnuthPenalty(lineFiller.getOpt(), -KnuthElement.INFINITE, |
| false, null, false)); |
| ignoreAtEnd = 1; |
| } |
| return this; |
| } else { |
| this.clear(); |
| return null; |
| } |
| } |
| |
| /** |
| * @return true if the sequence contains a box |
| */ |
| public boolean containsBox() { |
| for (Object o : this) { |
| KnuthElement el = (KnuthElement) o; |
| if (el.isBox()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| |
| private class LineBreakingAlgorithm extends BreakingAlgorithm { |
| private final LineLayoutManager thisLLM; |
| private final int pageAlignment; |
| private int activePossibility; |
| private int addedPositions; |
| private final int textIndent; |
| private final int lineHeight; |
| private final int lead; |
| private final int follow; |
| private static final double MAX_DEMERITS = 10e6; |
| |
| public LineBreakingAlgorithm(int pageAlign, int textAlign, int textAlignLast, int indent, int fillerWidth, |
| int lh, int ld, int fl, boolean first, int maxFlagCount, LineLayoutManager llm) { |
| super(textAlign, textAlignLast, first, false, maxFlagCount); |
| pageAlignment = pageAlign; |
| textIndent = indent; |
| lineHeight = lh; |
| lead = ld; |
| follow = fl; |
| thisLLM = llm; |
| activePossibility = -1; |
| } |
| |
| @Override |
| public void updateData1(int lineCount, double demerits) { |
| lineLayouts.addPossibility(lineCount, demerits); |
| if (log.isTraceEnabled()) { |
| log.trace("Layout possibility in " + lineCount + " lines; break at position:"); |
| } |
| } |
| |
| @Override |
| public void updateData2(KnuthNode bestActiveNode, |
| KnuthSequence par, |
| int total) { |
| // compute indent and adjustment ratio, according to |
| // the value of text-align and text-align-last |
| int startIndent; |
| int endIndent; |
| int difference = bestActiveNode.difference; |
| int textAlign = (bestActiveNode.line < total) ? alignment : alignmentLast; |
| |
| switch (textAlign) { |
| case Constants.EN_START: |
| startIndent = 0; |
| endIndent = difference > 0 ? difference : 0; |
| break; |
| case Constants.EN_END: |
| startIndent = difference; |
| endIndent = 0; |
| break; |
| case Constants.EN_CENTER: |
| startIndent = difference / 2; |
| endIndent = startIndent; |
| break; |
| default: |
| case Constants.EN_JUSTIFY: |
| startIndent = 0; |
| endIndent = 0; |
| break; |
| } |
| |
| /* |
| startIndent += (textAlign == Constants.EN_CENTER) |
| ? difference / 2 : (textAlign == Constants.EN_END) ? difference : 0; |
| */ |
| startIndent += (bestActiveNode.line == 1 && indentFirstPart && isFirstInBlock) |
| ? textIndent : 0; |
| |
| double ratio = (textAlign == Constants.EN_JUSTIFY |
| || difference < 0 && -difference <= bestActiveNode.availableShrink) |
| ? bestActiveNode.adjustRatio : 0; |
| |
| // add nodes at the beginning of the list, as they are found |
| // backwards, from the last one to the first one |
| |
| // the first time this method is called, initialize activePossibility |
| if (activePossibility == -1) { |
| activePossibility = 0; |
| addedPositions = 0; |
| } |
| |
| if (addedPositions == lineLayouts.getLineCount(activePossibility)) { |
| activePossibility++; |
| addedPositions = 0; |
| } |
| |
| int lack = difference + bestActiveNode.availableShrink; |
| // if this LLM is nested inside a BlockContainerLayoutManager that is constraining |
| // the available width and thus responsible for the overflow then we do not issue |
| // warning event here and instead let the BCLM handle that at a later stage |
| if (lack < 0 && !handleOverflow(-lack)) { |
| InlineLevelEventProducer eventProducer |
| = InlineLevelEventProducer.Provider.get( |
| getFObj().getUserAgent().getEventBroadcaster()); |
| if (curChildLM.getFObj() == null) { |
| eventProducer.lineOverflows(this, getFObj().getName(), bestActiveNode.line, |
| -lack, getFObj().getLocator()); |
| } else { |
| eventProducer.lineOverflows(this, curChildLM.getFObj().getName(), bestActiveNode.line, |
| -lack, curChildLM.getFObj().getLocator()); |
| } |
| } |
| |
| //log.debug("LLM> (" + (lineLayouts.getLineNumber(activePossibility) - addedPositions) |
| // + ") difference = " + difference + " ratio = " + ratio); |
| lineLayouts.addBreakPosition(makeLineBreakPosition(par, |
| (bestActiveNode.line > 1 ? bestActiveNode.previous.position + 1 : 0), |
| bestActiveNode.position, |
| bestActiveNode.availableShrink - (addedPositions > 0 |
| ? 0 : ((Paragraph) par).lineFiller.getShrink()), |
| bestActiveNode.availableStretch, |
| difference, ratio, startIndent, endIndent), activePossibility); |
| addedPositions++; |
| } |
| |
| /* reset activePossibility, as if breakpoints have not yet been computed |
| */ |
| public void resetAlgorithm() { |
| activePossibility = -1; |
| } |
| |
| private LineBreakPosition makeLineBreakPosition(KnuthSequence par, int firstElementIndex, int lastElementIndex, |
| int availableShrink, int availableStretch, int difference, double ratio, |
| int startIndent, int endIndent) { |
| // line height calculation - spaceBefore may differ from spaceAfter |
| // by 1mpt due to rounding |
| int spaceBefore = (lineHeight - lead - follow) / 2; |
| int spaceAfter = lineHeight - lead - follow - spaceBefore; |
| // height before the main baseline |
| int lineLead = lead; |
| // maximum follow |
| int lineFollow = follow; |
| // true if this line contains only zero-height, auxiliary boxes |
| // and the actual line width is 0; in this case, the line "collapses" |
| // i.e. the line area will have bpd = 0 |
| boolean isZeroHeightLine = (difference == ipd); |
| |
| // if line-stacking-strategy is "font-height", the line height |
| // is not affected by its content |
| if (fobj.getLineStackingStrategy() != EN_FONT_HEIGHT) { |
| ListIterator inlineIterator |
| = par.listIterator(firstElementIndex); |
| AlignmentContext lastAC = null; |
| int maxIgnoredHeight = 0; // See spec 7.13 |
| for (int j = firstElementIndex; |
| j <= lastElementIndex; |
| j++) { |
| KnuthElement element = (KnuthElement) inlineIterator.next(); |
| if (element instanceof KnuthInlineBox) { |
| AlignmentContext ac = ((KnuthInlineBox) element).getAlignmentContext(); |
| if (ac != null && lastAC != ac) { |
| if (!ac.usesInitialBaselineTable() |
| || ac.getAlignmentBaselineIdentifier() != EN_BEFORE_EDGE |
| && ac.getAlignmentBaselineIdentifier() != EN_AFTER_EDGE) { |
| if (fobj.getLineHeightShiftAdjustment() == EN_CONSIDER_SHIFTS |
| || ac.getBaselineShiftValue() == 0) { |
| int alignmentOffset = ac.getTotalAlignmentBaselineOffset(); |
| if (alignmentOffset + ac.getAltitude() > lineLead) { |
| lineLead = alignmentOffset + ac.getAltitude(); |
| } |
| if (ac.getDepth() - alignmentOffset > lineFollow) { |
| lineFollow = ac.getDepth() - alignmentOffset; |
| } |
| } |
| } else { |
| if (ac.getHeight() > maxIgnoredHeight) { |
| maxIgnoredHeight = ac.getHeight(); |
| } |
| } |
| lastAC = ac; |
| } |
| if (isZeroHeightLine |
| && (!element.isAuxiliary() || ac != null && ac.getHeight() > 0)) { |
| isZeroHeightLine = false; |
| } |
| } |
| } |
| |
| if (lineFollow < maxIgnoredHeight - lineLead) { |
| lineFollow = maxIgnoredHeight - lineLead; |
| } |
| } |
| |
| constantLineHeight = lineLead + lineFollow; |
| |
| if (isZeroHeightLine) { |
| return new LineBreakPosition(thisLLM, |
| knuthParagraphs.indexOf(par), |
| firstElementIndex, lastElementIndex, |
| availableShrink, availableStretch, |
| difference, ratio, 0, startIndent, endIndent, |
| 0, ipd, 0, 0, 0); |
| } else { |
| return new LineBreakPosition(thisLLM, |
| knuthParagraphs.indexOf(par), |
| firstElementIndex, lastElementIndex, |
| availableShrink, availableStretch, |
| difference, ratio, 0, startIndent, endIndent, |
| lineLead + lineFollow, |
| ipd, spaceBefore, spaceAfter, |
| lineLead); |
| } |
| } |
| |
| @Override |
| protected int filterActiveNodes() { |
| KnuthNode bestActiveNode = null; |
| |
| if (pageAlignment == EN_JUSTIFY) { |
| // leave all active nodes and find the optimum line number |
| //log.debug("LBA.filterActiveNodes> " + activeNodeCount + " layouts"); |
| for (int i = startLine; i < endLine; i++) { |
| for (KnuthNode node = getNode(i); node != null; node = node.next) { |
| //log.debug(" + lines = " |
| //+ node.line + " demerits = " + node.totalDemerits); |
| bestActiveNode = compareNodes(bestActiveNode, node); |
| } |
| } |
| |
| // scan the node set once again and remove some nodes |
| //log.debug("LBA.filterActiveList> layout selection"); |
| for (int i = startLine; i < endLine; i++) { |
| for (KnuthNode node = getNode(i); node != null; node = node.next) { |
| //if (Math.abs(node.line - bestActiveNode.line) > maxDiff) { |
| //if (false) { |
| if (node.line != bestActiveNode.line |
| && node.totalDemerits > MAX_DEMERITS) { |
| //log.debug(" XXX lines = " |
| //+ node.line + " demerits = " + node.totalDemerits); |
| removeNode(i, node); |
| } else { |
| //log.debug(" ok lines = " |
| //+ node.line + " demerits = " + node.totalDemerits); |
| } |
| } |
| } |
| } else { |
| // leave only the active node with fewest total demerits |
| for (int i = startLine; i < endLine; i++) { |
| for (KnuthNode node = getNode(i); node != null; node = node.next) { |
| bestActiveNode = compareNodes(bestActiveNode, node); |
| if (node != bestActiveNode) { |
| removeNode(i, node); |
| } |
| } |
| } |
| } |
| return bestActiveNode.line; |
| } |
| } |
| |
| |
| private int constantLineHeight = 12000; |
| |
| /** |
| * Create a new Line Layout Manager. |
| * This is used by the block layout manager to create |
| * line managers for handling inline areas flowing into line areas. |
| * @param block the block formatting object |
| * @param lh the default line height |
| * @param l the default lead, from top to baseline |
| * @param f the default follow, from baseline to bottom |
| */ |
| public LineLayoutManager(Block block, Length lh, int l, int f) { |
| super(block); |
| fobj = block; |
| // the child FObj are owned by the parent BlockLM |
| // this LM has all its childLMs preloaded |
| fobjIter = null; |
| lineHeight = lh; |
| lead = l; |
| follow = f; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void initialize() { |
| bidiLevel = fobj.getBidiLevel(); |
| textAlignment = fobj.getTextAlign(); |
| textAlignmentLast = fobj.getTextAlignLast(); |
| textIndent = fobj.getTextIndent(); |
| lastLineEndIndent = fobj.getLastLineEndIndent(); |
| hyphenationProperties = fobj.getCommonHyphenation(); |
| hyphenationLadderCount = fobj.getHyphenationLadderCount(); |
| wrapOption = fobj.getWrapOption(); |
| whiteSpaceTreament = fobj.getWhitespaceTreatment(); |
| // |
| effectiveAlignment = getEffectiveAlignment(textAlignment, textAlignmentLast); |
| isFirstInBlock = (this == getParent().getChildLMs().get(0)); |
| } |
| |
| private int getEffectiveAlignment(int alignment, int alignmentLast) { |
| if (textAlignment != EN_JUSTIFY && textAlignmentLast == EN_JUSTIFY) { |
| return 0; |
| } else { |
| return textAlignment; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public List getNextKnuthElements(LayoutContext context, int alignment) { |
| if (alignmentContext == null) { |
| FontInfo fi = fobj.getFOEventHandler().getFontInfo(); |
| FontTriplet[] fontkeys = fobj.getCommonFont().getFontState(fi); |
| Font fs = fi.getFontInstance(fontkeys[0], fobj.getCommonFont().fontSize.getValue(this)); |
| alignmentContext = new AlignmentContext(fs, lineHeight.getValue(this), |
| context.getWritingMode()); |
| } |
| context.setAlignmentContext(alignmentContext); |
| ipd = context.getRefIPD(); |
| |
| //PHASE 1: Create Knuth elements |
| if (knuthParagraphs == null) { |
| // it's the first time this method is called |
| knuthParagraphs = new ArrayList<KnuthSequence>(); |
| |
| // here starts Knuth's algorithm |
| collectInlineKnuthElements(context); |
| } else { |
| // this method has been called before |
| // all line breaks are already calculated |
| } |
| |
| // return finished when there's no content |
| if (knuthParagraphs.size() == 0) { |
| setFinished(true); |
| return null; |
| } |
| |
| //PHASE 2: Create line breaks |
| return createLineBreaks(context.getBPAlignment(), context); |
| } |
| |
| /** |
| * Get a sequence of KnuthElements representing the content |
| * of the node assigned to the LM. |
| * @param context the LayoutContext used to store layout information |
| * @param alignment the desired text alignment |
| * @param restartPosition position at restart |
| * @return the list of KnuthElements |
| * @see LayoutManager#getNextKnuthElements(LayoutContext,int) |
| */ |
| public List getNextKnuthElements(LayoutContext context, int alignment, |
| LeafPosition restartPosition) { |
| log.trace("Restarting line breaking from index " + restartPosition.getIndex()); |
| int parIndex = restartPosition.getLeafPos(); |
| |
| for (int i = 0; i < parIndex; i++) { |
| knuthParagraphs.remove(0); |
| } |
| parIndex = 0; |
| |
| KnuthSequence paragraph = knuthParagraphs.get(parIndex); |
| if (paragraph instanceof Paragraph) { |
| ((Paragraph) paragraph).ignoreAtStart = 0; |
| isFirstInBlock = false; |
| } |
| paragraph.subList(0, restartPosition.getIndex() + 1).clear(); |
| Iterator<KnuthElement> iter = paragraph.iterator(); |
| while (iter.hasNext() && !iter.next().isBox()) { |
| iter.remove(); |
| } |
| if (!iter.hasNext()) { |
| knuthParagraphs.remove(parIndex); |
| } |
| |
| // return finished when there's no content |
| if (knuthParagraphs.size() == 0) { |
| setFinished(true); |
| return null; |
| } |
| |
| ipd = context.getRefIPD(); |
| //PHASE 2: Create line breaks |
| return createLineBreaks(context.getBPAlignment(), context); |
| } |
| |
| /** |
| * Phase 1 of Knuth algorithm: Collect all inline Knuth elements before determining line breaks. |
| * @param context the LayoutContext |
| */ |
| private void collectInlineKnuthElements(LayoutContext context) { |
| LayoutContext inlineLC = LayoutContext.copyOf(context); |
| |
| // convert all the text in a sequence of paragraphs made |
| // of KnuthBox, KnuthGlue and KnuthPenalty objects |
| boolean previousIsBox = false; |
| |
| StringBuffer trace = new StringBuffer("LineLM:"); |
| |
| Paragraph lastPar = null; |
| |
| InlineLevelLayoutManager curLM; |
| while ((curLM = (InlineLevelLayoutManager) getChildLM()) != null) { |
| List inlineElements = curLM.getNextKnuthElements(inlineLC, effectiveAlignment); |
| if (inlineElements == null || inlineElements.size() == 0) { |
| /* curLM.getNextKnuthElements() returned null or an empty list; |
| * this can happen if there is nothing more to layout, |
| * so just iterate once more to see if there are other children */ |
| continue; |
| } |
| |
| if (lastPar != null) { |
| KnuthSequence firstSeq = (KnuthSequence) inlineElements.get(0); |
| |
| // finish last paragraph before a new block sequence |
| if (!firstSeq.isInlineSequence()) { |
| lastPar.endParagraph(); |
| ElementListObserver.observe(lastPar, "line", null); |
| lastPar = null; |
| if (log.isTraceEnabled()) { |
| trace.append(" ]"); |
| } |
| previousIsBox = false; |
| } |
| |
| // does the first element of the first paragraph add to an existing word? |
| if (lastPar != null) { |
| KnuthElement thisElement; |
| thisElement = (KnuthElement) firstSeq.get(0); |
| if (thisElement.isBox() && !thisElement.isAuxiliary() |
| && previousIsBox) { |
| lastPar.addALetterSpace(); |
| } |
| } |
| } |
| |
| // loop over the KnuthSequences (and single KnuthElements) in returnedList |
| for (Object inlineElement : inlineElements) { |
| KnuthSequence sequence = (KnuthSequence) inlineElement; |
| // the sequence contains inline Knuth elements |
| if (sequence.isInlineSequence()) { |
| // look at the last element |
| ListElement lastElement = sequence.getLast(); |
| assert lastElement != null; |
| previousIsBox = lastElement.isBox() |
| && !((KnuthElement) lastElement).isAuxiliary() |
| && ((KnuthElement) lastElement).getWidth() != 0; |
| |
| // if last paragraph is open, add the new elements to the paragraph |
| // else this is the last paragraph |
| if (lastPar == null) { |
| lastPar = new Paragraph(this, |
| textAlignment, textAlignmentLast, |
| textIndent.getValue(this), |
| lastLineEndIndent.getValue(this)); |
| lastPar.startSequence(); |
| if (log.isTraceEnabled()) { |
| trace.append(" ["); |
| } |
| } else { |
| if (log.isTraceEnabled()) { |
| trace.append(" +"); |
| } |
| } |
| lastPar.addAll(sequence); |
| if (log.isTraceEnabled()) { |
| trace.append(" I"); |
| } |
| |
| // finish last paragraph if it was closed with a linefeed |
| if (lastElement.isPenalty() |
| && ((KnuthPenalty) lastElement).getPenalty() |
| == -KnuthPenalty.INFINITE) { |
| // a penalty item whose value is -inf |
| // represents a preserved linefeed, |
| // which forces a line break |
| lastPar.removeLast(); |
| if (!lastPar.containsBox()) { |
| //only a forced linefeed on this line |
| //-> compensate with an auxiliary glue |
| lastPar.add(new KnuthGlue(ipd, 0, ipd, null, true)); |
| } |
| lastPar.endParagraph(); |
| ElementListObserver.observe(lastPar, "line", null); |
| lastPar = null; |
| if (log.isTraceEnabled()) { |
| trace.append(" ]"); |
| } |
| previousIsBox = false; |
| } |
| } else { // the sequence is a block sequence |
| // the positions will be wrapped with this LM in postProcessLineBreaks |
| knuthParagraphs.add(sequence); |
| if (log.isTraceEnabled()) { |
| trace.append(" B"); |
| } |
| } |
| } // end of loop over returnedList |
| } |
| |
| if (lastPar != null) { |
| lastPar.endParagraph(); |
| ElementListObserver.observe(lastPar, "line", fobj.getId()); |
| if (log.isTraceEnabled()) { |
| trace.append(" ]"); |
| } |
| } |
| log.trace(trace); |
| } |
| |
| /** |
| * Phase 2 of Knuth algorithm: find optimal break points. |
| * @param alignment alignment in BP direction of the paragraph |
| * @param context the layout context |
| * @return a list of Knuth elements representing broken lines |
| */ |
| private List<ListElement> createLineBreaks(int alignment, LayoutContext context) { |
| // find the optimal line breaking points for each paragraph |
| Iterator<KnuthSequence> paragraphsIterator = knuthParagraphs.iterator(); |
| lineLayoutsList = new LineLayoutPossibilities[knuthParagraphs.size()]; |
| LineLayoutPossibilities llPoss; |
| for (int i = 0; paragraphsIterator.hasNext(); i++) { |
| KnuthSequence seq = paragraphsIterator.next(); |
| if (!seq.isInlineSequence()) { |
| // This set of line layout possibilities does not matter; |
| // we only need an entry in lineLayoutsList. |
| llPoss = new LineLayoutPossibilities(); |
| } else { |
| llPoss = findOptimalBreakingPoints(alignment, (Paragraph) seq, |
| !paragraphsIterator.hasNext()); |
| } |
| lineLayoutsList[i] = llPoss; |
| } |
| |
| setFinished(true); |
| |
| //Post-process the line breaks found |
| return postProcessLineBreaks(alignment, context); |
| } |
| |
| /** |
| * Find the optimal linebreaks for a paragraph |
| * @param alignment alignment of the paragraph |
| * @param currPar the Paragraph for which the linebreaks are found |
| * @param isLastPar flag indicating whether currPar is the last paragraph |
| * @return the line layout possibilities for the paragraph |
| */ |
| private LineLayoutPossibilities findOptimalBreakingPoints(int alignment, Paragraph currPar, |
| boolean isLastPar) { |
| // use the member lineLayouts, which is read by LineBreakingAlgorithm.updateData1 and 2 |
| lineLayouts = new LineLayoutPossibilities(); |
| double maxAdjustment = 1; |
| LineBreakingAlgorithm alg = new LineBreakingAlgorithm(alignment, |
| textAlignment, textAlignmentLast, |
| textIndent.getValue(this), currPar.lineFiller.getOpt(), |
| lineHeight.getValue(this), lead, follow, |
| (knuthParagraphs.indexOf(currPar) == 0), |
| hyphenationLadderCount.getEnum() == EN_NO_LIMIT |
| ? 0 : hyphenationLadderCount.getValue(), |
| this); |
| alg.setConstantLineWidth(ipd); |
| boolean canWrap = (wrapOption != EN_NO_WRAP); |
| boolean canHyphenate = (canWrap && hyphenationProperties.hyphenate.getEnum() == EN_TRUE); |
| |
| // find hyphenation points, if allowed and not yet done |
| if (canHyphenate && !hyphenationPerformed) { |
| // make sure findHyphenationPoints() is bypassed if |
| // the method is called twice (e.g. due to changing page-ipd) |
| hyphenationPerformed = isLastPar; |
| findHyphenationPoints(currPar); |
| } |
| |
| // first try: do not consider hyphenation points as legal breaks |
| int allowedBreaks = (canWrap ? BreakingAlgorithm.NO_FLAGGED_PENALTIES |
| : BreakingAlgorithm.ONLY_FORCED_BREAKS); |
| int breakingPoints = alg.findBreakingPoints(currPar, maxAdjustment, false, allowedBreaks); |
| |
| if (breakingPoints == 0 || alignment == EN_JUSTIFY) { |
| // if the first try found a set of breaking points, save them |
| if (breakingPoints > 0) { |
| alg.resetAlgorithm(); |
| lineLayouts.savePossibilities(false); |
| } else { |
| // the first try failed |
| log.debug("No set of breaking points found with maxAdjustment = " + maxAdjustment); |
| } |
| |
| // now try something different |
| log.debug("Hyphenation possible? " + canHyphenate); |
| // Note: if allowedBreaks is guaranteed to be unchanged by alg.findBreakingPoints(), |
| // the below check can be simplified to 'if (canHyphenate) ...' |
| if (canHyphenate && allowedBreaks != BreakingAlgorithm.ONLY_FORCED_BREAKS) { |
| // consider every hyphenation point as a legal break |
| allowedBreaks = BreakingAlgorithm.ALL_BREAKS; |
| } else { |
| // try with a higher threshold |
| maxAdjustment = 5; |
| } |
| |
| breakingPoints = alg.findBreakingPoints(currPar, maxAdjustment, false, allowedBreaks); |
| if (breakingPoints == 0) { |
| // the second try failed too, try with a huge threshold |
| // and force the algorithm to find a set of breaking points |
| if (log.isDebugEnabled()) { |
| log.debug("No set of breaking points found with maxAdjustment = " |
| + maxAdjustment + (canHyphenate ? " and hyphenation" : "")); |
| } |
| maxAdjustment = 20; |
| alg.findBreakingPoints(currPar, maxAdjustment, true, allowedBreaks); |
| } |
| |
| // use non-hyphenated breaks, when possible |
| lineLayouts.restorePossibilities(); |
| } |
| |
| return lineLayouts; |
| } |
| |
| /** |
| * Creates the element list in BP direction for the broken lines. |
| * @param alignment the currently applicable vertical alignment |
| * @param context the layout context |
| * @return the newly built element list |
| */ |
| private List<ListElement> postProcessLineBreaks(int alignment, LayoutContext context) { |
| |
| List<ListElement> returnList = new LinkedList<ListElement>(); |
| |
| int endIndex = -1; |
| for (int p = 0; p < knuthParagraphs.size(); p++) { |
| // penalty between paragraphs |
| if (p > 0) { |
| Keep keep = getKeepTogether(); |
| returnList.add(new BreakElement( |
| new Position(this), |
| keep.getPenalty(), |
| keep.getContext(), |
| context)); |
| } |
| |
| LineLayoutPossibilities llPoss = lineLayoutsList[p]; |
| KnuthSequence seq = knuthParagraphs.get(p); |
| |
| if (!seq.isInlineSequence()) { |
| List<ListElement> targetList = new LinkedList<ListElement>(); |
| for (Object aSeq : seq) { |
| ListElement tempElement; |
| tempElement = (ListElement) aSeq; |
| LayoutManager lm = tempElement.getLayoutManager(); |
| if (baselineOffset < 0 && lm != null && lm.hasLineAreaDescendant()) { |
| baselineOffset = lm.getBaselineOffset(); |
| } |
| if (lm != this) { |
| tempElement.setPosition(notifyPos(new NonLeafPosition(this, |
| tempElement.getPosition()))); |
| } |
| targetList.add(tempElement); |
| } |
| returnList.addAll(targetList); |
| } else if (seq.isInlineSequence() && alignment == EN_JUSTIFY) { |
| /* justified vertical alignment (not in the XSL FO recommendation): |
| create a multi-layout sequence whose elements will contain |
| a conventional Position */ |
| Position returnPosition = new LeafPosition(this, p); |
| createElements(returnList, llPoss, returnPosition); |
| } else { |
| /* "normal" vertical alignment: create a sequence whose boxes |
| represent effective lines, and contain LineBreakPositions */ |
| int startIndex = 0; |
| int previousEndIndex = 0; |
| for (int i = 0; |
| i < llPoss.getChosenLineCount(); |
| i++) { |
| int orphans = fobj.getOrphans(); |
| int widows = fobj.getWidows(); |
| if (handlingFloat()) { |
| orphans = 1; |
| widows = 1; |
| } |
| if (returnList.size() > 0 |
| && i > 0 //if i==0 break generated above already |
| && i >= orphans && i <= llPoss.getChosenLineCount() - widows) { |
| // penalty allowing a page break between lines |
| Keep keep = getKeepTogether(); |
| returnList.add(new BreakElement( |
| new LeafPosition(this, p, endIndex), |
| keep.getPenalty(), |
| keep.getContext(), |
| context)); |
| } |
| endIndex = llPoss.getChosenPosition(i).getLeafPos(); |
| // create a list of the FootnoteBodyLM handling footnotes |
| // whose citations are in this line |
| List<FootnoteBodyLayoutManager> footnoteList = FootenoteUtil.getFootnotes( |
| seq, startIndex, endIndex); |
| List<FloatContentLayoutManager> floats = FloatContentLayoutManager.checkForFloats(seq, |
| startIndex, endIndex); |
| startIndex = endIndex + 1; |
| LineBreakPosition lbp = llPoss.getChosenPosition(i); |
| if (baselineOffset < 0) { |
| baselineOffset = lbp.spaceBefore + lbp.baseline; |
| } |
| if (floats.isEmpty()) { |
| returnList.add(new KnuthBlockBox(lbp.lineHeight + lbp.spaceBefore + lbp.spaceAfter, |
| footnoteList, lbp, false)); |
| } else { |
| // add a line with height zero and no content and attach float to it |
| returnList.add(new KnuthBlockBox(0, Collections.emptyList(), null, false, floats)); |
| // add a break element to signal that we should restart LB at this break |
| Keep keep = getKeepTogether(); |
| returnList.add(new BreakElement(new LeafPosition(this, p, previousEndIndex), keep |
| .getPenalty(), keep.getContext(), context)); |
| // add the original line where the float was but without the float now |
| returnList.add(new KnuthBlockBox(lbp.lineHeight + lbp.spaceBefore + lbp.spaceAfter, |
| footnoteList, lbp, false)); |
| } |
| previousEndIndex = endIndex; |
| } |
| } |
| } |
| |
| return returnList; |
| } |
| |
| private void createElements(List<ListElement> list, LineLayoutPossibilities llPoss, |
| Position elementPosition) { |
| /* number of normal, inner lines */ |
| int innerLines = 0; |
| /* number of lines that can be used in order to fill more space */ |
| int optionalLines = 0; |
| /* number of lines that can be used in order to fill more space |
| only if the paragraph is not parted */ |
| int conditionalOptionalLines = 0; |
| /* number of lines that can be omitted in order to fill less space */ |
| int eliminableLines = 0; |
| /* number of lines that can be omitted in order to fill less space |
| only if the paragraph is not parted */ |
| int conditionalEliminableLines = 0; |
| /* number of the first unbreakable lines */ |
| int firstLines = fobj.getOrphans(); |
| /* number of the last unbreakable lines */ |
| int lastLines = fobj.getWidows(); |
| /* sub-sequence used to separate the elements representing different lines */ |
| List<KnuthElement> breaker = new LinkedList<KnuthElement>(); |
| |
| /* comment out the next lines in order to test particular situations */ |
| if (fobj.getOrphans() + fobj.getWidows() <= llPoss.getMinLineCount()) { |
| innerLines = llPoss.getMinLineCount() - (fobj.getOrphans() + fobj.getWidows()); |
| optionalLines = llPoss.getMaxLineCount() - llPoss.getOptLineCount(); |
| eliminableLines = llPoss.getOptLineCount() - llPoss.getMinLineCount(); |
| } else if (fobj.getOrphans() + fobj.getWidows() <= llPoss.getOptLineCount()) { |
| optionalLines = llPoss.getMaxLineCount() - llPoss.getOptLineCount(); |
| eliminableLines = llPoss.getOptLineCount() - (fobj.getOrphans() + fobj.getWidows()); |
| conditionalEliminableLines |
| = (fobj.getOrphans() + fobj.getWidows()) - llPoss.getMinLineCount(); |
| } else if (fobj.getOrphans() + fobj.getWidows() <= llPoss.getMaxLineCount()) { |
| optionalLines = llPoss.getMaxLineCount() - (fobj.getOrphans() + fobj.getWidows()); |
| conditionalOptionalLines |
| = (fobj.getOrphans() + fobj.getWidows()) - llPoss.getOptLineCount(); |
| conditionalEliminableLines = llPoss.getOptLineCount() - llPoss.getMinLineCount(); |
| firstLines -= conditionalOptionalLines; |
| } else { |
| conditionalOptionalLines = llPoss.getMaxLineCount() - llPoss.getOptLineCount(); |
| conditionalEliminableLines = llPoss.getOptLineCount() - llPoss.getMinLineCount(); |
| firstLines = llPoss.getOptLineCount(); |
| lastLines = 0; |
| } |
| /* comment out the previous lines in order to test particular situations */ |
| |
| /* use these lines to test particular situations |
| innerLines = 0; |
| optionalLines = 1; |
| conditionalOptionalLines = 2; |
| eliminableLines = 0; |
| conditionalEliminableLines = 0; |
| firstLines = 1; |
| lastLines = 3; |
| */ |
| |
| if (lastLines != 0 |
| && (conditionalOptionalLines > 0 || conditionalEliminableLines > 0)) { |
| breaker.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, elementPosition, false)); |
| breaker.add(new KnuthGlue(0, -conditionalOptionalLines * constantLineHeight, |
| -conditionalEliminableLines * constantLineHeight, |
| Adjustment.LINE_NUMBER_ADJUSTMENT, elementPosition, false)); |
| breaker.add(new KnuthPenalty(conditionalOptionalLines * constantLineHeight, |
| 0, false, elementPosition, false)); |
| breaker.add(new KnuthGlue(0, conditionalOptionalLines * constantLineHeight, |
| conditionalEliminableLines * constantLineHeight, |
| Adjustment.LINE_NUMBER_ADJUSTMENT, elementPosition, false)); |
| } else if (lastLines != 0) { |
| breaker.add(new KnuthPenalty(0, 0, false, elementPosition, false)); |
| } |
| |
| // creation of the elements: |
| // first group of lines |
| list.add(new KnuthBox(firstLines * constantLineHeight, elementPosition, |
| (lastLines == 0 |
| && conditionalOptionalLines == 0 |
| && conditionalEliminableLines == 0))); |
| if (conditionalOptionalLines > 0 |
| || conditionalEliminableLines > 0) { |
| list.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, elementPosition, false)); |
| list.add(new KnuthGlue(0, conditionalOptionalLines * constantLineHeight, |
| conditionalEliminableLines * constantLineHeight, |
| Adjustment.LINE_NUMBER_ADJUSTMENT, elementPosition, false)); |
| list.add(new KnuthBox(0, elementPosition, (lastLines == 0))); |
| } |
| |
| // optional lines |
| for (int i = 0; i < optionalLines; i++) { |
| list.addAll(breaker); |
| list.add(new KnuthBox(0, elementPosition, false)); |
| list.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, elementPosition, false)); |
| list.add(new KnuthGlue(0, constantLineHeight, 0, |
| Adjustment.LINE_NUMBER_ADJUSTMENT, elementPosition, false)); |
| list.add(new KnuthBox(0, elementPosition, false)); |
| } |
| |
| // eliminable lines |
| for (int i = 0; i < eliminableLines; i++) { |
| list.addAll(breaker); |
| list.add(new KnuthBox(constantLineHeight, elementPosition, false)); |
| list.add(new KnuthPenalty(0, KnuthElement.INFINITE, false, elementPosition, false)); |
| list.add(new KnuthGlue(0, 0, constantLineHeight, |
| Adjustment.LINE_NUMBER_ADJUSTMENT, elementPosition, false)); |
| list.add(new KnuthBox(0, elementPosition, false)); |
| } |
| |
| // inner lines |
| for (int i = 0; i < innerLines; i++) { |
| list.addAll(breaker); |
| list.add(new KnuthBox(constantLineHeight, elementPosition, false)); |
| } |
| |
| // last group of lines |
| if (lastLines > 0) { |
| list.addAll(breaker); |
| list.add(new KnuthBox(lastLines * constantLineHeight, |
| elementPosition, true)); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean mustKeepTogether() { |
| return ((BlockLevelLayoutManager) getParent()).mustKeepTogether(); |
| } |
| |
| /** {@inheritDoc} */ |
| public KeepProperty getKeepTogetherProperty() { |
| return ((BlockLevelLayoutManager) getParent()).getKeepTogetherProperty(); |
| } |
| |
| /** {@inheritDoc} */ |
| public KeepProperty getKeepWithPreviousProperty() { |
| return ((BlockLevelLayoutManager) getParent()).getKeepWithPreviousProperty(); |
| } |
| |
| /** {@inheritDoc} */ |
| public KeepProperty getKeepWithNextProperty() { |
| return ((BlockLevelLayoutManager) getParent()).getKeepWithNextProperty(); |
| } |
| |
| /** {@inheritDoc} */ |
| public Keep getKeepTogether() { |
| return ((BlockLevelLayoutManager) getParent()).getKeepTogether(); |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean mustKeepWithPrevious() { |
| return !getKeepWithPrevious().isAuto(); |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean mustKeepWithNext() { |
| return !getKeepWithNext().isAuto(); |
| } |
| |
| /** {@inheritDoc} */ |
| public Keep getKeepWithNext() { |
| return Keep.KEEP_AUTO; |
| } |
| |
| /** {@inheritDoc} */ |
| public Keep getKeepWithPrevious() { |
| return Keep.KEEP_AUTO; |
| } |
| |
| /** {@inheritDoc} */ |
| public int negotiateBPDAdjustment(int adj, KnuthElement lastElement) { |
| Position lastPos = lastElement.getPosition(); |
| assert (lastPos instanceof LeafPosition); |
| LeafPosition pos = (LeafPosition) lastPos; |
| //if (lastElement.isPenalty()) { |
| // totalAdj += lastElement.getWidth(); |
| //} |
| //int lineNumberDifference = (int)((double) totalAdj / constantLineHeight); |
| int lineNumberDifference = (int) Math.round((double) adj / constantLineHeight |
| + (adj > 0 ? -0.4 : 0.4)); |
| //log.debug(" LLM> variazione calcolata = " + ((double) totalAdj / constantLineHeight) |
| //+ " variazione applicata = " + lineNumberDifference); |
| LineLayoutPossibilities llPoss; |
| llPoss = lineLayoutsList[pos.getLeafPos()]; |
| lineNumberDifference = llPoss.applyLineCountAdjustment(lineNumberDifference); |
| return lineNumberDifference * constantLineHeight; |
| } |
| |
| /** {@inheritDoc} */ |
| public void discardSpace(KnuthGlue spaceGlue) { |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public List getChangedKnuthElements(List oldList, int alignment, int depth) { |
| return getChangedKnuthElements(oldList, alignment); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public List getChangedKnuthElements(List oldList, int alignment) { |
| List<KnuthElement> returnList = new LinkedList<KnuthElement>(); |
| for (int p = 0; p < knuthParagraphs.size(); p++) { |
| LineLayoutPossibilities llPoss = lineLayoutsList[p]; |
| //log.debug("demerits of the chosen layout: " + llPoss.getChosenDemerits()); |
| int orphans = fobj.getOrphans(); |
| int widows = fobj.getWidows(); |
| if (handlingFloat()) { |
| orphans = 1; |
| widows = 1; |
| } |
| for (int i = 0; i < llPoss.getChosenLineCount(); i++) { |
| if (!((BlockLevelLayoutManager) parentLayoutManager).mustKeepTogether() && i >= orphans |
| && i <= llPoss.getChosenLineCount() - widows) { |
| // null penalty allowing a page break between lines |
| returnList.add(new KnuthPenalty(0, 0, false, new Position(this), false)); |
| } |
| LineBreakPosition lbp = llPoss.getChosenPosition(i); |
| //log.debug("LLM.getChangedKnuthElements> lineWidth= " |
| // + lbp.lineWidth + " difference= " + lbp.difference); |
| //log.debug(" shrink= " |
| // + lbp.availableShrink + " stretch= " + lbp.availableStretch); |
| //log.debug("linewidth= " + lbp.lineWidth + " difference= " |
| //+ lbp.difference + " indent= " + lbp.startIndent); |
| MinOptMax contentIPD; |
| if (alignment == EN_JUSTIFY) { |
| contentIPD = MinOptMax.getInstance( |
| lbp.lineWidth - lbp.difference - lbp.availableShrink, |
| lbp.lineWidth - lbp.difference, |
| lbp.lineWidth - lbp.difference + lbp.availableStretch); |
| } else if (alignment == EN_CENTER) { |
| contentIPD = MinOptMax.getInstance(lbp.lineWidth - 2 * lbp.startIndent); |
| } else if (alignment == EN_END) { |
| contentIPD = MinOptMax.getInstance(lbp.lineWidth - lbp.startIndent); |
| } else { |
| contentIPD |
| = MinOptMax.getInstance(lbp.lineWidth - lbp.difference + lbp.startIndent); |
| } |
| returnList.add(new KnuthBlockBox(lbp.lineHeight, contentIPD, (lbp.ipdAdjust != 0 |
| ? lbp.lineWidth - lbp.difference : 0), |
| lbp, false)); |
| } |
| } |
| return returnList; |
| } |
| |
| /** |
| * Find hyphenation points for every word in 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 |
| List updateList = new LinkedList(); |
| KnuthElement firstElement; |
| KnuthElement nextElement; |
| // current InlineLevelLayoutManager |
| InlineLevelLayoutManager currLM = null; |
| // number of KnuthBox elements containing word fragments |
| int boxCount; |
| // number of auxiliary KnuthElements between KnuthBoxes |
| int auxCount; |
| StringBuffer sbChars; |
| |
| // find all hyphenation points |
| while (currParIterator.hasNext()) { |
| firstElement = (KnuthElement) currParIterator.next(); |
| // |
| if (firstElement.getLayoutManager() != currLM) { |
| currLM = (InlineLevelLayoutManager) firstElement.getLayoutManager(); |
| if (currLM != null) { |
| updateList.add(new Update(currLM, currParIterator.previousIndex())); |
| } else { |
| break; |
| } |
| } else if (currLM == null) { |
| 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(); |
| sbChars.append(currLM.getWordChars(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 = (InlineLevelLayoutManager) nextElement.getLayoutManager(); |
| updateList.add(new Update(currLM, currParIterator.previousIndex())); |
| } |
| // append text to recreate the whole word |
| boxCount++; |
| sbChars.append(currLM.getWordChars(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 { |
| if (currLM != nextElement.getLayoutManager()) { |
| currLM = (InlineLevelLayoutManager) nextElement.getLayoutManager(); |
| updateList.add(new Update(currLM, currParIterator.previousIndex())); |
| } |
| // an auxiliary KnuthElement: simply ignore it |
| auxCount++; |
| } |
| } |
| if (log.isTraceEnabled()) { |
| log.trace(" Word to hyphenate: " + sbChars); |
| } |
| // 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()) { |
| ((InlineLevelLayoutManager) |
| element.getLayoutManager()).hyphenate(element.getPosition(), hc); |
| } else { |
| // nothing to do, element is an auxiliary KnuthElement |
| } |
| } |
| } |
| } |
| } |
| processUpdates(currPar, updateList); |
| } |
| |
| private void processUpdates(Paragraph par, List updateList) { |
| // create iterator for the updateList |
| ListIterator updateListIterator = updateList.listIterator(); |
| Update currUpdate; |
| int elementsAdded = 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.firstIndex; |
| int toIndex; |
| if (updateListIterator.hasNext()) { |
| Update nextUpdate = (Update) updateListIterator.next(); |
| toIndex = nextUpdate.firstIndex; |
| updateListIterator.previous(); |
| } else { |
| // maybe this is not always correct! |
| toIndex = par.size() - par.ignoreAtEnd |
| - elementsAdded; |
| } |
| |
| // applyChanges() returns true if the LM modifies its data, |
| // so it must return new KnuthElements to replace the old ones |
| if (currUpdate.inlineLM |
| .applyChanges(par.subList(fromIndex + elementsAdded, |
| toIndex + elementsAdded))) { |
| // insert the new KnuthElements |
| List newElements = currUpdate.inlineLM.getChangedKnuthElements( |
| par.subList(fromIndex + elementsAdded, |
| toIndex + elementsAdded), |
| /*flaggedPenalty,*/ effectiveAlignment); |
| // remove the old elements |
| par.subList(fromIndex + elementsAdded, |
| toIndex + elementsAdded).clear(); |
| // insert the new elements |
| par.addAll(fromIndex + elementsAdded, newElements); |
| elementsAdded += newElements.size() - (toIndex - fromIndex); |
| } |
| } |
| updateList.clear(); |
| } |
| |
| /** |
| * Line area is always considered to act as a fence. |
| * @param isNotFirst ignored |
| * @return always true |
| */ |
| @Override |
| protected boolean hasLeadingFence(boolean isNotFirst) { |
| return true; |
| } |
| |
| /** |
| * Line area is always considered to act as a fence. |
| * @param isNotLast ignored |
| * @return always true |
| */ |
| @Override |
| protected boolean hasTrailingFence(boolean isNotLast) { |
| return true; |
| } |
| |
| private HyphContext getHyphenContext(StringBuffer sbChars) { |
| // Find all hyphenation points in this word |
| // (get in an array of offsets) |
| // hyphenationProperties 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(hyphenationProperties.language.getString(), |
| hyphenationProperties.country.getString(), |
| getFObj().getUserAgent().getHyphenationResourceResolver(), |
| getFObj().getUserAgent().getHyphenationPatternNames(), |
| sbChars.toString(), |
| hyphenationProperties.hyphenationRemainCharacterCount.getValue(), |
| hyphenationProperties.hyphenationPushCharacterCount.getValue(), |
| getFObj().getUserAgent()); |
| // 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. |
| // 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. |
| if (hyph != null) { |
| return new HyphContext(hyph.getHyphenationPoints()); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean hasLineAreaDescendant() { |
| return true; |
| } |
| |
| @Override |
| public int getBaselineOffset() { |
| return baselineOffset; |
| } |
| |
| /** |
| * Add the areas with the break points. |
| * |
| * @param parentIter the iterator of break positions |
| * @param context the context for adding areas |
| */ |
| @Override |
| public void addAreas(PositionIterator parentIter, |
| LayoutContext context) { |
| while (parentIter.hasNext()) { |
| Position pos = parentIter.next(); |
| boolean isLastPosition = !parentIter.hasNext(); |
| if (pos instanceof LineBreakPosition) { |
| addInlineArea(context, (LineBreakPosition) pos, isLastPosition); |
| } else if ((pos instanceof NonLeafPosition) && pos.generatesAreas()) { |
| addBlockArea(context, pos, isLastPosition); |
| } else { |
| /* |
| * pos was the Position inside a penalty item, nothing to do; |
| * or Pos does not generate an area, |
| * i.e. it stand for spaces, borders and padding. |
| */ |
| } |
| } |
| setCurrentArea(null); // ?? necessary |
| } |
| |
| /** |
| * Add a line with inline content |
| * @param context the context for adding areas |
| * @param lbp the position for which the line is generated |
| * @param isLastPosition true if this is the last position of this LM |
| */ |
| private void addInlineArea(LayoutContext context, LineBreakPosition lbp, |
| boolean isLastPosition) { |
| |
| KnuthSequence seq = knuthParagraphs.get(lbp.parIndex); |
| int startElementIndex = lbp.startIndex; |
| int endElementIndex = lbp.getLeafPos(); |
| |
| LineArea lineArea = new LineArea( |
| (lbp.getLeafPos() < seq.size() - 1 ? textAlignment : textAlignmentLast), |
| lbp.difference, lbp.availableStretch, lbp.availableShrink); |
| lineArea.setChangeBarList(getChangeBarList()); |
| |
| if (lbp.startIndent != 0) { |
| lineArea.addTrait(Trait.START_INDENT, lbp.startIndent); |
| } |
| if (lbp.endIndent != 0) { |
| lineArea.addTrait(Trait.END_INDENT, lbp.endIndent); |
| } |
| lineArea.setBPD(lbp.lineHeight); |
| lineArea.setIPD(lbp.lineWidth); |
| lineArea.setBidiLevel(bidiLevel); |
| lineArea.addTrait(Trait.SPACE_BEFORE, lbp.spaceBefore); |
| lineArea.addTrait(Trait.SPACE_AFTER, lbp.spaceAfter); |
| alignmentContext.resizeLine(lbp.lineHeight, lbp.baseline); |
| |
| if (seq instanceof Paragraph) { |
| Paragraph currPar = (Paragraph) seq; |
| // ignore the first elements added by the LineLayoutManager |
| startElementIndex += (startElementIndex == 0) ? currPar.ignoreAtStart : 0; |
| |
| // if this is the last line area that for this paragraph, |
| // ignore the last elements added by the LineLayoutManager and |
| // subtract the last-line-end-indent from the area ipd |
| if (endElementIndex == (currPar.size() - 1)) { |
| endElementIndex -= currPar.ignoreAtEnd; |
| lineArea.setIPD(lineArea.getIPD() - lastLineEndIndent.getValue(this)); |
| } |
| } |
| |
| // ignore the last element in the line if it is a KnuthGlue object |
| ListIterator seqIterator = seq.listIterator(endElementIndex); |
| KnuthElement lastElement = (KnuthElement) seqIterator.next(); |
| // the TLM which created the last KnuthElement in this line |
| LayoutManager lastLM = lastElement.getLayoutManager(); |
| if (lastElement.isGlue()) { |
| // Remove trailing spaces if allowed so |
| if (whiteSpaceTreament == EN_IGNORE_IF_SURROUNDING_LINEFEED |
| || whiteSpaceTreament == EN_IGNORE |
| || whiteSpaceTreament == EN_IGNORE_IF_BEFORE_LINEFEED) { |
| endElementIndex--; |
| // this returns the same KnuthElement |
| seqIterator.previous(); |
| if (seqIterator.hasPrevious()) { |
| lastLM = ((KnuthElement) seqIterator.previous()).getLayoutManager(); |
| } |
| } |
| } |
| |
| // Remove leading spaces if allowed so |
| if (whiteSpaceTreament == EN_IGNORE_IF_SURROUNDING_LINEFEED |
| || whiteSpaceTreament == EN_IGNORE |
| || whiteSpaceTreament == EN_IGNORE_IF_AFTER_LINEFEED) { |
| // ignore KnuthGlue and KnuthPenalty objects |
| // at the beginning of the line |
| seqIterator = seq.listIterator(startElementIndex); |
| while (seqIterator.hasNext() && !((KnuthElement) seqIterator.next()).isBox()) { |
| startElementIndex++; |
| } |
| } |
| // Add the inline areas to lineArea |
| PositionIterator inlinePosIter = new KnuthPossPosIter(seq, startElementIndex, |
| endElementIndex + 1); |
| |
| LayoutContext lc = LayoutContext.offspringOf(context); |
| lc.setAlignmentContext(alignmentContext); |
| lc.setSpaceAdjust(lbp.dAdjust); |
| lc.setIPDAdjust(lbp.ipdAdjust); |
| lc.setLeadingSpace(new SpaceSpecifier(true)); |
| lc.setTrailingSpace(new SpaceSpecifier(false)); |
| lc.setFlags(LayoutContext.RESOLVE_LEADING_SPACE, true); |
| |
| setCurrentArea(lineArea); |
| setChildContext(lc); |
| LayoutManager childLM; |
| 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)); |
| } |
| |
| // if display-align is distribute, add space after |
| if (context.getSpaceAfter() > 0 |
| && (!context.isLastArea() || !isLastPosition)) { |
| lineArea.setBPD(lineArea.getBPD() + context.getSpaceAfter()); |
| } |
| lineArea.finish(); |
| if (lineArea.getBidiLevel() >= 0) { |
| BidiResolver.reorder(lineArea); |
| } |
| parentLayoutManager.addChildArea(lineArea); |
| } |
| |
| /** |
| * Add a line with block content |
| * @param context the context for adding areas |
| * @param pos the position for which the line is generated |
| * @param isLastPosition true if this is the last position of this LM |
| */ |
| private void addBlockArea(LayoutContext context, Position pos, boolean isLastPosition) { |
| /* Nested block-level content; |
| * go down the LM stack again; |
| * "unwrap" the positions and put the child positions in a new list. |
| * The positionList must contain one area-generating position, |
| * which creates one line area. |
| */ |
| List positionList = new ArrayList(1); |
| Position innerPosition = pos.getPosition(); |
| positionList.add(innerPosition); |
| |
| // do we have the last LM? |
| LayoutManager lastLM = null; |
| if (isLastPosition) { |
| lastLM = innerPosition.getLM(); |
| } |
| |
| LineArea lineArea = new LineArea(); |
| lineArea.setChangeBarList(getChangeBarList()); |
| setCurrentArea(lineArea); |
| LayoutContext lc = LayoutContext.newInstance(); |
| lc.setAlignmentContext(alignmentContext); |
| setChildContext(lc); |
| |
| PositionIterator childPosIter = new PositionIterator(positionList.listIterator()); |
| LayoutContext blocklc = LayoutContext.offspringOf(context); |
| blocklc.setLeadingSpace(new SpaceSpecifier(true)); |
| blocklc.setTrailingSpace(new SpaceSpecifier(false)); |
| blocklc.setFlags(LayoutContext.RESOLVE_LEADING_SPACE, true); |
| LayoutManager childLM; |
| while ((childLM = childPosIter.getNextChildLM()) != null) { |
| // set last area flag |
| blocklc.setFlags(LayoutContext.LAST_AREA, |
| (context.isLastArea() && childLM == lastLM)); |
| blocklc.setStackLimitBP(context.getStackLimitBP()); |
| // Add the line areas to Area |
| childLM.addAreas(childPosIter, blocklc); |
| blocklc.setLeadingSpace(blocklc.getTrailingSpace()); |
| blocklc.setTrailingSpace(new SpaceSpecifier(false)); |
| } |
| lineArea.updateExtentsFromChildren(); |
| if (lineArea.getBidiLevel() >= 0) { |
| BidiResolver.reorder(lineArea); |
| } |
| parentLayoutManager.addChildArea(lineArea); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void addChildArea(Area childArea) { |
| // Make sure childArea is inline area |
| if (childArea instanceof InlineArea) { |
| Area parent = getCurrentArea(); |
| if (getContext().resolveLeadingSpace()) { |
| addSpace(parent, getContext().getLeadingSpace().resolve(false), |
| getContext().getSpaceAdjust()); |
| } |
| parent.addChildArea(childArea); |
| } |
| } |
| |
| // --------- Property Resolution related functions --------- // |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean getGeneratesBlockArea() { |
| return true; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean getGeneratesLineArea() { |
| return true; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean isRestartable() { |
| return true; |
| } |
| |
| /** |
| * Whether this LM can handle horizontal overflow error messages (only a BlockContainerLayoutManager can). |
| * @param milliPoints horizontal overflow |
| * @return true if handled by a BlockContainerLayoutManager |
| */ |
| public boolean handleOverflow(int milliPoints) { |
| if (getParent() instanceof BlockLayoutManager) { |
| return ((BlockLayoutManager) getParent()).handleOverflow(milliPoints); |
| } |
| return false; |
| } |
| } |