diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java index 732e266bf..5f5a230c1 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java @@ -20,10 +20,10 @@ */ package com.openhtmltopdf.css.constants; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import java.util.TreeMap; - +import java.util.Objects; import com.openhtmltopdf.css.parser.CSSErrorHandler; import com.openhtmltopdf.css.parser.CSSParser; import com.openhtmltopdf.css.parser.PropertyValue; @@ -55,7 +55,7 @@ * * @author Patrick Wright */ -public final class CSSName implements Comparable { +public final class CSSName implements Comparable { /** * marker var, used for initialization */ @@ -115,12 +115,12 @@ public final class CSSName implements Comparable { /** * Map of all CSS properties */ - private static final Map ALL_PROPERTY_NAMES = new TreeMap(); + private static final Map ALL_PROPERTY_NAMES = new HashMap<>(); /** * Map of all non-shorthand CSS properties */ - private static final Map ALL_PRIMITIVE_PROPERTY_NAMES = new TreeMap(); + private static final Map ALL_PRIMITIVE_PROPERTY_NAMES = new HashMap<>(); /** * Unique CSSName instance for CSS2 property. @@ -915,6 +915,24 @@ public final class CSSName implements Comparable { INHERITS, new PrimitivePropertyBuilders.PageBreakInside() ); + + public final static CSSName BREAK_AFTER = + addProperty( + "break-after", + PRIMITIVE, + "auto", + NOT_INHERITED, + new PrimitivePropertyBuilders.BreakAfter() + ); + + public final static CSSName BREAK_BEFORE = + addProperty( + "break-before", + PRIMITIVE, + "auto", + NOT_INHERITED, + new PrimitivePropertyBuilders.BreakBefore() + ); /** * Unique CSSName instance for CSS2 property. @@ -1815,24 +1833,6 @@ public static int countCSSPrimitiveNames() { return ALL_PRIMITIVE_PROPERTY_NAMES.size(); } - /** - * Iterator of ALL CSS 2 visual property names. - * - * @return Returns - */ - public static Iterator allCSS2PropertyNames() { - return ALL_PROPERTY_NAMES.keySet().iterator(); - } - - /** - * Iterator of ALL primitive (non-shorthand) CSS 2 visual property names. - * - * @return Returns - */ - public static Iterator allCSS2PrimitivePropertyNames() { - return ALL_PRIMITIVE_PROPERTY_NAMES.keySet().iterator(); - } - /** * Returns true if the named property inherits by default, according to the * CSS2 spec. @@ -1877,8 +1877,7 @@ public static PropertyBuilder getPropertyBuilder(CSSName cssName) { * @return The byPropertyName value */ public static CSSName getByPropertyName(String propName) { - - return (CSSName) ALL_PROPERTY_NAMES.get(propName); + return ALL_PROPERTY_NAMES.get(propName); } public static CSSName getByID(int id) { @@ -1927,10 +1926,10 @@ private static synchronized CSSName addProperty( } static { - Iterator iter = ALL_PROPERTY_NAMES.values().iterator(); + Iterator iter = ALL_PROPERTY_NAMES.values().iterator(); ALL_PROPERTIES = new CSSName[ALL_PROPERTY_NAMES.size()]; while (iter.hasNext()) { - CSSName name = (CSSName) iter.next(); + CSSName name = iter.next(); ALL_PROPERTIES[name.FS_ID] = name; } } @@ -1941,8 +1940,8 @@ public void error(String uri, String message) { XRLog.cssParse("(" + uri + ") " + message); } }); - for (Iterator i = ALL_PRIMITIVE_PROPERTY_NAMES.values().iterator(); i.hasNext(); ) { - CSSName cssName = (CSSName)i.next(); + for (Iterator i = ALL_PRIMITIVE_PROPERTY_NAMES.values().iterator(); i.hasNext(); ) { + CSSName cssName = i.next(); if (cssName.initialValue.charAt(0) != '=' && cssName.implemented) { PropertyValue value = parser.parsePropertyValue( cssName, StylesheetInfo.USER_AGENT, cssName.initialValue); @@ -1959,14 +1958,14 @@ public void error(String uri, String message) { } } - //Assumed to be consistent with equals because CSSName is in essence an enum - public int compareTo(Object object) { - if (object == null) throw new NullPointerException();//required by Comparable - return FS_ID - ((CSSName) object).FS_ID;//will throw ClassCastException according to Comparable if not a CSSName + // Assumed to be consistent with equals because CSSName is in essence an enum + @Override + public int compareTo(CSSName object) { + Objects.requireNonNull(object); + return this.FS_ID - object.FS_ID; } - // FIXME equals, hashcode - + @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CSSName)) return false; @@ -1976,6 +1975,7 @@ public boolean equals(Object o) { return FS_ID == cssName.FS_ID; } + @Override public int hashCode() { return FS_ID; } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java index 383ed3688..564af3928 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java @@ -54,12 +54,9 @@ */ public class IdentValue implements FSDerivedValue { private static int maxAssigned = 0; + private static final Map ALL_IDENT_VALUES = new HashMap<>(); - /** - * Description of the Field - */ private final String ident; - public final int FS_ID; public final static IdentValue ABSOLUTE = addValue("absolute"); @@ -254,11 +251,10 @@ public class IdentValue implements FSDerivedValue { public static final IdentValue BORDER_BOX = addValue("border-box"); public static final IdentValue CONTENT_BOX = addValue("content-box"); - - /** - * Description of the Field + /* + * Column break. */ - private static Map ALL_IDENT_VALUES; + public static final IdentValue COLUMN = addValue("column"); /** * Constructor for the IdentValue object @@ -276,6 +272,7 @@ private IdentValue(String ident) { * * @return a string representation of the object. */ + @Override public String toString() { return ident; } @@ -290,22 +287,19 @@ public String toString() { * @return see desc. */ public static IdentValue getByIdentString(String ident) { - IdentValue val = (IdentValue) ALL_IDENT_VALUES.get(ident); + IdentValue val = ALL_IDENT_VALUES.get(ident); if (val == null) { throw new XRRuntimeException("Ident named " + ident + " has no IdentValue instance assigned to it."); } return val; } - /** - * TODO: doc - */ public static boolean looksLikeIdent(String ident) { - return (IdentValue) ALL_IDENT_VALUES.get(ident) != null; + return ALL_IDENT_VALUES.get(ident) != null; } public static IdentValue valueOf(String ident) { - return (IdentValue)ALL_IDENT_VALUES.get(ident); + return ALL_IDENT_VALUES.get(ident); } public static int getIdentCount() { @@ -318,10 +312,7 @@ public static int getIdentCount() { * @param ident The feature to be added to the Value attribute * @return Returns */ - private final static synchronized IdentValue addValue(String ident) { - if (ALL_IDENT_VALUES == null) { - ALL_IDENT_VALUES = new HashMap(); - } + private final static IdentValue addValue(String ident) { IdentValue val = new IdentValue(ident); ALL_IDENT_VALUES.put(ident, val); return val; diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java index 3f238294b..c41cb6b34 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java @@ -1303,7 +1303,25 @@ protected BitSet getAllowed() { return ALLOWED; } } + + public static class BreakBefore extends SingleIdent { + private static final BitSet ALLOWED = setFor( + new IdentValue[] { IdentValue.AUTO, IdentValue.COLUMN }); + + protected BitSet getAllowed() { + return ALLOWED; + } + } + public static class BreakAfter extends SingleIdent { + private static final BitSet ALLOWED = setFor( + new IdentValue[] { IdentValue.AUTO, IdentValue.COLUMN }); + + protected BitSet getAllowed() { + return ALLOWED; + } + } + public static class Page extends AbstractPropertyBuilder { public List buildDeclarations( CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java index 6f8fffacb..6bad460a6 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java @@ -943,6 +943,10 @@ public boolean isAutoZIndex() { } public boolean establishesBFC() { + if (hasColumns()) { + return true; + } + FSDerivedValue value = valueByName(CSSName.POSITION); if (value instanceof FunctionValue) { // running(header) @@ -1031,7 +1035,7 @@ public boolean isListItem() { } public boolean hasColumns() { - return !isIdent(CSSName.COLUMN_COUNT, IdentValue.AUTO) && asFloat(CSSName.COLUMN_COUNT) > 1; + return !isIdent(CSSName.COLUMN_COUNT, IdentValue.AUTO) && asFloat(CSSName.COLUMN_COUNT) > 1; } public int columnCount() { @@ -1091,6 +1095,14 @@ public boolean isForcePageBreakAfter() { return val == IdentValue.ALWAYS || val == IdentValue.LEFT || val == IdentValue.RIGHT; } + + public boolean isColumnBreakBefore() { + return isIdent(CSSName.BREAK_BEFORE, IdentValue.COLUMN); + } + + public boolean isColumnBreakAfter() { + return isIdent(CSSName.BREAK_AFTER, IdentValue.COLUMN); + } public boolean isAvoidPageBreakInside() { return isIdent(CSSName.PAGE_BREAK_INSIDE, IdentValue.AVOID); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/BoxBuilder.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/BoxBuilder.java index 999037329..91b30be22 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/BoxBuilder.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/BoxBuilder.java @@ -1154,7 +1154,7 @@ private static void createChildren( child.setElement(element); if (style.hasColumns() && c.isPrint()) { - FlowingColumnContainerBox cont = (FlowingColumnContainerBox) child; + FlowingColumnContainerBox cont = (FlowingColumnContainerBox) child; cont.setOnlyChild(c, new FlowingColumnBox(cont)); cont.getChild().setStyle(style.createAnonymousStyle(IdentValue.BLOCK)); cont.getChild().setElement(element); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/FloatManager.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/FloatManager.java index 891c54c4b..fab981133 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/FloatManager.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/FloatManager.java @@ -37,8 +37,8 @@ * non-floated (block) boxes. */ public class FloatManager { - private static final int LEFT = 1; - private static final int RIGHT = 2; + public static final int LEFT = 1; + public static final int RIGHT = 2; /* Lazily created for performance. */ private List _leftFloats = Collections.emptyList(); @@ -124,7 +124,7 @@ private void position(CssContext cssCtx, BlockFormattingContext bfc, } } - private List getFloats(int direction) { + public List getFloats(int direction) { return direction == LEFT ? _leftFloats : _rightFloats; } @@ -435,10 +435,10 @@ public void performFloatOperation(FloatOperation op) { performFloatOperation(op, getFloats(RIGHT)); } - private static class BoxOffset { - private BlockBox _box; - private int _x; - private int _y; + public static class BoxOffset { + private final BlockBox _box; + private final int _x; + private final int _y; public BoxOffset(BlockBox box, int x, int y) { _box = box; @@ -460,8 +460,8 @@ public int getY() { } private static class BoxDistance { - private BlockBox _box; - private int _distance; + private final BlockBox _box; + private final int _distance; public BoxDistance(BlockBox box, int distance) { _box = box; diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java index 04065bf4b..451fc3170 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java @@ -366,16 +366,8 @@ private int layoutRunningFooter(LayoutContext c) { } private boolean isNeedAnalyzePageBreaks() { - Box b = getParent(); - while (b != null) { - if (b.getStyle().isTable() && b.getStyle().isPaginateTable()) { - return false; - } - - b = b.getParent(); - } - - return true; + Box b = findAncestor(bx -> bx.getStyle().isTable() && bx.getStyle().isPaginateTable()); + return b == null; } private void analyzePageBreaks(LayoutContext c) { diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/BlockBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/BlockBox.java index 3e9abacd2..9ca07a3df 100755 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/BlockBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/BlockBox.java @@ -289,11 +289,7 @@ public LineBox getLineBox() { if (! isInline()) { return null; } else { - Box b = getParent(); - while (! (b instanceof LineBox)) { - b = b.getParent(); - } - return (LineBox) b; + return (LineBox) findAncestor(bx -> bx instanceof LineBox); } } @@ -930,7 +926,7 @@ private void calcExtraPageClearance(LayoutContext c) { } } - private void addBoxID(LayoutContext c) { + protected void addBoxID(LayoutContext c) { if (! isAnonymous()) { String name = c.getNamespaceHandler().getAnchorName(getElement()); if (name != null) { @@ -1308,7 +1304,7 @@ protected boolean isMayCollapseMarginsWithChildren() { // This will require a rethink if we ever truly layout incrementally // Should only ever collapse top margin and pick up collapsable // bottom margins by looking back up the tree. - private void collapseMargins(LayoutContext c) { + protected void collapseMargins(LayoutContext c) { if (! isTopMarginCalculated() || ! isBottomMarginCalculated()) { recalcMargin(c); RectPropertySet margin = getMargin(c); @@ -2263,11 +2259,7 @@ protected boolean isInlineBlock() { } public boolean isInMainFlow() { - Box flowRoot = this; - while (flowRoot.getParent() != null) { - flowRoot = flowRoot.getParent(); - } - + Box flowRoot = rootBox(); return flowRoot.isRoot(); } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/Box.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/Box.java index 996d22dbc..f11ca5aa6 100755 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/Box.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/Box.java @@ -32,6 +32,8 @@ import com.openhtmltopdf.layout.LayoutContext; import com.openhtmltopdf.layout.PaintingInfo; import com.openhtmltopdf.layout.Styleable; +import com.openhtmltopdf.render.FlowingColumnContainerBox.ColumnBreakStore; +import com.openhtmltopdf.util.LambdaUtil; import com.openhtmltopdf.util.XRLog; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -45,6 +47,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.Predicate; import java.util.logging.Level; public abstract class Box implements Styleable, DisplayListItem { @@ -1268,16 +1271,7 @@ protected void exportPageBoxText(RenderingContext c, Writer writer, int yPos) th } public boolean isInDocumentFlow() { - Box flowRoot = this; - while (true) { - Box parent = flowRoot.getParent(); - if (parent == null) { - break; - } else { - flowRoot = parent; - } - } - + Box flowRoot = rootBox(); return flowRoot.isRoot(); } @@ -1315,17 +1309,7 @@ protected boolean isMarginAreaRoot() { } public boolean isContainedInMarginBox() { - Box current = this; - while (true) { - Box parent = current.getParent(); - if (parent == null) { - break; - } else { - current = parent; - } - } - - return current.isMarginAreaRoot(); + return rootBox().isMarginAreaRoot(); } public int getEffectiveWidth() { @@ -1336,15 +1320,89 @@ protected boolean isInitialContainingBlock() { return false; } - public boolean isFlowingColumnBox() { - while (getParent() != null) { - if (getParent().isFlowingColumnBox()) { - return true; - } - } - - return false; - } + /** + * Is this box the first child of its parent? + */ + public boolean isFirstChild() { + return getParent() != null && + getParent().getChildCount() > 0 && + getParent().getChild(0) == this; + } + + /** + * Is this box unbreakable in regards to column break opportunities? + */ + public boolean isTerminalColumnBreak() { + return getChildCount() == 0; + } + + /** + * Creates a list of ancestors by walking up the chain of parent, + * grandparent, etc. Stops when the provided predicate returns false + * or the root box otherwise. + */ + public List ancestorsWhile(Predicate predicate) { + List ancestors = new ArrayList<>(4); + Box parent = this.getParent(); + + while (parent != null && predicate.test(parent)) { + ancestors.add(parent); + parent = parent.getParent(); + } + + return ancestors; + } + + /** + * Get all ancestors, up until the root box. + */ + public List ancestors() { + return ancestorsWhile(LambdaUtil.alwaysTrue()); + } + + /** + * Walks up the ancestor tree to the root testing ancestors agains + * the predicate. + * NOTE: Does not test against the current box (this). + * @return the box for which predicate returned true or null if none found. + */ + public Box findAncestor(Predicate predicate) { + Box parent = getParent(); + + while (parent != null && !predicate.test(parent)) { + parent = parent.getParent(); + } + + return parent; + } + + /** + * Returns the highest ancestor box. May be current box (this). + */ + public Box rootBox() { + return this.getParent() != null ? findAncestor(bx -> bx.getParent() == null) : this; + } + + /** + * Recursive method to find column break opportunities. + * @param store - use to report break opportunities. + */ + public void findColumnBreakOpportunities(ColumnBreakStore store) { + if (this.isTerminalColumnBreak() && this.isFirstChild()) { + // We report unprocessed ancestor container boxes so that they + // can be moved with the first child. + List ancestors = this.ancestorsWhile(store::checkContainerShouldProcess); + store.addBreak(this, ancestors); + } else if (this.isTerminalColumnBreak()) { + store.addBreak(this, null); + } else { + // This must be a container box so don't add it as a break opportunity. + // Recursively query children for their break opportunities. + for (Box child : getChildren()) { + child.findColumnBreakOpportunities(store); + } + } + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FloatedBoxData.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FloatedBoxData.java index 453a0cb31..af04efc6d 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FloatedBoxData.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FloatedBoxData.java @@ -56,4 +56,9 @@ public int getMarginFromSibling() { public void setMarginFromSibling(int marginFromSibling) { _marginFromSibling = marginFromSibling; } + + @Override + public String toString() { + return String.format("[manager='%s', drawing-layer='%s', margin-from-sibling='%d']", getManager(), getDrawingLayer(), getMarginFromSibling()); + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnBox.java index 81e4c831f..8c2d0309e 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnBox.java @@ -1,34 +1,29 @@ package com.openhtmltopdf.render; public class FlowingColumnBox extends BlockBox { - private final Box parent; - private int _colWidth; - - public FlowingColumnBox(Box parent) { - this.parent = parent; - } - - @Override - public boolean isFlowingColumnBox() { - return true; - } - - @Override - public int getWidth() { - return this._colWidth; - } - - @Override - public int getContentWidth() { - return this._colWidth; - } + private final Box parent; + private int _colWidth; - @Override - public Box getParent() { - return parent; - } + public FlowingColumnBox(Box parent) { + this.parent = parent; + } - public void setColumnWidth(int columnWidth) { - this._colWidth = columnWidth; - } + @Override + public int getWidth() { + return this._colWidth; + } + + @Override + public int getContentWidth() { + return this._colWidth; + } + + @Override + public Box getParent() { + return parent; + } + + public void setColumnWidth(int columnWidth) { + this._colWidth = columnWidth; + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnContainerBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnContainerBox.java index a8a9f7d72..88c12f824 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnContainerBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/FlowingColumnContainerBox.java @@ -1,122 +1,330 @@ package com.openhtmltopdf.render; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import com.openhtmltopdf.css.constants.CSSName; import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.layout.BlockFormattingContext; +import com.openhtmltopdf.layout.FloatManager; +import com.openhtmltopdf.layout.FloatManager.BoxOffset; import com.openhtmltopdf.layout.LayoutContext; +import com.openhtmltopdf.layout.PersistentBFC; public class FlowingColumnContainerBox extends BlockBox { - private FlowingColumnBox _child; - - private int findPageIndex(List pages, int y) { - int idx = 0; - for (PageBox page : pages) { - if (y >= page.getTop() && y <= page.getBottom()) { - return idx; - } - idx++; - } - return idx - 1; - } - - private static class ColumnPosition { - private final int copyY; // Absolute, What y position starts the column in the long column block. - private final int pasteY; // Absolute, What y position starts the column in the flowing column block for final render. - private final int maxColHeight; // Absolute, Maximum bottom of the column. - private final int pageIdx; - - private ColumnPosition(int copyY, int pasteY, int maxColHeight, int pageIdx) { - this.copyY = copyY; - this.pasteY = pasteY; - this.maxColHeight = maxColHeight; - this.pageIdx = pageIdx; - } - } - - private int adjust(LayoutContext c, Box child, int colGap, int colWidth, int columnCount, int xStart) { - int startY = this.getAbsY(); - List pages = c.getRootLayer().getPages(); - - int pageIdx = findPageIndex(pages, startY); - int colStart = startY; - int colHeight = pages.get(pageIdx).getBottom(); - int colIdx = 0; - int finalHeight = 0; - - List cols = new ArrayList(); - cols.add(new ColumnPosition(0, startY, colHeight, pageIdx)); - - for (Object chld : child.getChildren()) { - Box ch = (Box) chld; - - int yAdjust = cols.get(colIdx).pasteY - cols.get(colIdx).copyY; - int yProposedFinal = ch.getY() + yAdjust; - - if (yProposedFinal + ch.getHeight() > cols.get(colIdx).maxColHeight) { - int newColIdx = colIdx + 1; - int newPageIdx = newColIdx % columnCount == 0 ? cols.get(colIdx).pageIdx + 1 : cols.get(colIdx).pageIdx; - - if (newPageIdx >= pages.size()) { - c.getRootLayer().addPage(c); - } - - int pasteY = newColIdx % columnCount == 0 ? pages.get(newPageIdx).getTop() : cols.get(colIdx).pasteY; - - cols.add(new ColumnPosition(ch.getY(), pasteY, pages.get(newPageIdx).getBottom(), newPageIdx)); - colIdx++; - - yAdjust = cols.get(colIdx).pasteY - cols.get(colIdx).copyY; - yProposedFinal = ch.getY() + yAdjust; - } - - ch.setY(yProposedFinal - colStart); - finalHeight = Math.max(yProposedFinal - colStart + ch.getHeight(), finalHeight); - - int xAdjust = ((colIdx % columnCount) * colWidth) + ((colIdx % columnCount) * colGap); - ch.setX(ch.getX() + xAdjust + xStart); - } - - return finalHeight; - } - - @Override - public void layout(LayoutContext c, int contentStart) { - this.calcDimensions(c); - - int colCount = getStyle().columnCount(); - int colGapCount = colCount - 1; - - float colGap = getStyle().isIdent(CSSName.COLUMN_GAP, IdentValue.NORMAL) ? - getStyle().getLineHeight(c) : /* Use the line height as a normal column gap. */ - getStyle().getFloatPropertyProportionalWidth(CSSName.COLUMN_GAP, getContentWidth(), c); - - float totalGap = colGap * colGapCount; - int colWidth = (int) ((this.getContentWidth() - totalGap) / colCount); - - _child.setContainingLayer(this.getContainingLayer()); - _child.setContentWidth(colWidth); - _child.setColumnWidth(colWidth); - _child.setAbsX(this.getAbsX()); - _child.setAbsY(this.getAbsY()); - - c.setIsPrintOverride(false); - _child.layout(c, contentStart); - c.setIsPrintOverride(null); - - int height = adjust(c, _child, (int) colGap, colWidth, colCount, this.getLeftMBP() + this.getX()); - _child.setHeight(0); - this.setHeight(height); - _child.calcChildLocations(); - } - - public void setOnlyChild(LayoutContext c, FlowingColumnBox child) { - this._child = child; - this.addChild(child); - } - - public FlowingColumnBox getChild() { - return _child; - } + private FlowingColumnBox _child; + + // FIXME: Inefficient, replace with binary search. + private int findPageIndex(List pages, int y) { + int idx = 0; + for (PageBox page : pages) { + if (y >= page.getTop() && y <= page.getBottom()) { + return idx; + } + idx++; + } + return idx - 1; + } + + private static class ColumnPosition { + private final int columnIndex; + private final int copyY; // Absolute, What y position starts the column in the long column block. + private final int pasteY; // Absolute, What y position starts the column in the flowing column block for + // final render. + private final int maxColHeight; // Absolute, Maximum height of the column. + private final int pageIdx; + + private ColumnPosition(int columnIndex, int copyY, int pasteY, int maxColHeight, int pageIdx) { + this.columnIndex = columnIndex; + this.copyY = copyY; + this.pasteY = pasteY; + this.maxColHeight = maxColHeight; + this.pageIdx = pageIdx; + } + + @Override + public String toString() { + return String.format("[index='%d', copyY='%d', pasteY='%d', maxColHeight='%d', pageIdx='%d']", + columnIndex, copyY, pasteY, maxColHeight, pageIdx); + } + } + + public static class ColumnBreakOpportunity { + private final Box box; // The box where we can break. + private final List ancestors; // Ancestors of this box which should be moved with it. + + private ColumnBreakOpportunity(Box box, List ancestors) { + this.box = box; + this.ancestors = ancestors; + } + + static ColumnBreakOpportunity of(Box box, List ancestors) { + return new ColumnBreakOpportunity(box, ancestors); + } + + @Override + public String toString() { + return String.valueOf(box); + } + } + + public static class ColumnBreakStore { + // Break opportunity boxes. + private final List breaks = new ArrayList<>(); + // Which container boxes have been processed, so we don't move them twice. + private final Set processedContainers = new HashSet<>(); + + /** + * Add a break opportunity. If this is a break opportunity and a first child, it + * should also add all unprocessed ancestors, so they can be moved with the + * first child. + */ + public void addBreak(Box box, List ancestors) { + breaks.add(ColumnBreakOpportunity.of(box, ancestors)); + } + + /** + * Whether an ancestor box needs to be added to the list of ancestors. + * @return true to process this ancestor (we haven't seen it yet). + */ + public boolean checkContainerShouldProcess(Box container) { + if (container instanceof FlowingColumnContainerBox || + container instanceof FlowingColumnBox) { + return false; + } + + return processedContainers.add(container); + } + + @Override + public String toString() { + return breaks.toString(); + } + } + + private void layoutFloats(TreeMap columns, List floats, int columnCount, int colWidth, int colGap) { + for (BoxOffset bo : floats) { + BlockBox floater = bo.getBox(); + + ColumnBreakStore store = new ColumnBreakStore(); + floater.findColumnBreakOpportunities(store); + + for (ColumnBreakOpportunity breakOp : store.breaks) { + Map.Entry entry = columns.floorEntry(breakOp.box.getAbsY()); + ColumnPosition column = entry.getValue(); + + int yAdjust = column.pasteY - column.copyY; + int xAdjust = ((column.columnIndex % columnCount) * colWidth) + ((column.columnIndex % columnCount) * colGap); + + reposition(breakOp.box, xAdjust, yAdjust); + + if (breakOp.ancestors != null) { + repositionAncestors(breakOp.ancestors, xAdjust, yAdjust); + } + + if (breakOp.box instanceof LineBox) { + breakOp.box.calcChildLocations(); + } + } + } + } + + private void layoutFloats(TreeMap columnMap, PersistentBFC bfc, int columnCount, int colWidth, int colGap) { + List floatsL = this.getPersistentBFC().getFloatManager().getFloats(FloatManager.LEFT); + List floatsR = this.getPersistentBFC().getFloatManager().getFloats(FloatManager.RIGHT); + + layoutFloats(columnMap, floatsL, columnCount, colWidth, colGap); + layoutFloats(columnMap, floatsR, columnCount, colWidth, colGap); + } + + private void reposition(Box box, int xAdjust, int yAdjust) { + if (box instanceof BlockBox && + ((BlockBox) box).isFloated()) { + box.setX(box.getX() + xAdjust); + box.setY(box.getY() + yAdjust); + } else { + box.setAbsY(box.getAbsY() + yAdjust); + box.setAbsX(box.getAbsX() + xAdjust); + } + } + + private void repositionAncestors(List ancestors, int xAdjust, int yAdjust) { + for (Box ancestor : ancestors) { + reposition(ancestor, xAdjust, yAdjust); + } + + // FIXME: We do not resize or duplicate ancestor container boxes, + // so if user has used border, background color + // or overflow: hidden it will produce incorrect results. + } + + private int adjustUnbalanced(LayoutContext c, Box child, int colGap, int colWidth, int columnCount, int xStart) { + // At the start of this method we have one long column in child. + // This method works by going through the boxes and adjusting their position + // into the current column. + + final int startY = this.getAbsY(); + final List pages = c.getRootLayer().getPages(); + + final boolean haveFloats = + !this.getPersistentBFC().getFloatManager().getFloats(FloatManager.LEFT).isEmpty() || + !this.getPersistentBFC().getFloatManager().getFloats(FloatManager.RIGHT).isEmpty(); + + // We only need the tree map if we have floats. + final TreeMap columnMap = haveFloats ? new TreeMap<>() : null; + + // These are all running values that change as we layout our boxes into columns. + int pageIdx = findPageIndex(pages, startY); + int colStart = startY; + int colHeight = pages.get(pageIdx).getBottom() - this.getChild().getAbsY(); + int colIdx = 0; + int finalHeight = 0; + + if (child.getHeight() <= colHeight) { + // We fit in the first column. + return child.getHeight(); + } + + // Recursively find all the column break opportunities (typically line boxes). + ColumnBreakStore store = new ColumnBreakStore(); + child.findColumnBreakOpportunities(store); + + if (store.breaks.isEmpty() || store.breaks.size() == 1) { + // Nothing we can do except overflow. + // The only break is at the start of the first child. + return this.getChild().getHeight(); + } + + // Add our first column. + ColumnPosition current = new ColumnPosition(colIdx, /* copy-from */ colStart, /* copy-to */ colStart, colHeight, pageIdx); + if (haveFloats) { + columnMap.put(colStart, current); + } + + // FIXME: Don't sort if we have in order - common case. + Collections.sort(store.breaks, + Comparator.comparingInt(brk -> brk.box.getAbsY() + brk.box.getBorderBoxHeight(c))); + + for (int i = 0; i < store.breaks.size(); i++) { + ColumnBreakOpportunity br = store.breaks.get(i); + ColumnBreakOpportunity nextBr = i < store.breaks.size() - 1 ? store.breaks.get(i + 1) : null; + Box ch = br.box; + + int yAdjust = current.pasteY - current.copyY; + int yProposedFinal = ch.getAbsY() + yAdjust; + ch.setAbsY(yProposedFinal); + + // We need the max height of the column which is the bottom of the current box + // minus the top of the column. + finalHeight = Math.max((yProposedFinal + ch.getBorderBoxHeight(c)) - startY, finalHeight); + + // x position should be easy. + int xAdjust = ((colIdx % columnCount) * colWidth) + ((colIdx % columnCount) * colGap); + ch.setAbsX(ch.getAbsX() + xAdjust); + + if (br.ancestors != null) { + // We move container ancestors with the first child that is + // a break opportunity. + // EXAMPLE: column box -> p -> ul -> li -> line box + // We would move the p, ul and li on the first line of the first li. + // For the second li we only have to move the parent li as p and ul have + // already been processed. + repositionAncestors(br.ancestors, xAdjust, yAdjust); + } + + if (ch instanceof LineBox) { + // We do not call this on other kind of boxes as it would undo our work in moving them. + ch.calcChildLocations(); + } + + if (nextBr != null) { + Box next = nextBr.box; + int nextYHeight = next.getAbsY() + yAdjust + next.getBorderBoxHeight(c) - current.pasteY; + + if (nextYHeight > current.maxColHeight || + ch.getStyle().isColumnBreakAfter() || + next.getStyle().isColumnBreakBefore()) { + // We have moved past the bottom of the current column (or explicit break). + // Time for a new column. + // FIXME: What if box doesn't fit in new column either? + int newColIdx = colIdx + 1; + + // And possibly a new page. + boolean needNewPage = newColIdx % columnCount == 0; + int newPageIdx = needNewPage ? current.pageIdx + 1 : current.pageIdx; + + if (newPageIdx >= pages.size()) { + c.getRootLayer().addPage(c); + } + + // We need the y top of the new column. + PageBox page = pages.get(newPageIdx); + int pasteY = needNewPage ? page.getTop() : current.pasteY; + int copyY = next.getAbsY(); + + current = new ColumnPosition(newColIdx, copyY, pasteY, page.getBottom() - pasteY, newPageIdx); + if (haveFloats) { + columnMap.put(copyY, current); + } + colIdx++; + } + } + } + + if (haveFloats) { + layoutFloats(columnMap, this.getPersistentBFC(), columnCount, colWidth, colGap); + } + + return finalHeight; + } + + @Override + public void layout(LayoutContext c, int contentStart) { + BlockFormattingContext bfc = new BlockFormattingContext(this, c); + c.pushBFC(bfc); + + addBoxID(c); + + this.calcDimensions(c); + + int colCount = getStyle().columnCount(); + int colGapCount = colCount - 1; + + float colGap = getStyle().isIdent(CSSName.COLUMN_GAP, IdentValue.NORMAL) ? getStyle().getLineHeight(c) + : /* Use the line height as a normal column gap. */ + getStyle().getFloatPropertyProportionalWidth(CSSName.COLUMN_GAP, getContentWidth(), c); + + float totalGap = colGap * colGapCount; + int colWidth = (int) ((this.getContentWidth() - totalGap) / colCount); + + _child.setContainingLayer(this.getContainingLayer()); + _child.setContentWidth(colWidth); + _child.setColumnWidth(colWidth); + _child.setAbsX(this.getAbsX()); + _child.setAbsY(this.getAbsY()); + + c.setIsPrintOverride(false); + _child.layout(c, contentStart); + c.setIsPrintOverride(null); + + int height = adjustUnbalanced(c, _child, (int) colGap, colWidth, colCount, this.getLeftMBP() + this.getX()); + _child.setHeight(0); + this.setHeight(height); + c.popBFC(); + } + + public void setOnlyChild(LayoutContext c, FlowingColumnBox child) { + this._child = child; + this.addChild(child); + } + + public FlowingColumnBox getChild() { + return _child; + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java index 888c15fa7..a71a57ad5 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java @@ -504,11 +504,7 @@ private void addToContentList(List list) { } public LineBox getLineBox() { - Box b = getParent(); - while (! (b instanceof LineBox)) { - b = b.getParent(); - } - return (LineBox)b; + return (LineBox) findAncestor(bx -> bx instanceof LineBox); } public List getElementWithContent() { @@ -873,10 +869,7 @@ protected void restyleChildren(LayoutContext c) { public Box getRestyleTarget() { // Inline boxes may be broken across lines so back out // to the nearest block box - Box result = getParent(); - while (result instanceof InlineLayoutBox) { - result = result.getParent(); - } + Box result = findAncestor(bx -> !(bx instanceof InlineLayoutBox)); return result.getParent(); } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/LineBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/LineBox.java index 597f7a7f9..5b18bbb28 100755 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/LineBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/LineBox.java @@ -42,6 +42,7 @@ import com.openhtmltopdf.layout.Layer; import com.openhtmltopdf.layout.LayoutContext; import com.openhtmltopdf.layout.PaintingInfo; +import com.openhtmltopdf.render.FlowingColumnContainerBox.ColumnBreakStore; import com.openhtmltopdf.util.XRRuntimeException; /** @@ -152,8 +153,7 @@ private void lookForDynamicFunctions(RenderingContext c) { } public boolean isFirstLine() { - Box parent = getParent(); - return parent != null && parent.getChildCount() > 0 && parent.getChild(0) == this; + return super.isFirstChild(); } public void prunePendingInlineBoxes() { @@ -643,6 +643,12 @@ public boolean isLayedOutRTL() { public boolean hasNonTextContent(CssContext c) { return _textDecorations != null && _textDecorations.size() > 0; } + + @Override + public boolean isTerminalColumnBreak() { + // A line box can not be further broken for the purpose of column breaks. + return true; + } } /* diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LambdaUtil.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LambdaUtil.java new file mode 100644 index 000000000..8b08c3846 --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LambdaUtil.java @@ -0,0 +1,15 @@ +package com.openhtmltopdf.util; + +import java.util.function.Predicate; + +public class LambdaUtil { + private LambdaUtil() { } + + public static Predicate alwaysTrue() { + return (unused) -> true; + } + + public static Predicate alwaysFalse() { + return (unused) -> false; + } +} diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-floats-unbalanced.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-floats-unbalanced.pdf new file mode 100644 index 000000000..54bc6f095 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-floats-unbalanced.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-nested-unbalanced.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-nested-unbalanced.pdf new file mode 100644 index 000000000..1d75ad5b9 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-nested-unbalanced.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-simple-unbalanced.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-simple-unbalanced.pdf new file mode 100644 index 000000000..109d45715 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/columns-simple-unbalanced.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-floats-unbalanced.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-floats-unbalanced.html new file mode 100644 index 000000000..2709441d9 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-floats-unbalanced.html @@ -0,0 +1,73 @@ + + + + + +

Column Support

+
+

Chapter 1

+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat feugiat massa vitae congue. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed non blandit nisl, +id scelerisque sem. +
+

Chapter 2

+
+
    +
  1. One
  2. +
  3. Two
  4. +
+
+Pellentesque sagittis maximus elit et eleifend. Mauris vitae eros quis eros bibendum bibendum. +Cras sed massa lectus. Suspendisse mollis mi id tortor commodo, in semper eros gravida. Integer pulvinar nulla urna, +vel sagittis leo maximus id. Nam sit amet arcu non justo molestie interdum. Phasellus gravida nisi ut bibendum feugiat. +
+

Chapter 3

+
+This is a left float. +
+Aenean ultricies augue vitae quam viverra, et maximus tortor eleifend. +Duis quis venenatis turpis, eu sollicitudin magna. Proin ullamcorper sem vitae sem facilisis +luctus. Mauris sed lectus ac nulla placerat pellentesque ut vel diam. Sed vulputate ornare sem vel placerat. +
+
+
+Nam auctor libero ante, et volutpat arcu dictum ac. +Class aptent taciti sociosqu ad litora torquent per conubia nostra, +per inceptos himenaeos. Nullam non sodales augue, in efficitur turpis. Duis maximus a orci in consequat. +
+

Chapter 4

+Flyingsaucer +Mauris luctus nec ex et volutpat. Nullam mollis luctus ultrices. Sed malesuada porttitor dui et blandit. +Fusce ultrices metus sem, sed vehicula sem hendrerit vel. Phasellus sed turpis eget urna luctus sagittis in eget mauris. +Vivamus rhoncus cursus lectus vel commodo. Nunc vulputate commodo porta. Nunc eget augue nec nisl hendrerit tincidunt sit +amet sed urna. +
+ + diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-nested-unbalanced.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-nested-unbalanced.html new file mode 100644 index 000000000..9e79c87a3 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-nested-unbalanced.html @@ -0,0 +1,64 @@ + + + + + +
+
+

+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat feugiat massa vitae congue. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed non blandit nisl, +id scelerisque sem. +

+

+Pellentesque sagittis maximus elit et eleifend. Mauris vitae eros quis eros bibendum bibendum. +Cras sed massa lectus. Suspendisse mollis mi id tortor commodo, in semper eros gravida. Integer pulvinar nulla urna, +vel sagittis leo maximus id. Nam sit amet arcu non justo molestie interdum. Phasellus gravida nisi ut bibendum feugiat. +

+
    +
  • One
  • +
  • Two
  • +
  • Three
  • +
  • Four
  • +
  • Five
  • +
  • Vivamus rhoncus cursus lectus vel commodo. Nunc vulputate commodo porta. Nunc eget augue nec nisl hendrerit tincidunt sit +amet sed urna.
  • +
+Flyingsaucer +

+In vitae nulla nec sapien imperdiet interdum eget et sem. +

+

+Aenean ultricies augue vitae quam viverra, et maximus tortor eleifend. Duis quis venenatis turpis, eu sollicitudin magna. +Proin ullamcorper sem vitae sem facilisis luctus. Mauris sed lectus ac nulla placerat pellentesque ut vel diam. +Sed vulputate ornare sem vel placerat. +

+

+Nam auctor libero ante, et volutpat arcu dictum ac. Class aptent taciti sociosqu ad litora torquent per conubia nostra, +per inceptos himenaeos. Nullam non sodales augue, in efficitur turpis. Duis maximus a orci in consequat. +Mauris luctus nec ex et volutpat. Nullam mollis luctus ultrices. Sed malesuada porttitor dui et blandit. +

+
+ + diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-simple-unbalanced.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-simple-unbalanced.html new file mode 100644 index 000000000..bf9fbdf42 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/columns-simple-unbalanced.html @@ -0,0 +1,38 @@ + + + + + +
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat feugiat massa vitae congue. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed non blandit nisl, +id scelerisque sem. Pellentesque sagittis maximus elit et eleifend. Mauris vitae eros quis eros bibendum bibendum. +Cras sed massa lectus. Suspendisse mollis mi id tortor commodo, in semper eros gravida. Integer pulvinar nulla urna, +vel sagittis leo maximus id. Nam sit amet arcu non justo molestie interdum. Phasellus gravida nisi ut bibendum feugiat. +In vitae nulla nec sapien imperdiet interdum eget et sem. Aenean ultricies augue vitae quam viverra, et +maximus tortor eleifend. Duis quis venenatis turpis, eu sollicitudin magna. Proin ullamcorper sem vitae sem facilisis +luctus. Mauris sed lectus ac nulla placerat pellentesque ut vel diam. Sed vulputate ornare sem vel placerat. +Nam auctor libero ante, et volutpat arcu dictum ac. Class aptent taciti sociosqu ad litora torquent per conubia nostra, +per inceptos himenaeos. Nullam non sodales augue, in efficitur turpis. Duis maximus a orci in consequat. +Mauris luctus nec ex et volutpat. Nullam mollis luctus ultrices. Sed malesuada porttitor dui et blandit. +Fusce ultrices metus sem, sed vehicula sem hendrerit vel. Phasellus sed turpis eget urna luctus sagittis in eget mauris. +Vivamus rhoncus cursus lectus vel commodo. Nunc vulputate commodo porta. Nunc eget augue nec nisl hendrerit tincidunt sit +amet sed urna. +
+ + diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java index 684ee4bce..7adb5b889 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java @@ -614,4 +614,30 @@ public void testJustifySpaceAtEnd() throws IOException { assertTrue(vtester.runTest("text-justify-space-at-end", WITH_COLLAPSED_LINE_BREAKER)); } + /** + * Tests that flowing columns containing only text in unbalanced mode + * are correctly laid out. + */ + @Test + public void testColumnsSimpleUnbalanced() throws IOException { + assertTrue(run("columns-simple-unbalanced")); + } + + /** + * Tests columns with nested content such as paragraphs, lists and span. + */ + @Test + public void testColumnsNestedUnbalanced() throws IOException { + assertTrue(run("columns-nested-unbalanced")); + } + + /** + * Tests columns containing floated and clear elements. + * Also tests explicit column breaks. + */ + @Test + public void testColumnsFloatsUnbalanced() throws IOException { + assertTrue(run("columns-floats-unbalanced")); + } + }