MIT Free for personal and commercial use. No licensing fees for private or commercial .NET apps. License

Layout

TerraPDF provides three layout elements: Column, Row, and Table. They are all accessed through extension methods on IContainer.

For a visual explanation of how Column and Row relate to each other see Row & Column Layout.


Column

Column stacks its children vertically (top → bottom).

C#
container.Column(col =>
{
    col.Spacing(8);          // vertical gap between items in points

    col.Item().Text("First paragraph");
    col.Item().Text("Second paragraph");
    col.Item().Background(Color.Grey.Lighten4).Padding(10).Text("Highlighted box");
});

Column alignment

Each Item() can be aligned independently by calling an AlignItems* method before the Item() call it should affect:

C#
col.AlignItemsLeft();           // default
col.Item().Text("Left");

col.AlignItemsCenter();
col.Item().Text("Centred");

col.AlignItemsRight();
col.Item().Text("Right");

ColumnDescriptor API

Method Description
Spacing(double) Vertical gap between items in points
AlignItemsLeft() Left-align subsequent items (default)
AlignItemsCenter() Centre-align subsequent items
AlignItemsRight() Right-align subsequent items
Item() Adds an item slot; returns IContainer
PageBreak() Inserts an explicit page break at this position

Row

Row arranges its children horizontally (left → right).

C#
container.Row(row =>
{
    row.Spacing(6);   // horizontal gap between items in points

    row.RelativeItem(2).Text("Takes 2x the share");
    row.RelativeItem(1).Text("Takes 1x the share");
    row.AutoItem().Text("Natural content width");
    row.ConstantItem(80).Text("Always 80 pt wide");
});

Item sizing

Method Width
RelativeItem(weight = 1) Proportional share of remaining space after constant/auto items
AutoItem() Measured natural content width
ConstantItem(points) Fixed number of PDF points

Width calculation order:

  1. Subtract spacing from available width.
  2. Measure all ConstantItem widths and AutoItem widths (content measurement pass).
  3. Distribute remaining space among RelativeItem slots proportionally by weight.

RowDescriptor API

Method Returns Description
Spacing(double) RowDescriptor Horizontal gap in points
RelativeItem(double weight = 1) IContainer Proportional slot
AutoItem() IContainer Auto-sized slot
ConstantItem(double pts) IContainer Fixed-width slot

Table

Table renders a grid with fixed or proportional columns. Header rows are automatically repeated on every continuation page when the table spans multiple pages.

C#
container.Table(table =>
{
    // 1. Define columns
    table.ColumnsDefinition(cols =>
    {
        cols.RelativeColumn(4);    // proportional
        cols.RelativeColumn(1);
        cols.ConstantColumn(70);   // fixed width in points
    });

    // 2. Header row (repeated on continuation pages)
    table.HeaderRow(row =>
    {
        row.Cell().Background("#1a4a8a").Padding(6)
           .Text("Description").Bold().FontColor(Color.White);
        row.Cell().Background("#1a4a8a").Padding(6).AlignCenter()
           .Text("Qty").Bold().FontColor(Color.White);
        row.Cell().Background("#1a4a8a").Padding(6).AlignRight()
           .Text("Price").Bold().FontColor(Color.White);
    });

    // 3. Data rows
    bool shade = false;
    foreach (var item in lineItems)
    {
        string bg = shade ? Color.Grey.Lighten4 : Color.White;
        table.Row(row =>
        {
            row.Cell().Background(bg).Padding(6).Text(item.Name);
            row.Cell().Background(bg).Padding(6).AlignCenter().Text(item.Qty.ToString());
            row.Cell().Background(bg).Padding(6).AlignRight().Text($"{item.Price:N2}");
        });
        shade = !shade;
    }
});

Column definitions

Method Description
RelativeColumn(weight = 1) Proportional share of available width
ConstantColumn(points) Fixed width in PDF points

TableDescriptor API

Method Description
ColumnsDefinition(Action<ColumnsDefinitionDescriptor>) Define all columns
HeaderRow(Action<TableRowDescriptor>) Add a header row (repeats on new pages)
Row(Action<TableRowDescriptor>) Add a data row

TableRowDescriptor API

Method Returns Description
Cell(columnSpan = 1, rowSpan = 1) IContainer Next free cell slot (left to right); supports all decorators

Tip: Cells support the full decorator chain: row.Cell().Background(bg).Padding(6).AlignRight().Text("value")

Spanning columns and rows

Cell takes an optional columnSpan and rowSpan. A cell is placed in the first column not already covered by an earlier cell, so a span moves the cursor past everything it covers — you write one Cell call per cell the row actually contains, with no placeholder calls for the columns a span already fills.

C#
container.Table(table =>
{
    table.ColumnsDefinition(cols =>
    {
        cols.RelativeColumn();
        cols.RelativeColumn();
        cols.RelativeColumn();
    });

    table.Row(row =>
    {
        row.Cell(columnSpan: 2).Background("#eeeeee").Padding(6).Text("Spans columns 1-2");
        row.Cell().Padding(6).Text("Column 3");          // lands in column 3, not column 2
    });

    table.Row(row =>
    {
        row.Cell(rowSpan: 2).Padding(6).Text("Spans rows 2-3");
        row.Cell().Padding(6).Text("Row 2, column 2");
        row.Cell().Padding(6).Text("Row 2, column 3");
    });

    table.Row(row =>
    {
        row.Cell().Padding(6).Text("Row 3, column 2");   // column 1 is still held by the span
        row.Cell().Padding(6).Text("Row 3, column 3");
    });
});

A spanned cell is measured like any other: when its content is taller than the rows it covers, those rows grow to hold it. Spans below 1 are treated as 1.

Pagination

A table splits between rows when it is taller than the page:

  • Header rows repeat at the top of every continuation page. Declare them with HeaderRow before any Row call.
  • A table with no header row still splits — it simply has nothing to repeat.
  • A table works wherever you put it, whether it is an item inside a Column or placed straight into page.Content().
  • A row span is never divided by a page break. The rows a span covers travel together, so the split falls above or below the span, never through it. A group of spanned rows taller than a whole page is emitted anyway rather than looping.

Note: Keep header rows self-contained. A row span that starts in a header row and reaches into data rows renders correctly on the first page, but is truncated to the header rows on every continuation page — the header block repeats, while the data rows it also covers stay behind on the previous page. Row spans that start in a data row are unaffected.

See the Table Spans & Pagination sample for a worked, multi-page example of everything above.


Header on First Page Only

By default, the page header appears on every page. Use HeaderOnFirstPageOnly() to show the header only on the first page, freeing vertical space for content on continuation pages:

C#
page.Header().Text("Confidential").Bold();
page.HeaderOnFirstPageOnly();  // header only on page 1

This is useful for cover pages or documents where the header should not repeat.


Nesting Layouts

Column, Row, and Table nest freely inside each other.

Two-column page layout

C#
container.Row(row =>
{
    row.RelativeItem().Column(left =>
    {
        left.Spacing(6);
        left.Item().Text("Left heading").Bold();
        left.Item().Text("Left body text.");
    });

    row.ConstantItem(1).Background(Color.Grey.Lighten2);  // divider

    row.RelativeItem().PaddingLeft(12).Column(right =>
    {
        right.Spacing(6);
        right.Item().Text("Right heading").Bold();
        right.Item().Text("Right body text.");
    });
});
C#
container.Column(page =>
{
    page.Spacing(10);

    // Header row
    page.Item().Row(header =>
    {
        header.RelativeItem().Text("Logo").Bold();
        header.AutoItem().Text("Page 1").FontColor(Color.Grey.Medium);
    });

    // Body
    page.Item().Text("Main content goes here.");

    // Footer row
    page.Item().Row(footer =>
    {
        footer.RelativeItem().Text("Company Name");
        footer.AutoItem().Text("Confidential").Italic();
    });
});