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.
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.13 | v1.0.17 |
|---|---|---|
| Coluna de span 12, alinhada à direita | borda direita em x = 424.6 pt: 114 pt antes da margem | borda direita em x = 538.6 pt (a margem) nas 9 páginas |
| Coluna de span 3, alinhada à direita | Page 1 of numa linha, 9 quebrado para a seguinte | Page 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
- Números de página, cabeçalhos e rodapés que simplesmente funcionam — o modelo completo de cabeçalho/rodapé e onde entra o resolver de duas passadas
- Como faço uma tabela se estender por várias páginas? — de onde vêm as páginas extras deste exemplo
- Como alinhar uma coluna à borda direita de um Row no gpdf? — quando você prefere o número numa coluna própria à direita
- Guia de uso básico — todas as opções de
template.New
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