Todas as publicações

Como colocar número de página X de Y no rodapé do gpdf?

Coloque document.PageNumberPlaceholder e TotalPagesPlaceholder em um único c.Text. O gpdf v1.0.14+ substitui os dois e mantém a linha alinhada.

Publicado em 12 de novembro de 2026
por Taiki Noda

A pergunta, em outras palavras

Você quer que o rodapé mostre Page 3 of 12: uma linha, uma string, alinhada à direita como o resto do rodapé. O gpdf tem c.PageNumber() e c.TotalPages(), mas cada um é um elemento separado. Como colocar as palavras e os dois números em uma só linha sem colar quatro colunas?

TL;DR

Os dois placeholders são constantes string exportadas. Coloque-os no meio de uma string comum:

c.Text("Page "+document.PageNumberPlaceholder+" of "+document.TotalPagesPlaceholder,
	template.AlignRight())

O gpdf faz a substituição depois da paginação, em todas as páginas. No gpdf v1.0.14 ou posterior a linha é medida de novo após a troca, então o alinhamento à direita se mantém quando o número da página passa de 9 para 10.

O código

Um rodapé sobre uma tabela de 400 linhas que transborda para nove páginas A4. Testado com o 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 - imprime de Page 1 of 9 a Page 9 of 9, um por página. Não há mais nada para configurar.

Por que isso funciona

c.PageNumber() não tem mágica nenhuma. A implementação inteira é "adicionar um document.Text cujo conteúdo é document.PageNumberPlaceholder". c.TotalPages() faz o mesmo com a outra constante. Os placeholders são strings sentinela envolvidas em bytes NUL ("\x00GPDF_PAGE\x00" e "\x00GPDF_TOTAL\x00"), escolhidas para que nenhum texto real colida com elas.

A substituição acontece depois que o paginador decidiu quantas páginas existem. Ele percorre cada nó de texto posicionado em cada página e roda strings.ReplaceAll para os dois placeholders. Não importa se o nó contém só um placeholder ou uma frase inteira com um no meio. Por isso "Page " + PageNumberPlaceholder + " of " + TotalPagesPlaceholder recebe o mesmo tratamento que os métodos dedicados.

Os nós de cabeçalho e rodapé são diagramados uma vez e compartilhados entre as páginas físicas, então o resolver trabalha numa cópia por página. O rodapé da página 3 recebe 3, o da página 4 recebe 4, e o seu *template.Document não é alterado; se você renderizar de novo, os placeholders continuam lá.

Esse último ponto passa despercebido com facilidade. O mesmo truque funciona no corpo do documento, o que é útil numa linha de capa:

c.Text(fmt.Sprintf("This report has %s pages.", document.TotalPagesPlaceholder))

Isso imprime This report has 9 pages. na página 1 do mesmo documento de nove páginas.

Por que o nosso próprio post mandava usar quatro colunas

Em maio, Números de página, cabeçalhos e rodapés que simplesmente funcionam chamou o "Página X de Y" numa linha só de ponto fraco e mostrou um contorno com quatro colunas. O conselho estava certo para o gpdf daquela época, e o motivo é a ordem de medição.

O motor de layout quebra e alinha o texto antes de o total de páginas existir. Até a v1.0.13 ele fazia isso com o sentinela bruto dentro da string. \x00GPDF_TOTAL\x00 tem 11 bytes, então o motor achava que Page 1 of 9 era bem mais largo do que de fato era. Rodei o programa acima de novo na v1.0.13 para ver o tamanho do desvio:

Layout do rodapév1.0.13v1.0.17
Coluna de span 12, alinhada à direitaborda direita em x = 424.6 pt: 114 pt antes da margemborda direita em x = 538.6 pt (a margem) nas 9 páginas
Coluna de span 3, alinhada à direitaPage 1 of numa linha, 9 quebrado para a seguintePage 1 of 9 numa linha
Páginas de 2 dígitos (32 páginas)—a borda direita fica em 538.6 pt de Page 9 of 32 a Page 10 of 32

114 pontos são 40 mm. Num rodapé alinhado à direita isso não parece um detalhe de kerning. Parece que alguém apertou Tab. Em colunas mais estreitas, o commit da correção descreve a versão pior: a quebra de linha cortava o próprio sentinela, e fragmentos como ?GP e DF eram impressos literalmente.

A v1.0.14 (commit b58626a) mudou duas coisas. Durante o layout, cada placeholder é medido e quebrado como um único dígito ("0"), e depois o placeholder é restaurado no conteúdo de cada linha. Em seguida, ResolvePageNumbersWithFonts mede de novo cada linha resolvida com a fonte real e devolve as linhas centralizadas e alinhadas à direita ao seu ponto de ancoragem. É esse segundo passo que faz Page 10 of 32 terminar no mesmo x que Page 9 of 32, mesmo tendo um dígito a mais.

Se você está preso na v1.0.13 ou anterior, o layout de várias colunas do post anterior continua funcionando, porque cada placeholder fica sozinho na sua coluna. Caso contrário, atualize:

go get github.com/gpdf-dev/gpdf@latest

Variações

Estilo com barra. document.PageNumberPlaceholder + " / " + document.TotalPagesPlaceholder gera 3 / 12.

fmt.Sprintf. Os placeholders são strings comuns, então %s funciona. Não tente %d: você ainda não tem os números. É justamente essa a ideia.

fmt.Sprintf("Page %s of %s", document.PageNumberPlaceholder, document.TotalPagesPlaceholder)

Japonês ou outro idioma CJK. Registre uma fonte que cubra os caracteres e aplique-a ao texto. Um formato como PageNumberPlaceholder + " / " + TotalPagesPlaceholder + " ページ" é resolvido do mesmo jeito. Só os glifos que você usa de fato entram no subset da fonte.

JSON e Go templates. Fique com elementos separados {"type": "pageNumber"} e {"type": "totalPages"}. Escrever \u0000GPDF_PAGE\u0000 numa string JSON funciona na v1.0.17. Mas a API é o nome da constante; o valor é detalhe de implementação. Eu não apostaria um template de produção na permanência desse valor.

O que ainda não dá para fazer é ramificar conforme a página. A closure do rodapé não sabe se está renderizando a página 1, então "sem número na capa" pede outra abordagem. Isso fica para uma receita própria.

Receitas relacionadas

Experimente o gpdf

gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte nativo a CJK.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Ler a documentação