Skip to content

Commit 284a3a0

Browse files
author
Dmitry Radchuk
committed
Add column width and gap calculation
DEVSIX-7553
1 parent 5a61173 commit 284a3a0

File tree

11 files changed

+200
-5
lines changed

11 files changed

+200
-5
lines changed

layout/src/main/java/com/itextpdf/layout/exceptions/LayoutExceptionMessageConstant.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,8 @@ public final class LayoutExceptionMessageConstant {
5151
public static final String INLINE_VERTICAL_ALIGNMENT_DOESN_T_NEED_A_VALUE =
5252
"Inline vertical alignment \"{0}\" doesn't need a value";
5353

54+
public static final String INVALID_COLUMN_PROPERTIES =
55+
"Invalid column-count/column-width/column-gap properties, they're absent or have negative value";
56+
5457
private LayoutExceptionMessageConstant(){}
5558
}

layout/src/main/java/com/itextpdf/layout/properties/Property.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public final class Property {
6868
public static final int COLLAPSING_MARGINS = 89;
6969
public static final int COLSPAN = 16;
7070
public static final int COLUMN_COUNT = 138;
71+
public static final int COLUMN_WIDTH = 142;
72+
public static final int COLUMN_GAP = 143;
7173
public static final int DESTINATION = 17;
7274
public static final int FILL_AVAILABLE_AREA = 86;
7375
public static final int FILL_AVAILABLE_AREA_ON_SPLIT = 87;
@@ -216,7 +218,7 @@ public final class Property {
216218
* related to textual operations. Indicates whether or not this type of property is inheritable.
217219
*/
218220
private static final boolean[] INHERITED_PROPERTIES;
219-
private static final int MAX_INHERITED_PROPERTY_ID = 140;
221+
private static final int MAX_INHERITED_PROPERTY_ID = 143;
220222

221223
static {
222224
INHERITED_PROPERTIES = new boolean[MAX_INHERITED_PROPERTY_ID + 1];

layout/src/main/java/com/itextpdf/layout/renderer/MulticolRenderer.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This file is part of the iText (R) project.
2525
import com.itextpdf.kernel.geom.Rectangle;
2626
import com.itextpdf.layout.borders.Border;
2727
import com.itextpdf.layout.element.MulticolContainer;
28+
import com.itextpdf.layout.exceptions.LayoutExceptionMessageConstant;
2829
import com.itextpdf.layout.layout.LayoutArea;
2930
import com.itextpdf.layout.layout.LayoutContext;
3031
import com.itextpdf.layout.layout.LayoutResult;
@@ -47,6 +48,7 @@ public class MulticolRenderer extends AbstractRenderer {
4748
private float columnWidth;
4849
private float approximateHeight;
4950
private Float heightFromProperties;
51+
private float columnGap;
5052

5153
/**
5254
* Creates a DivRenderer from its corresponding layout object.
@@ -80,9 +82,9 @@ public LayoutResult layout(LayoutContext layoutContext) {
8082
applyPaddings(actualBBox, false);
8183
applyBorderBox(actualBBox, false);
8284
applyMargins(actualBBox, false);
85+
calculateColumnCountAndWidth(actualBBox.getWidth());
86+
8387
heightFromProperties = determineHeight(actualBBox);
84-
columnCount = (int) this.<Integer>getProperty(Property.COLUMN_COUNT);
85-
columnWidth = actualBBox.getWidth() / columnCount;
8688
if (this.elementRenderer == null) {
8789
// initialize elementRenderer on first layout when first child represents renderer of element which
8890
// should be layouted in multicol, because on the next layouts this can have multiple children
@@ -259,6 +261,30 @@ private MulticolLayoutResult balanceContentAndLayoutColumns(LayoutContext prelay
259261
return result;
260262
}
261263

264+
//algorithm is based on pseudo algorithm from https://www.w3.org/TR/css-multicol-1/#propdef-column-span
265+
private void calculateColumnCountAndWidth(float initialWidth) {
266+
final Integer columnCount = (Integer)this.<Integer>getProperty(Property.COLUMN_COUNT);
267+
final Float columnWidth = (Float)this.<Float>getProperty(Property.COLUMN_WIDTH);
268+
final Float columnGap = (Float)this.<Float>getProperty(Property.COLUMN_GAP);
269+
this.columnGap = columnGap != null ? columnGap.floatValue() : 0;
270+
if ((columnCount == null && columnWidth == null)
271+
|| (columnCount != null && columnCount.intValue() < 0)
272+
|| (columnWidth != null && columnWidth.floatValue() < 0)) {
273+
throw new IllegalStateException(LayoutExceptionMessageConstant.INVALID_COLUMN_PROPERTIES);
274+
}
275+
if (columnWidth == null) {
276+
this.columnCount = columnCount.intValue();
277+
} else if (columnCount == null) {
278+
this.columnCount = Math.max(1, (int) Math.floor((double)((initialWidth + this.columnGap)
279+
/ (columnWidth.floatValue() + this.columnGap))));
280+
} else {
281+
this.columnCount = Math.min((int) columnCount,
282+
Math.max(1, (int) Math.floor((double) ((initialWidth + this.columnGap)
283+
/ (columnWidth.floatValue() + this.columnGap)))));
284+
}
285+
this.columnWidth = Math.max(0.0f, ((initialWidth + this.columnGap)/this.columnCount - this.columnGap));
286+
}
287+
262288
private void clearOverFlowRendererIfNeeded(MulticolLayoutResult result) {
263289
//When we have a height set on the element but the content doesn't fit in the given height
264290
//we don't want to render the overflow renderer as it would be rendered in the next area
@@ -309,7 +335,7 @@ private MulticolLayoutResult layoutColumnsAndReturnOverflowRenderer(LayoutContex
309335
LayoutArea tempArea = preLayoutContext.getArea().clone();
310336
tempArea.getBBox().setWidth(columnWidth);
311337
tempArea.getBBox().setHeight(workingHeight);
312-
tempArea.getBBox().setX(actualBBox.getX() + columnWidth * i);
338+
tempArea.getBBox().setX(actualBBox.getX() + (columnWidth + columnGap) * i);
313339
tempArea.getBBox().setY(actualBBox.getY() + actualBBox.getHeight() - tempArea.getBBox().getHeight());
314340

315341
LayoutContext columnContext = new LayoutContext(tempArea, preLayoutContext.getMarginsCollapseInfo(),

layout/src/test/java/com/itextpdf/layout/element/MulticolContainerTest.java

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This file is part of the iText (R) project.
2525
import com.itextpdf.commons.utils.PlaceHolderTextUtil;
2626
import com.itextpdf.commons.utils.PlaceHolderTextUtil.PlaceHolderTextBy;
2727
import com.itextpdf.io.image.ImageDataFactory;
28+
import com.itextpdf.io.source.ByteArrayOutputStream;
2829
import com.itextpdf.io.util.UrlUtil;
2930
import com.itextpdf.kernel.colors.Color;
3031
import com.itextpdf.kernel.colors.ColorConstants;
@@ -35,6 +36,7 @@ This file is part of the iText (R) project.
3536
import com.itextpdf.layout.Document;
3637
import com.itextpdf.layout.borders.Border;
3738
import com.itextpdf.layout.borders.SolidBorder;
39+
import com.itextpdf.layout.exceptions.LayoutExceptionMessageConstant;
3840
import com.itextpdf.layout.logs.LayoutLogMessageConstant;
3941
import com.itextpdf.layout.properties.Background;
4042
import com.itextpdf.layout.properties.HorizontalAlignment;
@@ -994,8 +996,89 @@ public void continuousColumContainerSetHeightSmaller() throws IOException, Inter
994996
});
995997
}
996998

999+
@Test
1000+
public void paragraphWithColumnWidthTest() throws IOException, InterruptedException {
1001+
String outFileName = DESTINATION_FOLDER + "paragraphWithColumnWidthTest.pdf";
1002+
String cmpFileName = SOURCE_FOLDER + "cmp_paragraphWithColumnWidthTest.pdf";
1003+
1004+
try (Document document = new Document(new PdfDocument(new PdfWriter(outFileName)))) {
1005+
Div columnContainer = new MulticolContainer();
1006+
columnContainer.setProperty(Property.COLUMN_WIDTH, 200.0f);
1007+
Paragraph paragraph = createDummyParagraph();
1008+
columnContainer.add(paragraph);
1009+
document.add(columnContainer);
1010+
}
1011+
//expecting 2 columns with ~260px width each
1012+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
1013+
}
1014+
1015+
@Test
1016+
public void paragraphWithColumnWidthAndColumnCountTest() throws IOException, InterruptedException {
1017+
String outFileName = DESTINATION_FOLDER + "paragraphWithColumnWidthAndColumnCountTest.pdf";
1018+
String cmpFileName = SOURCE_FOLDER + "cmp_paragraphWithColumnWidthAndColumnCountTest.pdf";
1019+
1020+
try (Document document = new Document(new PdfDocument(new PdfWriter(outFileName)))) {
1021+
Div columnContainer = new MulticolContainer();
1022+
//column width is ignored in this case, because column-count requires higher width
1023+
columnContainer.setProperty(Property.COLUMN_WIDTH, 100.0f);
1024+
columnContainer.setProperty(Property.COLUMN_COUNT, 2);
1025+
Paragraph paragraph = createDummyParagraph();
1026+
columnContainer.add(paragraph);
1027+
document.add(columnContainer);
1028+
}
1029+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
1030+
}
1031+
1032+
@Test
1033+
public void paragraphWithInvalidColumnValuesTest() {
1034+
1035+
try (Document document = new Document(new PdfDocument(new PdfWriter(new ByteArrayOutputStream())))) {
1036+
Div columnContainer = new MulticolContainer();
1037+
//column width is ignored in this case, because column-count requires higher width
1038+
columnContainer.setProperty(Property.COLUMN_WIDTH, -30.0f);
1039+
columnContainer.setProperty(Property.COLUMN_COUNT, -2);
1040+
columnContainer.setProperty(Property.COLUMN_GAP, -20.0f);
1041+
Paragraph paragraph = createDummyParagraph();
1042+
columnContainer.add(paragraph);
1043+
Throwable exception = Assert.assertThrows(IllegalStateException.class, () -> document.add(columnContainer));
1044+
Assert.assertEquals(LayoutExceptionMessageConstant.INVALID_COLUMN_PROPERTIES, exception.getMessage());
1045+
}
1046+
}
1047+
1048+
@Test
1049+
public void paragraphWithColumnWidthAndGapTest() throws IOException, InterruptedException {
1050+
String outFileName = DESTINATION_FOLDER + "paragraphWithColumnWidthAndGapTest.pdf";
1051+
String cmpFileName = SOURCE_FOLDER + "cmp_paragraphWithColumnWidthAndGapTest.pdf";
1052+
1053+
try (Document document = new Document(new PdfDocument(new PdfWriter(outFileName)))) {
1054+
Div columnContainer = new MulticolContainer();
1055+
columnContainer.setProperty(Property.COLUMN_WIDTH, 100.0f);
1056+
columnContainer.setProperty(Property.COLUMN_GAP, 100.0f);
1057+
Paragraph paragraph = createDummyParagraph();
1058+
columnContainer.add(paragraph);
1059+
document.add(columnContainer);
1060+
}
1061+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
1062+
}
1063+
1064+
@Test
1065+
public void paragraphWithColumnCountAndGapTest() throws IOException, InterruptedException {
1066+
String outFileName = DESTINATION_FOLDER + "paragraphWithColumnCountAndGapTest.pdf";
1067+
String cmpFileName = SOURCE_FOLDER + "cmp_paragraphWithColumnCountAndGapTest.pdf";
1068+
1069+
try (Document document = new Document(new PdfDocument(new PdfWriter(outFileName)))) {
1070+
Div columnContainer = new MulticolContainer();
1071+
columnContainer.setProperty(Property.COLUMN_COUNT, 5);
1072+
columnContainer.setProperty(Property.COLUMN_GAP, 50.0f);
1073+
Paragraph paragraph = createDummyParagraph();
1074+
columnContainer.add(paragraph);
1075+
document.add(columnContainer);
1076+
}
1077+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
1078+
}
1079+
9971080

998-
private <T extends IBlockElement> void executeTest(String testName, T container, Consumer<T> executor)
1081+
private void executeTest(String testName, MulticolContainer container, Consumer<MulticolContainer> executor)
9991082
throws IOException, InterruptedException {
10001083
String filename = DESTINATION_FOLDER + testName + ".pdf";
10011084
String cmpName = SOURCE_FOLDER + "cmp_" + testName + ".pdf";
@@ -1012,6 +1095,35 @@ private <T extends IBlockElement> void executeTest(String testName, T container,
10121095
Assert.assertNull(compareTool.compareByContent(filename, cmpName, DESTINATION_FOLDER, "diff_"));
10131096
}
10141097

1098+
private static Paragraph createDummyParagraph() {
1099+
return new Paragraph("Lorem ipsum dolor sit amet, consectetur adipiscing elit, " +
1100+
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " +
1101+
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute " +
1102+
"irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " +
1103+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim " +
1104+
"id est laborum.");
1105+
}
1106+
1107+
private static String generateLongString(int amountOfWords) {
1108+
StringBuilder sb = new StringBuilder();
1109+
int random = 1;
1110+
for (int i = 0; i < amountOfWords; i++) {
1111+
random = getPseudoRandomInt(i + random);
1112+
for (int j = 1; j <= random; j++) {
1113+
sb.append('a');
1114+
}
1115+
sb.append(' ');
1116+
}
1117+
return sb.toString();
1118+
}
1119+
1120+
private static int getPseudoRandomInt(int prev) {
1121+
final int first = 93840;
1122+
final int second = 1929;
1123+
final int max = 7;
1124+
return (prev * first + second) % max;
1125+
}
1126+
10151127
private static Div createFirstPageFiller() {
10161128
Div firstPageFiller = new Div();
10171129
firstPageFiller.setProperty(Property.MARGIN_TOP, UnitValue.createPointValue(50));

styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/CommonCssConstants.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,8 +892,16 @@ public class CommonCssConstants {
892892
*/
893893
public static final String COLUMN_REVERSE = "column-reverse";
894894

895+
/**
896+
* The Constant COLUMN_COUNT.
897+
*/
895898
public static final String COLUMN_COUNT = "column-count";
896899

900+
/**
901+
* The Constant COLUMN_WIDTH.
902+
*/
903+
public static final String COLUMN_WIDTH = "column-width";
904+
897905
/**
898906
* The Constant CONTAIN.
899907
*/

styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/validate/impl/CssDefaultValidator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ public CssDefaultValidator() {
111111
defaultValidators.put(CommonCssConstants.COLUMN_GAP, new MultiTypeDeclarationValidator(
112112
new CssLengthValueValidator(false), new CssPercentageValueValidator(false), normalValidator,
113113
inheritInitialUnsetValidator));
114+
defaultValidators.put(CommonCssConstants.COLUMN_WIDTH, new MultiTypeDeclarationValidator(
115+
new CssLengthValueValidator(false), new CssPercentageValueValidator(false),
116+
new CssEnumValidator(CommonCssConstants.AUTO), inheritInitialUnsetValidator));
117+
defaultValidators.put(CommonCssConstants.COLUMN_COUNT, new MultiTypeDeclarationValidator(
118+
new CssNumberValueValidator(false), new CssEnumValidator(CommonCssConstants.AUTO),
119+
inheritInitialUnsetValidator));
114120
defaultValidators.put(CommonCssConstants.ROW_GAP, new MultiTypeDeclarationValidator(
115121
new CssLengthValueValidator(false), new CssPercentageValueValidator(false), normalValidator,
116122
inheritInitialUnsetValidator));

0 commit comments

Comments
 (0)