Recursive AI

Generate a PDF with tables

All onepdf recipes

Generate a multi-page PDF table in C# with OnePdf.Layout: column widths, striped rows, header and footer rows on every page, spans and page numbers.

Terminal
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0
C#
using System.Globalization;
using OnePdf.Layout;
 
var us = CultureInfo.GetCultureInfo("en-US");
var rows = Enumerable.Range(1, 120)
    .Select(i => (Date: new DateOnly(2026, 1, 1).AddDays(i * 2), Crew: $"Crew {(i % 6) + 1}", Kg: 40m + (i * 37 % 90)))
    .ToList();
 
LayoutDocument.Compose(doc =>
{
    doc.Title("Catch log, 2026").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(54);
        page.Content().Column(col =>
        {
            col.Spacing(12);
            col.Item().Heading(1).Text("Catch log, 2026").FontSize(20).Bold();
            col.Item().Table(table =>
            {
                table.Columns(c =>
                {
                    c.Constant(90);       // a fixed width in points
                    c.Relative(2);        // shares the rest of the width by weight
                    c.Relative(1);
                });
                table.CellPadding(4);
                table.CellBorder(0.5, Colors.LightGrey);
                table.RowBackground(i => i % 2 == 1 ? LayoutColor.FromHex("#F2F2F2") : null);   // striped body rows
 
                table.HeaderRow(r =>                                     // repeated at the top of every page
                {
                    r.Cell().Text("Date").Bold();
                    r.Cell().Text("Crew").Bold();
                    r.Cell().AlignEnd().Text("Catch (kg)").Bold();
                });
                foreach (var row in rows)
                {
                    table.Row(r =>
                    {
                        r.Cell().Text(row.Date.ToString("MMM d", us));
                        r.Cell().Text(row.Crew);
                        r.Cell().AlignEnd().Text(row.Kg.ToString("N1", us));
                    });
                }
                table.FooterRow(r =>                                     // repeated at the bottom of every page
                {
                    r.Cell().Span(2).Text("Total, all crews").Bold();     // one cell over two columns
                    r.Cell().AlignEnd().Text(rows.Sum(x => x.Kg).ToString("N1", us)).Bold();
                });
            });
        });
        page.Footer().AlignCenter().Text(t =>
        {
            t.FontSize(9);
            t.Span("Page ");
            t.CurrentPageNumber();
            t.Span(" of ");
            t.TotalPages().ReservedDigits(2);
        });
    });
}).Save("catch-log.pdf");

The 120 rows fill five US Letter pages. Each page starts with the “Date, Crew, Catch (kg)” header row and ends with the total row, above the page number.

Columns

  • Constant(points) keeps a fixed width.
  • Relative(weight) shares the width left over by weight, and never gets narrower than its longest word.
  • Auto() takes the width of its widest content when there is room. An Auto column makes every row count for the width, so prefer Constant and Relative for long tables.

Rows and cells

  • HeaderRow and FooterRow repeat on every page the table reaches and never split. Body rows split between lines of text; r.KeepTogether() moves a row to the next page instead.
  • Span(columns, rows) merges cells. CellBackground, CellBorder, CellPadding and CellAlign set one cell. RowHeader() makes a cell a row header for screen readers.
  • Borders are collapsed: where two cells meet they share one line.
  • A cell holds any content: text, an image, a nested column, another table.

Good to know

  • ReservedDigits sets the room left for the total page count, which isn’t known until the end. Reserve as many digits as the count can have; a centered footer shows the unused room as a gap.
  • For a running subtotal per page (“Carried forward”), use Accumulate on the amount cells and DynamicText in the footer, as shown in Layout.
  • The output is tagged, with TH header cells, so the same document can be saved as PDF/UA: see Create PDF/UA.
  • Save(path) writes page by page, so memory stays small for long tables, and layout time grows in proportion to the number of rows.
  • Text is set in Liberation Sans unless you register other fonts on doc.Fonts.

Every table option and the column-width rules are in Layout.

For a complete multi-page invoice with an ASP.NET Core download, see Create PDF invoices and reports in C#.

Coming from MigraDoc tables: Migrate from PDFsharp and MigraDoc.