全部文章

如何把 gpdf 的 Col 对齐到行的右边缘?

需要做两件事:用一个空的填充 Col 把列推到右边缘,再用 template.AlignRight() 让列内文本右对齐。两者是分开的,通常都要。

换个说法

你写了一个只包含一个 r.Col(4, ...) 的行,以为这个列会落在页面右侧。结果它渲染在左边,占了三分之一宽度,右侧留下一大片空白。或者列确实到了右边,但里面的文本还是贴着左边。在 gpdf 里正确的写法是什么?

结论

gpdf 没有 Offset,没有 Push,也没有行级别的 Justify把列推到右边靠的是一个空的填充 Col,让列内文本靠右靠的是 template.AlignRight() 这是两件不同的事,而且几乎总是两个都要。

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(8, func(c *template.ColBuilder) {}) // 填充
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("合计: ¥25,575.00", template.AlignRight())
    })
})

代码

一个完整的程序 —— 也就是大多数人因此来搜索的那种发票抬头结构:

package main

import (
    "log"
    "os"

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

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

    page := doc.AddPage()

    // 左右两块放在一行:8 + 4 = 12。
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("艾克梅有限公司", template.Bold(), template.FontSize(16))
            c.Text("上海市浦东新区世纪大道 1-2-3 号", template.TextColor(pdf.Gray(0.4)))
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("发票 No.1042", template.AlignRight(), template.Bold())
            c.Text("开具日期 2026-09-02", template.AlignRight(),
                template.TextColor(pdf.Gray(0.4)))
        })
    })

    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Spacer(document.Mm(10))
        })
    })

    // 合计区块:8 跨度的空白,然后 4 跨度紧贴边距。
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {})
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("小计:      ¥23,250.00", template.AlignRight())
            c.Text("增值税 10%:  ¥2,325.00", template.AlignRight())
            c.Spacer(document.Mm(2))
            c.Line(template.LineThickness(document.Pt(1)))
            c.Spacer(document.Mm(2))
            c.Text("合计:      ¥25,575.00", template.AlignRight(),
                template.Bold(), template.FontSize(14))
        })
    })

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

为什么必须要填充列

这一段值得理解而不是死记,因为它同时解释了 gpdf 排版中其他几个让人意外的行为。

行是一个水平方向的盒子。RowBuilder.build 按顺序遍历 r.cols,每个列生成一个子盒子,而它给这个子盒子设置的样式只有宽度:

colBox := &document.Box{
    Content: cb.buildNodes(),
    BoxStyle: document.BoxStyle{
        Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
    },
}

gridColumns 是 12。所以 Col(4) 字面意思就是「一个宽度为行宽 33.33% 的盒子」,仅此而已。行从左到右排列子元素,子元素排完就停。它没有「剩余空间」这个概念,因为 document.BoxStyle 根本没有 JustifyContent 字段 —— 在整个仓库里 grep 也找不到。它只有 WidthHeightMinWidthMaxWidthMinHeightMaxHeightMarginPaddingBorderBackgroundDirectionPosition。这就是全部词汇。

所以单独一个 Col(4) 产生的是一个只含 33.33% 宽盒子的行,位于水平流的起点。剩下的 66.67% 不是「你的列右边的空白」,而是根本什么都没有。那里没有元素可以顶住。

填充用的 Col(8) 给了布局引擎一个占据这段空间的东西。代价是树里多一个空盒子,渲染出来什么都没有。

顺带一提,Col(8) 并不特殊。Col(6) + Col(3) + Col(3) 同样能把最后一块推到右边,而且以后往填充列里塞内容也不用重构结构。

陷阱:列在右,文本在左

最浪费时间的错误是这个:

r.Col(8, func(c *template.ColBuilder) {})
r.Col(4, func(c *template.ColBuilder) {
    c.Text("合计: ¥25,575.00") // ← 少了 AlignRight
})

列在你想要的位置。文本不在。它从列的左边界开始 —— 也就是页面 66.67% 的位置 —— 所以看上去有点像右对齐。如果你的数字位数恰好都一样,可能一直没发现,直到三位数的合计和五位数的排在一起,小数点对不齐了才注意到。

template.AlignRight() 是一个 TextOption(设置 TextAlignfunc(*document.Style)),所以它加在单个 c.Text 调用上。没有办法给整个列设置一次。块里每一行都要写。

表格、图片、页码

文本选项进不到其他元素内部。每种元素都有自己的对齐入口:

// Table:按位置对应 header 切片的逐列对齐。
c.Table(header, rows, template.ColumnAlign(
    document.AlignLeft, document.AlignRight, document.AlignRight,
))

// Image:在所属列内的对齐。
c.Image(logoBytes, template.WithAlign(document.AlignRight))

// 页码接受 TextOption,所以 AlignRight 直接可用。
c.PageNumber(template.AlignRight())

ColumnAlign 接受的是 document.TextAlign 值,不是 TextOption —— 是 document.AlignRight,不是 template.AlignRight()。容易搞混,不过编译器会立刻告诉你。

当行帮不上忙时

对于要固定在某个坐标、和页面上其他内容无关的东西 —— 已付印章、斜向水印、角标 —— 就离开网格:

page.Absolute(document.Mm(140), document.Mm(60),
    func(c *template.ColBuilder) {
        c.Text("已付款", template.AlignRight(), template.Bold(),
            template.FontSize(28), template.TextColor(pdf.RGBHex(0xC62828)))
    },
    template.AbsoluteWidth(document.Mm(50)),
)

这是逃生口,不是惯用写法。如果你发现自己在为普通内容计算 x 坐标,答案其实是填充列。

最后还有一点:Col 会把单个跨度限制在 1〜12,但没有人检查总和。一行里写三个 Col(5),宽度百分比加起来是 125%,第三列不会换行,而是溢出右边距。gpdf 不会警告你。跨度请自己数。

相关做法

试试 gpdf

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

go get github.com/gpdf-dev/gpdf

⭐ 在 GitHub 上 Star · 阅读文档