¿Cómo poner número de página X de Y en el pie de gpdf?
Pon document.PageNumberPlaceholder y TotalPagesPlaceholder en un solo c.Text. gpdf v1.0.14+ sustituye ambos y mantiene la línea alineada.
La pregunta, en otras palabras
Quieres que el pie diga Page 3 of 12: una línea, una cadena, alineada a la derecha como el resto del pie. gpdf tiene c.PageNumber() y c.TotalPages(), pero cada uno es un elemento independiente. ¿Cómo metes las palabras y los dos números en una sola línea sin pegar cuatro columnas?
TL;DR
Los dos placeholders son constantes de tipo string exportadas. Ponlos en medio de una cadena normal:
c.Text("Page "+document.PageNumberPlaceholder+" of "+document.TotalPagesPlaceholder,
template.AlignRight())
gpdf los sustituye después de paginar, en todas las páginas. En gpdf v1.0.14 o posterior la línea se vuelve a medir tras el cambio, así que la alineación a la derecha se mantiene cuando el número pasa de 9 a 10.
El código
Un pie de página sobre una tabla de 400 filas que se desborda a nueve páginas A4. Probado con 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, uno por página. No hay nada más que configurar.
Por qué funciona
c.PageNumber() no tiene nada de magia. Toda su implementación consiste en "añadir un document.Text cuyo contenido es document.PageNumberPlaceholder". c.TotalPages() hace lo mismo con la otra constante. Los placeholders son cadenas centinela envueltas en bytes NUL ("\x00GPDF_PAGE\x00" y "\x00GPDF_TOTAL\x00"), elegidas para que ningún texto real choque nunca con ellas.
La sustitución ocurre cuando el paginador ya ha decidido cuántas páginas hay. Recorre cada nodo de texto colocado en cada página y ejecuta strings.ReplaceAll para los dos placeholders. Le da igual si el nodo contiene solo un placeholder o una frase entera con uno en medio. Así que "Page " + PageNumberPlaceholder + " of " + TotalPagesPlaceholder recibe el mismo trato que los métodos dedicados.
Los nodos de cabecera y pie se maquetan una vez y se comparten entre las páginas físicas, así que el resolver trabaja sobre una copia por página. El pie de la página 3 recibe 3, el de la página 4 recibe 4, y tu *template.Document no se modifica, de modo que si lo renderizas una segunda vez sigue encontrando los placeholders.
Ese último detalle se pasa por alto fácilmente. El mismo truco funciona en el cuerpo del documento, lo que viene bien para una línea de portada:
c.Text(fmt.Sprintf("This report has %s pages.", document.TotalPagesPlaceholder))
Eso imprime This report has 9 pages. en la página 1 del mismo documento de nueve páginas.
Por qué nuestro propio artículo te decía que usaras cuatro columnas
En mayo, Números de página, cabeceras y pies que simplemente funcionan calificó el "Página X de Y" en una sola línea como un punto débil y mostró un apaño con cuatro columnas. Ese consejo era correcto para el gpdf de entonces, y la razón está en el orden de medición.
El motor de layout parte y alinea el texto antes de que exista el número total de páginas. Hasta v1.0.13 lo hacía con el centinela sin sustituir dentro de la cadena. \x00GPDF_TOTAL\x00 ocupa 11 bytes, así que el motor creía que Page 1 of 9 era mucho más ancho de lo que era en realidad. Volví a ejecutar el programa de arriba con v1.0.13 para ver cuánto se desvía:
| Disposición del pie | v1.0.13 | v1.0.17 |
|---|---|---|
| Columna de span 12, alineada a la derecha | borde derecho en x = 424.6 pt: 114 pt antes del margen | borde derecho en x = 538.6 pt (el margen) en las 9 páginas |
| Columna de span 3, alineada a la derecha | Page 1 of en una línea y 9 en la siguiente | Page 1 of 9 en una línea |
| Páginas de 2 dígitos (32 páginas) | — | el borde derecho se queda en 538.6 pt de Page 9 of 32 a Page 10 of 32 |
114 puntos son 40 mm. En un pie alineado a la derecha eso no parece un pequeño problema de kerning. Parece que alguien pulsó Tab. En columnas más estrechas, el commit que lo corrigió describe la versión peor: el algoritmo de partido de líneas cortaba el propio centinela, y fragmentos como ?GP y DF se imprimían tal cual.
v1.0.14 (commit b58626a) cambió dos cosas. Durante el layout, cada placeholder se mide y se parte como si fuera un solo dígito ("0"), y después se restaura el placeholder en el contenido de cada línea. Luego ResolvePageNumbersWithFonts vuelve a medir cada línea ya resuelta con la fuente real y devuelve las líneas centradas y alineadas a la derecha a su punto de anclaje. Ese segundo paso es la razón por la que Page 10 of 32 termina en la misma x que Page 9 of 32 aunque tenga un dígito más.
Si no puedes salir de v1.0.13 o anterior, el layout de varias columnas del artículo anterior sigue funcionando, porque cada placeholder va solo en su propia columna. Si no, actualiza:
go get github.com/gpdf-dev/gpdf@latest
Variantes
Estilo con barra. document.PageNumberPlaceholder + " / " + document.TotalPagesPlaceholder da 3 / 12.
fmt.Sprintf. Los placeholders son strings normales, así que %s funciona. No intentes %d: todavía no tienes los números. De eso se trata.
fmt.Sprintf("Page %s of %s", document.PageNumberPlaceholder, document.TotalPagesPlaceholder)
Japonés u otros idiomas CJK. Registra una fuente que cubra esos caracteres y asígnala al texto. Un formato como PageNumberPlaceholder + " / " + TotalPagesPlaceholder + " ページ" se resuelve igual. En el subset de la fuente solo entran los glifos que realmente uses.
JSON y Go templates. Quédate con elementos separados {"type": "pageNumber"} y {"type": "totalPages"}. Escribir \u0000GPDF_PAGE\u0000 dentro de un string JSON sí funciona en v1.0.17. Pero la API es el nombre de la constante; su valor es un detalle de implementación. Yo no apostaría una plantilla de producción a que ese valor no cambie.
Lo que todavía no se puede hacer es ramificar según la página. El closure del pie no sabe si está renderizando la página 1, así que "sin número en la portada" requiere otro enfoque. Eso merece su propia receta.
Recetas relacionadas
- Números de página, cabeceras y pies que simplemente funcionan — el modelo completo de cabeceras y pies, y dónde encaja el resolver de dos pasadas
- ¿Cómo hago que una tabla abarque varias páginas? — de ahí salen las páginas desbordadas de este ejemplo
- ¿Cómo alineo una columna al borde derecho de un Row en gpdf? — cuando prefieres el número en su propia columna a la derecha
- Guía de uso básico — todas las opciones de
template.New
Prueba gpdf
gpdf es una biblioteca Go para generar PDF. Licencia MIT, cero dependencias externas, soporte nativo de CJK.
go get github.com/gpdf-dev/gpdf