如何在 gpdf 页脚显示页码「第 X 页 共 Y 页」?
把 PageNumberPlaceholder 和 TotalPagesPlaceholder 放进一个 c.Text,v1.0.14+ 自动替换并保持对齐。
换个说法提问
你希望页脚显示 Page 3 of 12——一行、一个字符串,和页脚里其他内容一样右对齐。gpdf 有 c.PageNumber() 和 c.TotalPages(),但它们各自是独立的元素。怎样把文字和两个数字放进同一行,而不是拼四个列?
TL;DR
这两个占位符是导出的字符串常量。直接把它们嵌进普通字符串中间:
c.Text("Page "+document.PageNumberPlaceholder+" of "+document.TotalPagesPlaceholder,
template.AlignRight())
gpdf 会在分页之后、在每一页上替换它们。在 gpdf v1.0.14 及以上版本,替换后会重新测量这一行,所以页码从 9 变成 10 时右对齐也不会乱。
代码
一个 400 行的表格溢出到 9 页 A4,加上页脚。已在 gpdf v1.0.17 上验证。
package main
import (
"fmt"
"log"
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
pageOf := "Page " + document.PageNumberPlaceholder +
" of " + document.TotalPagesPlaceholder
doc := template.New(
template.WithPageSize(document.A4),
template.WithMargins(document.UniformEdges(document.Mm(20))),
)
doc.Footer(func(p *template.PageBuilder) {
p.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(pageOf,
template.AlignRight(),
template.FontSize(9),
template.TextColor(pdf.Gray(0.4)),
)
})
})
})
rows := make([][]string, 0, 400)
for i := 1; i <= 400; i++ {
rows = append(rows, []string{fmt.Sprint(i), fmt.Sprintf("Line item %03d", i)})
}
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Table([]string{"#", "Description"}, rows)
})
})
out, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("page-x-of-y.pdf", out, 0o644); err != nil {
log.Fatal(err)
}
}
pdftotext -layout page-x-of-y.pdf - 会逐页输出 Page 1 of 9 到 Page 9 of 9。不需要任何额外配置。
为什么这样可行
c.PageNumber() 并没有什么魔法。它的全部实现就是“追加一个内容为 document.PageNumberPlaceholder 的 document.Text”。c.TotalPages() 也一样,只是换了另一个常量。占位符是用 NUL 字节包裹的哨兵字符串("\x00GPDF_PAGE\x00" 和 "\x00GPDF_TOTAL\x00"),这样选是为了不和任何真实文本冲突。
替换发生在分页器确定总页数之后。它遍历每一页上已放置的所有文本节点,对两个占位符分别执行 strings.ReplaceAll。节点里是只有一个占位符,还是一整句话中间夹着一个,它并不在意。所以 "Page " + PageNumberPlaceholder + " of " + TotalPagesPlaceholder 和专用方法得到的是同样的处理。
页眉和页脚节点只布局一次,在各物理页之间共享,所以解析器处理的是每页的副本。第 3 页的页脚得到 3,第 4 页得到 4,而你的 *template.Document 不会被修改,第二次渲染时占位符依然在。
最后这一点容易被忽略。同样的做法在正文里也能用,适合写在封面上:
c.Text(fmt.Sprintf("This report has %s pages.", document.TotalPagesPlaceholder))
在同一份 9 页文档中,第 1 页会输出 This report has 9 pages.。
为什么我们自己之前的文章让你用四列
5 月的 在 Go PDF 中正确实现页码、页眉和页脚 把单行“Page X of Y”列为一个不够顺手的地方,并给出了四列的变通写法。对当时的 gpdf 来说那个建议是对的,原因在于测量顺序。
布局引擎在总页数确定之前就要完成文本的换行和对齐。到 v1.0.13 为止,它是带着原始哨兵字符串来做这件事的。\x00GPDF_TOTAL\x00 有 11 个字节,所以引擎认为 Page 1 of 9 比实际宽得多。我用 v1.0.13 重新跑了上面的程序,看看偏差有多大:
| 页脚布局 | v1.0.13 | v1.0.17 |
|---|---|---|
| 12 列宽的列,右对齐 | 右边缘在 x = 424.6 pt——距页边距差 114 pt | 9 页的右边缘都在 x = 538.6 pt(页边距处) |
| 3 列宽的列,右对齐 | Page 1 of 在一行,9 被折到下一行 | Page 1 of 9 在同一行 |
| 两位数页码(32 页) | — | 从 Page 9 of 32 到 Page 10 of 32,右边缘始终在 538.6 pt |
114 点就是 40 mm。在右对齐的页脚上,这看起来不像字距的小问题,更像有人按了一下 Tab。在更窄的列里,修复它的那个提交描述了更糟的情况:换行器把哨兵字符串本身切开,?GP 和 DF 这样的碎片被原样印了出来。
v1.0.14(提交 b58626a)改了两处。布局阶段,每个占位符按一个数字("0")来测量和换行,之后再在每一行的内容里恢复占位符。然后 ResolvePageNumbersWithFonts 用真实字体重新测量每一行替换后的文本,并把居中和右对齐的行移回锚点。正是第二步保证了 Page 10 of 32 虽然多一位数字,结尾的 x 坐标仍和 Page 9 of 32 相同。
如果你只能停留在 v1.0.13 或更早版本,之前文章里的多列布局依然可用,因为每个占位符单独占一列。否则就升级:
go get github.com/gpdf-dev/gpdf@latest
变体
斜杠格式。 document.PageNumberPlaceholder + " / " + document.TotalPagesPlaceholder 得到 3 / 12。
fmt.Sprintf。 占位符就是普通字符串,所以用 %s。别用 %d——这时你还没有数字,这正是占位符存在的意义。
fmt.Sprintf("Page %s of %s", document.PageNumberPlaceholder, document.TotalPagesPlaceholder)
中文或其他 CJK。 注册一个覆盖所需字符的字体,并设置到这段文本上。像 "第 " + PageNumberPlaceholder + " 页,共 " + TotalPagesPlaceholder + " 页" 这样的格式,替换方式完全相同。字体子集里只会包含实际用到的字形。
JSON 与 Go 模板。 仍然使用分开的 {"type": "pageNumber"} 和 {"type": "totalPages"} 元素。在 v1.0.17 上,往 JSON 字符串里写 \u0000GPDF_PAGE\u0000 确实能用。但常量的名字才是 API,它的值是实现细节。我不会把生产环境的模板押在这个值不变上。
目前还做不到的是按页分支。页脚闭包不知道自己正在渲染第 1 页,所以“封面不显示页码”需要换一种做法。那是另一篇食谱的内容。
相关食谱
- 在 Go PDF 中正确实现页码、页眉和页脚——完整的页眉/页脚模型,以及两遍解析器在其中的位置
- 如何让表格跨多页输出?——本例中溢出的页面从何而来
- 如何把 gpdf 的 Col 对齐到行的右边缘?——想把页码放进单独的右侧列时
- 基本用法指南——
template.New的全部选项
试试 gpdf
gpdf 是一个用于生成 PDF 的 Go 库。MIT 许可,零外部依赖,原生支持 CJK。
go get github.com/gpdf-dev/gpdf