A table is a normal flow block. Define its columns, add a header and rows, and
place it directly in pageFlow or inside a section/module.
document.pageFlow(page -> page
.addTable(table -> table
.autoColumns(3)
.headerRow("Item", "Qty", "Amount")
.row("API support", "2", "£240")
.row("Hosting", "1", "£80")
.totalRow("Total", "", "£320")));Start there. Add style or pagination behaviour only when the document needs it.
| You want to… | Add |
|---|---|
| Control fixed or automatic column widths | .columns(DocumentTableColumn...) |
| Style every cell | .defaultCellStyle(...) |
| Add several lines to one cell | DocumentTableCell.lines(String...) |
| Put any document node in a cell | DocumentTableCell.node(DocumentNode) |
| Merge cells across columns or rows | DocumentTableCell.text(...).colSpan(...) / .rowSpan(...) |
| Colour alternating rows | .zebra(odd, even) |
| Give the total a branded style | .totalRow(style, values...) |
| Repeat headings after a page break | .repeatHeader() |
The rest of this page builds on that basic table. The features compose: one table can contain structured cells, spans, zebra rows, a branded total, and a header repeated on every continuation page.
Three factories, three shapes. Pick by what the cell holds, not by what the text looks like:
// One line. The argument is the line — a "\n" inside it is not a
// line break, it is just whitespace between two words.
DocumentTableCell.text("Design review");
// Several lines. One argument per line.
DocumentTableCell.lines("Design review", "2 h 30 m");
// Any registered node: paragraph, list, section, container, row,
// layer stack, or a nested table.
DocumentTableCell.node(new ParagraphNode(
"Note", "**Blocked** — waiting on legal",
DocumentTextStyle.DEFAULT, TextAlign.LEFT, 0.0,
DocumentInsets.zero(), DocumentInsets.zero()));text("Design review\n2 h 30 m") renders as the single line
Design review 2 h 30 m. That is the plain-text cell's contract: a
cell's line list is what the author passed, and text(...) passes
exactly one. Use lines(...) when the break is part of the content,
or node(...) when the cell needs styling, markdown, or structure
around it.
Paragraph content is a different story: ParagraphNode does treat
\n as a hard line break, inside a cell exactly as it does anywhere
else on the page. So a composed cell built from a paragraph whose text
contains \n breaks where the author wrote it.
A composed cell reserves the child's measured height and renders the
child's whole sub-tree, so a SectionNode of three paragraphs is three
paragraphs in the cell, not just the section's background. The row stays
atomic: a composed cell does not split across a page break, so keep the
child shorter than one page's content area.
Spanning cells declare how many rows they cover via rowSpan(int).
The layout layer skips occupied grid positions when interpreting
subsequent source rows, so authors only specify the cells that are
not yet covered by a prior spanning cell.
import com.demcha.compose.document.table.DocumentTableCell;
import com.demcha.compose.document.table.DocumentTableColumn;
addTable(table -> table
.columns(
DocumentTableColumn.auto(),
DocumentTableColumn.auto(),
DocumentTableColumn.auto())
.defaultCellStyle(bordered)
// Row 0: Tall middle cell spans BOTH rows below it.
.rowCells(
DocumentTableCell.text("A0"),
DocumentTableCell.lines("Tall middle", "(spans 3 rows)").rowSpan(3),
DocumentTableCell.text("C0"))
// Row 1: only A1 + C1 — middle is occupied by the spanning cell.
.rowCells(
DocumentTableCell.text("A1"),
DocumentTableCell.text("C1"))
.rowCells(
DocumentTableCell.text("A2"),
DocumentTableCell.text("C2")));Row span composes with colSpan — a single cell can be both
colSpan(2).rowSpan(3) to merge a 2x3 block. The layout layer raises
a precise diagnostic if a span overlaps another cell, exceeds the
remaining rows / columns, or leaves a gap.
zebra(odd, even) paints odd-indexed rows (0, 2, 4 — first, third,
fifth visually) in one fill and even-indexed rows (1, 3, 5) in
another. Either argument may be null to skip painting that parity.
import com.demcha.compose.document.style.DocumentColor;
DocumentColor zebraOdd = DocumentColor.rgb(244, 247, 252);
DocumentColor zebraEven = DocumentColor.WHITE;
addTable(table -> table
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(bordered)
.headerRow("Item", "Qty", "Amount")
.row("Apples", "12", "$24.00")
.row("Pears", "6", "$18.00")
.row("Strawberries", "20", "$40.00")
.row("Mangoes", "4", "$16.00")
.zebra(zebraOdd, zebraEven));A two-arg overload accepts full DocumentTableStyle values when the
zebra row needs more than just a fill colour:
.zebra(
DocumentTableStyle.builder().fillColor(zebraOdd).padding(DocumentInsets.of(8)).build(),
DocumentTableStyle.builder().fillColor(zebraEven).padding(DocumentInsets.of(8)).build())Zebra is applied lazily at build() time and only to rows that don't
already have an explicit rowStyle(idx, ...) override, so
headerStyle(...) and totalRow(...) always win.
totalRow(values...) appends the row at the end of the table and
assigns a default totals style (bold text + a subtle gray-blue fill).
A two-arg overload takes a custom DocumentTableStyle for branded
totals rows.
DocumentTableStyle goldenTotal = DocumentTableStyle.builder()
.fillColor(DocumentColor.rgb(232, 220, 180))
.stroke(DocumentStroke.of(rule, 0.6))
.padding(DocumentInsets.of(7))
.textStyle(DocumentTextStyle.builder()
.decoration(DocumentTextDecoration.BOLD)
.color(ink)
.build())
.build();
addTable(table -> table
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(bordered)
.headerRow("Item", "Qty", "Amount")
.row("Apples", "12", "$24.00")
.row("Pears", "6", "$18.00")
.row("Mangoes", "4", "$16.00")
.totalRow(goldenTotal, "Total", "22", "$58.00")
.zebra(zebraOdd, zebraEven));The totals row is registered with an explicit rowStyle entry at the
last row index, so it wins over zebra alternation on that row
regardless of parity.
repeatHeader() repeats the first row at the top of every
continuation page when the table is split across pages.
repeatHeader(int) repeats N leading rows — useful when you have
both a title row AND a column-header row that should both repeat.
addTable(table -> {
TableBuilder t = table
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(bordered)
.headerRow("Item", "Amount")
.headerStyle(headerStyle)
.repeatHeader(); // ← header re-emits on every page
for (int i = 1; i <= 60; i++) {
t.row("Row " + i, String.format("$%d.00", i));
}
});repeatHeader(0) is the default and behaves exactly like a v1.4
table — the second page starts directly with whatever data row the
split landed on. Authors opting into the feature only pay for it when
they explicitly call the method.
The runnable example
examples/.../TableAdvancedExample.java
combines the advanced table features on one PDF: a 3-column invoice with a
row-spanning side note, zebra body rows, a totals row, and a
repeating "Item / Qty / Amount" header on every continuation page.
The output lands at examples/target/generated-pdfs/table-advanced.pdf
on every full example sweep.
The table regression suite pins five invariants:
- Row span placement —
TableBuilderRowSpanTestverifies the spanning cell's height equals the SUM of its covered row heights and that subsequent rows correctly skip occupied columns. - Spanning cell render direction —
TableResolvedCell.yOffsetis negative for spanning cells, so they extend visually downward through the rows they merge instead of upward beyond the starting row. - Zebra precedence —
TableBuilderZebraAndTotalsTestverifies that explicit row styles (header, totals, manualrowStyle(idx)) always win over zebra alternation. - Header repeats on every continuation page —
TableBuilderRepeatHeaderTestwalks all pages of a 60-row table and asserts every page's first row is the configured header. - Default repeat-header = 0 — tables without a
repeatHeader()call paginate exactly as they did in v1.4, so existing layouts stay byte-stable.
TableBuilder— full builder API.DocumentTableCell— cell payload withcolSpan/rowSpanmutators.DocumentTableStyle— style overrides for fill, stroke, text style, padding.- Runnable example:
examples/src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java.