All posts

How do I set column widths in a gpdf table?

Pass template.ColumnWidths(...) to c.Table. Values are percentages of the parent column's width. Sum to 100; trailing missing values auto-distribute.

The question, in other words

I have a table with four columns. By default c.Table(header, rows) makes them all the same width, but for an invoice the Description column should be wide and Qty should be narrow. How do I tell gpdf the per-column widths, and what unit am I actually setting?

The quick answer

Pass template.ColumnWidths(...) as a TableOption:

c.Table(header, rows, template.ColumnWidths(40, 15, 20, 25))

The values are percentages of the parent column's content width, not points. They don't have to sum to 100, but they usually should — anything missing leaves empty space on the right, anything extra overflows the cell.

That's it for the common case. The interesting parts are what happens when the percentages don't sum to 100, when you pass fewer values than there are columns, and what the parent width actually is.

Working code (a four-column invoice table)

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/pdf"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    header := []string{"Description", "Qty", "Unit price", "Total"}
    rows := [][]string{
        {"Annual support contract", "1", "$1,200.00", "$1,200.00"},
        {"On-site training (per day)", "3", "$800.00", "$2,400.00"},
        {"Custom template development", "12", "$95.00", "$1,140.00"},
    }

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Table(header, rows,
                template.ColumnWidths(40, 15, 20, 25),
                template.TableHeaderStyle(template.BgColor(pdf.Gray(0.92))),
            )
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("invoice.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

r.Col(12, ...) claims the full row width. The table sits inside that column, so 100% of the table = 100% of the grid Col content area. With ColumnWidths(40, 15, 20, 25) summing to 100, every PDF point of horizontal space is used.

What the percentages are a percentage of

The number you pass is forwarded to document.Pct(w) and resolved against the table's content width. That's the width of the grid Col the table lives in, minus any margin, padding, and border on the table itself (in practice, just the Col width — table styling defaults to none).

So with r.Col(6, ...) (half the row) and ColumnWidths(50, 50), each table column is 25% of the row width, not 50%. The percentages are local to the table, not to the page.

This matters if you ever swap a table from a full-width row into a side-by-side layout. The ColumnWidths call doesn't need to change — it scales.

What gpdf does when the math doesn't work out

Two cases come up constantly. Both are handled by the layout engine in a specific way that's worth knowing.

Case 1: percentages don't sum to 100. Each value is taken at face value. ColumnWidths(40, 30, 20) on a three-column table produces columns at 40%, 30%, and 20% of the parent — leaving 10% empty on the right. ColumnWidths(50, 50, 50) overflows; the third column extends past the parent edge and may bleed into the next page column or off the page.

There is no normalization step. gpdf trusts you to do the arithmetic.

Case 2: fewer widths than there are columns. This is the more interesting one. The trailing columns become auto-width and share the remainder equally:

// Three-column table, only two widths given.
c.Table(header3, rows3, template.ColumnWidths(40, 30))
// → 40% / 30% / 30%   (the third column gets 100 - 40 - 30 = 30%)

If the explicit widths already sum to 100 or more, the auto columns get zero width and effectively disappear. If they sum to less than 100, the leftover is divided equally among the auto columns:

// Five-column table, two widths given.
c.Table(header5, rows5, template.ColumnWidths(50, 10))
// → 50% / 10% / 13.33% / 13.33% / 13.33%   (40% split three ways)

There's a useful trick hiding in this rule: passing 0 for a column also makes it auto. So ColumnWidths(0, 30, 30) on a three-column table fixes the last two at 30% each and gives the first column the remaining 40%. This is how you say "I care about these specific widths; let gpdf handle the rest."

The other direction: too many widths

Extra values beyond the column count are silently ignored. ColumnWidths(40, 30, 20, 10) on a two-column table just uses the first two. This is forgiving but it's also a bug magnet — if you delete a column from your header but forget to drop the matching width, gpdf won't tell you. There's no warning log.

The column count itself comes from the header row's length (or the first body row, if there's no header). Add a header cell and you've added a column; gpdf will discover this and re-balance whatever ColumnWidths you passed.

When percentages aren't what you want

The builder API only exposes percentages. If you genuinely need a fixed-width column — say, a 50pt "Qty" column that doesn't scale with the page — you have to drop one layer down to the document tree:

import "github.com/gpdf-dev/gpdf/document"

tbl := &document.Table{
    Columns: []document.TableColumn{
        {Width: document.Auto},
        {Width: document.Pt(50)},
        {Width: document.Pt(80)},
        {Width: document.Pt(80)},
    },
    Header: /* ... */,
    Body:   /* ... */,
}

Mix and match: Auto, Pt, Mm, Pct all work at the document layer. The first column with Auto gets whatever's left after the three fixed columns are subtracted. This is closer to the CSS <col> element than to a percentage system.

You give up the convenience of c.Table(header, rows, ...) doing the cell construction for you, but for invoices that need to print on physical letterhead with stable column positions, the trade is worth it.

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, native CJK support.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs