全部文章

gpdf 的 12 列网格是怎么工作的?

gpdf 的 12 列网格用 r.Col(span, fn) 接收 1–12 的整数。列宽为 span/12 的比例,没有断点、没有槽宽间距,为 PDF 固定宽度设计。

作者: gpdf team

换个说法的问题

你用过 gpdf 的 API——页面构建器、行构建器、列构建器——列的构造函数接收一个数字:r.Col(4, fn)r.Col(8, fn)。这个数字是什么?总和不到 12 会怎样?和你已经熟悉的 CSS 网格有什么区别?

短答

r.Col(span, fn) 接收 1 到 12 的整数。这个整数代表此列在行里的占比——span / 12 的可用宽度。小于 1 会被夹到 1,大于 12 会被夹到 12,每行总和是否为 12 由你决定,库本身不强制。网格被固定为 12 个分格,剩下的就是你如何切行的问题。

完整示例

package main

import (
    "log"
    "os"

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

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

    page := doc.AddPage()

    // 整行
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("发票 #2026-0416", template.FontSize(18), template.Bold())
        })
    })

    // 两列表头 (6 + 6)
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(6, func(c *template.ColBuilder) {
            c.Text("收票方")
            c.Text("Acme 有限公司")
        })
        r.Col(6, func(c *template.ColBuilder) {
            c.Text("开票日期")
            c.Text("2026-04-16")
        })
    })

    // 三列摘要 (4 + 4 + 4)
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("小计")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("税额")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("合计")
        })
    })

    // 非对称 (8 + 4) — 正文 + 侧栏
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("明细在这里列出")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("备注")
        })
    })

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

运行 go run main.go 即得一页 PDF,四行各用不同方式切分。

为什么是 12

12 可以干净地被 2、3、4、6 整除。一半 (6+6)、三等分 (4+4+4)、四等分 (3+3+3+3)、带侧栏 (3+9 或 4+8)、正文加边栏 (8+4)——真实世界的版式几乎全都落在这些组合里。如果选因子少的数字,这些组合里会有一种直接失效。Bootstrap 在 2011 年就定下了 12 列,到今天"12 列网格"已经是设计师和前端工程师共享的行话。gpdf 特意沿用这个习语——PDF 布局和网页布局在思维模型上并无本质差别,哪怕输出的是固定宽度的纸张。

把数学写清楚

A4 纵向、四边各 15 mm 的话,可用宽度是 180 mm。行内的 Col(4) 占 4/12,也就是 60 mm。Col(8) 占 120 mm。列与列之间默认没有槽宽 (gutter)。如果想要留白,就在较短的列里放一个 c.Spacer,或把一个 Col(1) 留空。

宽度在构建期按百分比计算(相关实现在 gpdf/template/grid.go),布局引擎再用"当前页宽减去边距"换算为实际的点值。也就是说同样的 r.Col(6, fn),在 A4 和 Letter 上物理宽度不同,但占行的比例始终不变

总和不是 12 会怎样

gpdf 并不校验 span 的合计。这是有意的。

  • 合计 < 12:行的右侧留空。想让元素贴在左边、其余留白时用得上。
  • 合计 > 12:最后一列溢出右边距。通常是 bug。PDF 看起来会错位,但不会 crash。

多数布局正好是每行 12,因为那样能填满页面。但你想"在行中间只放一块 6 宽的内容"时,最直接的写法就是 Col(3) 空、Col(6) 内容、Col(3) 空——这个网格本来就是为这种简写设计的。

AutoRow 与 Row 的区别

page.AutoRow(fn) 的高度随最高的列伸展。大多数行都应该用它。

page.Row(height, fn) 固定高度,超出的内容会被裁剪。用在"发票表头必须刚好 30 mm 以便后续装订对齐"之类的场景——视觉一致性优先于内容自由度。

page.Row(document.Mm(30), func(r *template.RowBuilder) {
    r.Col(8, func(c *template.ColBuilder) {
        c.Text("Logo")
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("发票号")
    })
})

网格没做的事

不支持嵌套。ColBuilder 接收内容元素 (Text / Image / Table / List / Spacer),但不能塞一个子行进去。看似需要嵌套的结构,通常用页面层面的两个兄弟行就能更清爽地表达。

没有偏移列。Bootstrap 的 .offset-2 在 gpdf 里不存在。要把内容推到右边,就在左边放一个空的 Col(n)

没有断点。PDF 页面不会自适应。在任何设备上打开都是同一版式——因为输出是定坐标的光栅,而不是会重新流式布局的 DOM。

这些"没有"正是设计的核心。网格少一个特性,读 PDF 结果时就少一类需要推理的歧义。

延伸阅读

试用 gpdf

gpdf 是一个 Go 语言的 PDF 生成库。MIT 许可、零外部依赖、原生 CJK 支持。

go get github.com/gpdf-dev/gpdf

⭐ 在 GitHub 加星 · 查看文档