--- test/layoutengine/standard-testcases/block_list-block_orphans_widows.xml (revision ) +++ test/layoutengine/standard-testcases/block_list-block_orphans_widows.xml (revision ) @@ -0,0 +1,128 @@ + + + + + +

+ This test checks the behavior of the "orphans" and "widows" properties + in case list-blocks are nested in blocks. +

+
+ + + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + + + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + + + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + + + + + + + + + + + + +
--- test/layoutengine/standard-testcases/block_table_orphans_widows.xml (revision ) +++ test/layoutengine/standard-testcases/block_table_orphans_widows.xml (revision ) @@ -0,0 +1,119 @@ + + + + + +

+ This test checks the behavior of the "orphans" and "widows" properties + in case tables are nested in blocks. +

+
+ + + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + + + + + + + + + + + + + + + + +
--- test/layoutengine/standard-testcases/block_widows.xml (revision ) +++ test/layoutengine/standard-testcases/block_widows.xml (revision ) @@ -0,0 +1,143 @@ + + + + + +

+ This test checks the default behavior of the "widows" property. +

+
+ + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + fermentum tempor nunc. Class aptent + taciti sociosqu ad litora torquent per + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + fermentum tempor nunc. Class aptent + taciti sociosqu ad litora torquent per + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + + + + + Lorem ipsum + dolor sit amet, + consectetur + adipiscing elit. + Cras placerat, + lectus vel + iaculis euismod, + ipsum enim + dapibus urna, + eu pellentesque + velit dolor + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + fermentum tempor nunc. Class aptent + + Lorem ipsum + + + + + + + + + + + + + + + + + +
--- test/layoutengine/standard-testcases/block_orphans.xml (revision ) +++ test/layoutengine/standard-testcases/block_orphans.xml (revision ) @@ -0,0 +1,167 @@ + + + + + +

+ This test checks the default behavior of the "orphans" property. +

+
+ + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + + + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + + + + + + Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Cras placerat, lectus vel + iaculis euismod, ipsum enim dapibus + urna, eu pellentesque velit dolor ac + purus. Maecenas vitae pulvinar turpis. + Duis venenatis tincidunt velit, fringilla + dignissim sapien faucibus vel. Quisque + placerat ornare consectetur. Aenean + dui tortor, tempor ut convallis in, + + Lorem ipsum + dolor sit amet, + consectetur + velit dolor + + + + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + Lorem ipsum + + Lorem ipsum + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
--- src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java (revision 1073116) +++ src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java (revision ) @@ -66,6 +66,9 @@ private MinOptMax effSpaceBefore; private MinOptMax effSpaceAfter; + private int orphans = 2; + private int widows = 2; + /** * Creates a new BlockLayoutManager. * @param inBlock the block FO object to create the layout manager for. @@ -97,6 +100,8 @@ .getOptimum(this).getLength().getValue(this); adjustedSpaceAfter = fo.getCommonMarginBlock().spaceAfter.getSpace() .getOptimum(this).getLength().getValue(this); + orphans = fo.getOrphans(); + widows = fo.getWidows(); } /** {@inheritDoc} */ @@ -110,10 +115,42 @@ public List getNextKnuthElements(LayoutContext context, int alignment, Stack lmStack, Position restartPosition, LayoutManager restartAtLM) { resetSpaces(); - return super.getNextKnuthElements( + + List contentList = super.getNextKnuthElements( context, alignment, lmStack, restartPosition, restartAtLM); + + if (!this.hasNextChildLM()) { + // handle widows + int lineCount = 0, boxCount = 0, index = contentList.size(); + ListElement current; + for (ListIterator it = contentList.listIterator(contentList.size()); + it.hasPrevious();) { + current = it.previous(); + index--; + if (current.isBox() && !((KnuthBox) current).isAuxiliary()) { + boxCount++; + // non-auxiliary box => increase lineCount + if (current instanceof KnuthBlockBox) { + lineCount += ((KnuthBlockBox) current).getLineCount(); + } else { + // assume one line + lineCount++; - } + } + if (lineCount >= widows) { + break; + } + } + } + // if the sublist only consists of boxes, nothing to remove + if (index <= (contentList.size() - boxCount)) { + ElementListUtils.removeLegalBreaks( + contentList.subList(index, contentList.size())); + } + } + return contentList; + } + /** * Overridden to take into account that the childLM may be the block's * {@link LineLayoutManager}. @@ -132,25 +169,77 @@ // nop; will have been properly set by makeChildLayoutContext() } - if (childLM == this.childLMs.get(0)) { + boolean isFirst = childLM == this.childLMs.get(0); + if (isFirst) { childLC.setFlags(LayoutContext.SUPPRESS_BREAK_BEFORE); //Handled already by the parent (break collapsing, see above) } + List childElements; if (lmStack == null) { - return childLM.getNextKnuthElements(childLC, alignment); + childElements = childLM.getNextKnuthElements(childLC, alignment); } else { if (childLM instanceof LineLayoutManager) { assert (restartPosition instanceof LeafPosition); - return ((LineLayoutManager) childLM).getNextKnuthElements(childLC, alignment, + childElements + = ((LineLayoutManager) childLM).getNextKnuthElements(childLC, alignment, - (LeafPosition) restartPosition); + (LeafPosition) restartPosition); } else { - return childLM.getNextKnuthElements(childLC, alignment, + childElements = childLM.getNextKnuthElements(childLC, alignment, lmStack, restartPosition, restartAtLM); } } + + if (isFirst) { + // handle orphans + int lineCount = 0, boxCount = 0, endIndex = 0; + ListElement current; + for (ListIterator it = childElements.listIterator(); it.hasNext();) { + current = it.next(); + endIndex++; + if (current.isBox() && !((KnuthBox) current).isAuxiliary()) { + boxCount++; + // non-auxiliary box => increase lineCount + if (current instanceof KnuthBlockBox) { + lineCount += ((KnuthBlockBox) current).getLineCount(); + } else { + // assume one line + lineCount++; - } + } + if (lineCount >= orphans) { + break; + } + } + } + // if the sublist only consists of boxes, nothing to remove + if (endIndex > boxCount) { + ElementListUtils.removeLegalBreaks(childElements.subList(0, endIndex)); + } + } + return childElements; + } + + /** + * Overridden to deal with a special case for the "orphans" property. + * {@inheritDoc} + */ + @Override + protected void addInBetweenBreak(List contentList, + LayoutContext parentLC, LayoutContext childLC) { + + if (this.childLMs.size() > 1 + && this.curChildLM == this.childLMs.get(1)) { + // special case: second childLM; if the first childLM did not produce enough + // lines to satisfy this LM's orphans constraint, avoid adding a break possibility + if (ElementListUtils.getLineCount(contentList) < orphans) { + return; + } + } + + super.addInBetweenBreak(contentList, parentLC, childLC); + } + private void resetSpaces() { this.discardBorderBefore = false; this.discardBorderAfter = false; --- src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java (revision 1088079) +++ src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java (revision ) @@ -918,10 +918,7 @@ for (int i = 0; i < llPoss.getChosenLineCount(); i++) { - if (returnList.size() > 0 - && i > 0 //if i==0 break generated above already - && i >= fobj.getOrphans() - && i <= llPoss.getChosenLineCount() - fobj.getWidows()) { + if (returnList.size() > 0 && i > 0) { // penalty allowing a page break between lines Keep keep = getKeepTogether(); returnList.add(new BreakElement( --- src/java/org/apache/fop/layoutmgr/table/TableStepper.java (revision 1083863) +++ src/java/org/apache/fop/layoutmgr/table/TableStepper.java (revision ) @@ -181,6 +181,7 @@ TableContentPosition lastTCPos = null; LinkedList returnList = new LinkedList(); int laststep = 0; + int stepLines; int step = getFirstStep(); do { int maxRemainingHeight = getMaxRemainingHeight(); @@ -201,10 +202,15 @@ LinkedList footnoteList = new LinkedList(); //Put all involved grid units into a list List cellParts = new java.util.ArrayList(columnCount); + stepLines = 0; for (Iterator iter = activeCells.iterator(); iter.hasNext();) { ActiveCell activeCell = (ActiveCell) iter.next(); CellPart part = activeCell.createCellPart(); cellParts.add(part); + int partLines = part.getLineCount(); + if (partLines > stepLines) { + stepLines = partLines; + } activeCell.addFootnotes(footnoteList); } @@ -219,12 +225,15 @@ } lastTCPos = tcpos; + KnuthBlockBox newBox; // TODO TableStepper should remain as footnote-agnostic as possible if (footnoteList.isEmpty()) { - returnList.add(new KnuthBox(boxLen, tcpos, false)); + newBox = new KnuthBlockBox(boxLen, tcpos, false); } else { - returnList.add(new KnuthBlockBox(boxLen, footnoteList, tcpos, false)); + newBox = new KnuthBlockBox(boxLen, footnoteList, tcpos, false); } + newBox.setLineCount(stepLines); + returnList.add(newBox); int effPenaltyLen = Math.max(0, penaltyOrGlueLen); TableHFPenaltyPosition penaltyPos = new TableHFPenaltyPosition(getTableLM()); --- src/java/org/apache/fop/layoutmgr/KnuthBlockBox.java (revision 893238) +++ src/java/org/apache/fop/layoutmgr/KnuthBlockBox.java (revision ) @@ -19,6 +19,7 @@ package org.apache.fop.layoutmgr; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -35,54 +36,66 @@ * it isn't possible to get the opt value stored in a MinOptMax object. */ private int bpd; - private List footnoteList; + private List footnoteList; /** List of Knuth elements. This is a list of LinkedList elements. */ - private List elementLists = null; + private List> elementLists; + private int lineCount; /** * Creates a new box. * * @param width block progression dimension of this box + * @param pos the Position stored in this box + * @param auxiliary is this box auxiliary? + */ + public KnuthBlockBox(int width, Position pos, boolean auxiliary) { + super(width, pos, auxiliary); + // assume one line by default, if the box is not auxiliary + this.lineCount = (auxiliary ? 0 : 1); + } + + /** + * Creates a new box. + * + * @param width block progression dimension of this box * @param range min, opt, max inline progression dimension of this box * @param bpdim natural width of the line represented by this box. * @param pos the Position stored in this box * @param auxiliary is this box auxiliary? */ public KnuthBlockBox(int width, MinOptMax range, int bpdim, Position pos, boolean auxiliary) { - super(width, pos, auxiliary); - ipdRange = range; - bpd = bpdim; - footnoteList = new LinkedList(); + this(width, pos, auxiliary); + this.ipdRange = range; + this.bpd = bpdim; } /** * Creates a new box. * * @param width block progression dimension of this box - * @param list footnotes cited by elements in this box. The list contains the corresponding - * FootnoteBodyLayoutManagers + * @param footnoteList footnotes cited by elements in this box. + * The list contains the corresponding FootnoteBodyLayoutManagers * @param pos the Position stored in this box * @param auxiliary is this box auxiliary? */ - public KnuthBlockBox(int width, List list, Position pos, boolean auxiliary) { - super(width, pos, auxiliary); - ipdRange = MinOptMax.ZERO; - bpd = 0; - footnoteList = new LinkedList(list); + public KnuthBlockBox(int width, List footnoteList, Position pos, boolean auxiliary) { + this(width, MinOptMax.ZERO, 0, pos, auxiliary); + this.footnoteList = new LinkedList(footnoteList); } /** * @return the LMs for the footnotes cited in this box. */ - public List getFootnoteBodyLMs() { - return footnoteList; + public List getFootnoteBodyLMs() { + return (footnoteList == null + ? Collections.emptyList() : footnoteList); } /** * @return true if this box contains footnote citations. */ public boolean hasAnchors() { - return (footnoteList.size() > 0); + return (footnoteList != null && footnoteList.size() > 0); } /** @@ -92,7 +105,7 @@ */ public void addElementList(List list) { if (elementLists == null) { - elementLists = new LinkedList(); + elementLists = new LinkedList>(); } elementLists.add(list); } @@ -122,4 +135,21 @@ public int getBPD() { return bpd; } + + /** + * Returns the (maximum) number of lines in this box. + * @return the maximum number of lines contained in this box. + */ + public int getLineCount() { + return lineCount; -} + } + + /** + * Sets the maximum number of lines in this box. + * @param lineCount the number of lines + */ + public void setLineCount(int lineCount) { + this.lineCount = lineCount; + } + +} --- src/java/org/apache/fop/layoutmgr/table/CellPart.java (revision 990144) +++ src/java/org/apache/fop/layoutmgr/table/CellPart.java (revision ) @@ -20,6 +20,7 @@ package org.apache.fop.layoutmgr.table; import org.apache.fop.fo.flow.table.PrimaryGridUnit; +import org.apache.fop.layoutmgr.ElementListUtils; /** * Represents a non-divisible part of a grid unit. Used by the table stepper. @@ -117,6 +118,10 @@ return condAfterContentLength; } + int getLineCount() { + return ElementListUtils.getLineCount(pgu.getElements().subList(start, end + 1)); + } + /** {@inheritDoc} */ public String toString() { StringBuffer sb = new StringBuffer("Part: "); --- src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java (revision 1069941) +++ src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java (revision ) @@ -316,6 +316,7 @@ // footnote-agnostic as possible LinkedList footnoteList = null; ListElement el; + int stepLines = 0; for (int i = 0; i < elementLists.length; i++) { for (int j = start[i]; j <= end[i]; j++) { el = (ListElement) elementLists[i].get(j); @@ -326,17 +327,25 @@ footnoteList.addAll(((KnuthBlockBox) el).getFootnoteBodyLMs()); } } + int lineCount = ElementListUtils.getLineCount( + elementLists[i].subList(start[i], end[i] + 1)); + if (lineCount > stepLines) { + stepLines = lineCount; - } + } + } // add the new elements addedBoxHeight += boxHeight; ListItemPosition stepPosition = new ListItemPosition(this, start[0], end[0], start[1], end[1]); + KnuthBlockBox newBox; if (footnoteList == null) { - returnList.add(new KnuthBox(boxHeight, stepPosition, false)); + newBox = new KnuthBlockBox(boxHeight, stepPosition, false); } else { - returnList.add(new KnuthBlockBox(boxHeight, footnoteList, stepPosition, false)); + newBox = new KnuthBlockBox(boxHeight, footnoteList, stepPosition, false); } + newBox.setLineCount(stepLines); + returnList.add(newBox); if (addedBoxHeight < totalHeight) { Keep keep = keepWithNextActive.compare(getKeepTogether()); @@ -694,6 +703,5 @@ body.reset(); } - } --- src/java/org/apache/fop/layoutmgr/KnuthPenalty.java (revision 1079013) +++ src/java/org/apache/fop/layoutmgr/KnuthPenalty.java (revision ) @@ -46,6 +46,9 @@ /** Dummy, zero-width penalty */ public static final KnuthPenalty DUMMY_ZERO_PENALTY = new KnuthPenalty(0, 0, false, null, true); + /** Dummy break inhibitor */ + public static final KnuthPenalty DUMMY_INFINITE_PENALTY + = new KnuthPenalty(0, KnuthElement.INFINITE, false, null, true); private int penalty; private boolean penaltyFlagged; --- src/java/org/apache/fop/layoutmgr/ElementListUtils.java (revision 1056513) +++ src/java/org/apache/fop/layoutmgr/ElementListUtils.java (revision ) @@ -133,6 +133,87 @@ } /** + * Removes all legal break points from the given {@code List}. + * That is, this method:
+ *
    + *
  • removes penalties in between two boxes
  • + *
  • converts other penalties to inhibit breaks
  • + *
  • converts glues following a box into auxiliary boxes + * or penalty-glue sequences
  • + *
+ *
+ * Note: leading glues will not be treated, as it is assumed nothing precedes + * the first element of the given list. Similarly, trailing penalties would just be set + * to {@link KnuthElement#INFINITE}, as there is never a following box.
+ * Care should be taken to provide as much context elements as required. Ideally, the + * list should start with a box and end with a box or glue.
+ * + * @param elements the element list from which the breaks will be removed + */ + public static void removeLegalBreaks(List elements) { + + ListElement current; + boolean previousIsBox = false; + + for (ListIterator it = elements.listIterator(); it.hasNext();) { + + current = it.next(); + + if (current.isBox()) { + previousIsBox = true; + continue; + } else if (current.isGlue() && previousIsBox) { + KnuthGlue glue = (KnuthGlue)current; + if (glue.getStretch() == 0 && glue.getShrink() == 0) { + // non-stretchable glue => replace with a box of the same width + it.set(new KnuthBox(glue.getWidth(), glue.getPosition(), true)); + } else { + // stretchable glue => add break-inhibitor + it.previous(); + it.add(KnuthPenalty.DUMMY_INFINITE_PENALTY); + it.next(); + } + } else if (current.isPenalty() || current instanceof BreakElement + && !current.isForcedBreak()) { + boolean nextIsBox = (it.hasNext() && it.next().isBox()); + it.previous(); + if (previousIsBox && nextIsBox) { + // penalty or BreakElement in between boxes => remove + it.previous(); + it.remove(); + } else { + if (current.isPenalty()) { + ((KnuthPenalty)current).setPenalty(KnuthElement.INFINITE); + } else { + ((BreakElement)current).setPenaltyValue(KnuthElement.INFINITE); + } + } + } + previousIsBox = false; + } + } + + /** + * Obtains the (maximum) number of lines in the given list. + * @param elements the list to count the lines for + * @return the number of lines + */ + public static int getLineCount(List elements) { + int lineCount = 0; + for (ListElement current : elements) { + if (current.isBox() && !((KnuthBox) current).isAuxiliary()) { + if (current instanceof KnuthBlockBox) { + lineCount += ((KnuthBlockBox) current).getLineCount(); + } else { + // assume one line + lineCount++; + } + } + } + return lineCount; + } + + /** * Calculates the content length of the given element list. Warning: It doesn't take any * stretch and shrink possibilities into account. * @param elems the element list